/library/Adapto/Relation/OneToMany.php
PHP | 1489 lines | 872 code | 147 blank | 470 comment | 114 complexity | 057120989577caf1c0ab8e6cb72ed818 MD5 | raw file
- <?php
- /**
- * This file is part of the Adapto Toolkit.
- * Detailed copyright and licensing information can be found
- * in the doc/COPYRIGHT and doc/LICENSE files which should be
- * included in the distribution.
- *
- * @package adapto
- * @subpackage relations
- *
- * @copyright (c)2000-2004 Ivo Jansch
- * @license http://www.achievo.org/atk/licensing ATK Open Source License
- *
- */
- /**
- * @internal Include base class.
- */
- userelation("atkrelation");
- /**
- * Only allow deletion of master item when there are no child records
- */
- define("AF_RESTRICTED_DELETE", AF_SPECIFIC_1);
- /**
- * Use pop-up dialogs for adding records
- */
- define("AF_ONETOMANY_ADD_DIALOG", AF_SPECIFIC_2);
- /**
- * Use pop-up dialog for whatever a new record must be copied or must be added.
- */
- define("AF_ONETOMANY_ADDORCOPY_DIALOG", AF_SPECIFIC_3);
- /**
- * Show the OTM in add mode.
- * Warning! Not on by default because this only works in simple cases.
- *
- * What ATK does is, when you are in OTM add mode, it stores everything you add
- * in the session, then when you're actually saving, it persists everything to
- * the database.
- *
- * However, as you may guess, not having an id will lead to strange results for:
- * - Entitys that use the foreign key in their descriptor
- * - Entitys with unique records (AF_UNIQUE always just checks the database)
- * - Combined primary keys
- */
- define("AF_ONETOMANY_SHOW_ADD", AF_SPECIFIC_4);
- /**
- * Implementation of one-to-many relationships.
- *
- * Can be used to create one to many relations ('1 library has N books').
- * A common term for this type of relation is a master-detail relationship.
- * The detailrecords can be edited inline.
- *
- * @author ijansch
- * @package adapto
- * @subpackage relations
- *
- */
- class Adapto_Relation_OneToMany extends Adapto_Relation
- {
- public $m_recordlist; // defaulted to public
- /**
- * Instance of atk.recordlist.atkrecordlistcache
- * @access private
- * @var Object
- */
- public $m_recordlistcache; // defaulted to public
- /**
- * List of keys from the destination entity that refer to the master record.
- * @access private
- * @var array
- */
- public $m_refKey = array(); // defaulted to public
- /**
- * The maximum number of detail records. If the number of detail records
- * exceeds this maximum, the link for adding new details disappears.
- * @access private
- * @var int
- */
- public $m_maxRecords = 0; // defaulted to public
- /**
- * The load method might build a custom filter. When it does, we might want
- * to use it again in other methods.
- * @access private
- * @var string
- */
- public $m_loadFilter = ""; // defaulted to public
- /**
- * The field that the foreign key in the destination points to.
- * Is set to the primary key if no value is provided.
- * @access private
- * @var array;
- */
- public $m_ownerFields = array(); // defaulted to public
- /**
- * Use destination filter for autolink add link?
- *
- * @access private
- * @var boolean
- */
- public $m_useFilterForAddLink = true; // defaulted to public
- /**
- * Use destination filter for edit link (edit button)?
- *
- * @access private
- * @var boolean
- */
- public $m_useFilterForEditLink = true; // defaulted to public
- /**
- * Use referential key for load filter?
- *
- * @var boolean
- */
- protected $m_useRefKeyForFilter = true;
- /**
- * Function names for recordlist header/footer generation
- *
- * @access private
- * @var string
- */
- public $m_headerName = ""; // defaulted to public
- public $m_footerName = ""; // defaulted to public
- /**
- * Fields to exclude in the grid
- *
- * @access private
- * @var array
- */
- public $m_excludes = array(); // defaulted to public
- /**
- * Default constructor.
- *
- * <b>Example: </b> Suppose a department has many employees. To edit the
- * list of employees in a department, this relationship can be built like
- * this, in the department entity:
- * <code>
- * $this->add(new Adapto_Relation_OneToMany("employees", "mymod.employee", "department_id"));
- * </code>
- *
- * @param String $name The unique name of this relation within an entity.
- * In contrast with most other attributes, the name
- * does not correspond to a database field. (Because
- * in one2many relations, the databasefield that
- * stores the link, is in the destination entity and not
- * in the owner entity).
- * @param String $destination The entity to which the relationship is made
- * (in module.entityname notation).
- * @param mixed $refKey For regular oneToMany relationships, $refKey is
- * name of the referential key in the destination
- * entity. In the case of multi-foreign key
- * relationships, $refKey can be an array of fields.
- * @param int $flags Attribute flags that influence this attributes'
- * behavior.
- */
- public function __construct($name, $destination, $refKey = "", $flags = 0)
- {
- parent::__construct($name, $destination, $flags | AF_NO_SORT | AF_HIDE_ADD);
- if (is_array($refKey)) {
- $this->m_refKey = $refKey;
- } else if (empty($refKey)) {
- $this->m_refKey = array();
- } else {
- $this->m_refKey[] = $refKey;
- }
- $this->setGridExcludes($this->m_refKey);
- }
- public function addFlag($flag)
- {
- $ret = parent::addFlag($flag);
- if (hasFlag($this->m_flags, AF_ONETOMANY_SHOW_ADD)) {
- $this->removeFlag(AF_HIDE_ADD);
- }
- return $ret;
- }
- /**
- * Set the ownerfields
- *
- * @param array $ownerfields
- */
- function setOwnerFields($ownerfields)
- {
- $this->m_ownerFields = $ownerfields;
- }
- /**
- * Get the owner fields
- *
- * @return mixed Array or String with ownerfield(s)
- */
- function getOwnerFields()
- {
- if (is_array($this->m_ownerFields) && count($this->m_ownerFields) > 0) {
- return $this->m_ownerFields;
- }
- return $this->m_ownerInstance->m_primaryKey;
- }
- /**
- * Use destination filter for auto add link?
- *
- * @param boolean $useFilter use destination filter for add link?
- */
- function setUseFilterForAddLink($useFilter)
- {
- $this->m_useFilterForAddLink = $useFilter;
- }
- /**
- * Use destination filter for edit link (edit button)?
- *
- * @param boolean $useFilter use destnation filter for edit link (edit button)?
- */
- function setUseFilterForEditLink($useFilter)
- {
- $this->m_useFilterForEditLink = $useFilter;
- }
- /**
- * Use referential key for filtering the records. If you disable this only the
- * explicitly set destination filter will be used.
- *
- * @param bool $useRefKey
- */
- public function setUseRefKeyForFilter($useRefKey)
- {
- $this->m_useRefKeyForFilter = $useRefKey;
- }
- /**
- * Create the datagrid for the edit and display actions. The datagrid is
- * configured with the correct entity filter, excludes etc.
- *
- * The datagrid uses for both the edit and display actions the partial_grid
- * method to update it's view.
- *
- * @param array $record the record
- * @param string $mode the mode
- * @param string $action the action
- * @param boolean $useSession use session?
- *
- * @return atkDataGrid grid
- */
- protected function createGrid($record, $mode, $action, $useSession = true)
- {
- $this->createDestination();
- $grid = atkDataGrid::create($this->m_destInstance, str_replace('.', '_', $this->getOwnerInstance()->atkEntityType()) . '_' . $this->fieldName() . '_grid',
- null, true, $useSession);
- $grid->setMode($mode);
- $grid->setMasterEntity($this->getOwnerInstance());
- $grid->setMasterRecord($record);
- $grid->removeFlag(atkDataGrid::EXTENDED_SEARCH);
- if ($action == 'view') {
- $grid->removeFlag(atkDataGrid::MULTI_RECORD_ACTIONS);
- $grid->removeFlag(atkDataGrid::MULTI_RECORD_PRIORITY_ACTIONS);
- $grid->removeFlag(atkDataGrid::LOCKING);
- }
- $grid->setBaseUrl(partial_url($this->getOwnerInstance()->atkEntityType(), $action, 'attribute.' . $this->fieldName() . '.grid'));
- $grid->setExcludes($this->getGridExcludes());
- $grid->addFilter($this->_getLoadWhereClause($record));
- if ($this->m_destinationFilter != '') {
- $grid->addFilter($this->parseFilter($this->m_destinationFilter, $record));
- }
- $this->modifyDataGrid($grid, atkDataGrid::CREATE);
- return $grid;
- }
- /**
- * Updates the datagrid for the edit and display actions.
- *
- * @return string grid html
- */
- public function partial_grid()
- {
- $this->createDestination();
- $entity = $this->getDestination();
- try {
- $grid = atkDataGrid::resume($entity);
- $this->modifyDataGrid($grid, atkDataGrid::RESUME);
- } catch (Exception $e) {
- $grid = atkDataGrid::create($entity);
- $this->modifyDataGrid($grid, atkDataGrid::CREATE);
- }
- return $grid->render();
- }
- /**
- * Modify grid.
- *
- * @param atkDataGrid $grid grid
- * @param int $mode CREATE or RESUME
- */
- protected function modifyDataGrid(atkDataGrid $grid, $mode)
- {
- $method = 'modifyDataGrid';
- if (method_exists($this->getDestination(), $method)) {
- $this->getDestination()->$method($grid, $mode);
- }
- $method = $this->fieldName() . '_modifyDataGrid';
- if (method_exists($this->getOwnerInstance(), $method)) {
- $this->getOwnerInstance()->$method($grid, $mode);
- }
- }
- /**
- * Returns a displayable string for this value, to be used in HTML pages.
- *
- * The Adapto_Relation_OneToMany displays a list of detail records in "view"
- * mode, in the form of a read-only data grid. In "list" mode, a plain
- * list of detail record descriptors is displayed.
- *
- * @param array $record The record that holds the value for this attribute
- * @param String $mode The display mode ("view" for viewpages, or "list"
- * for displaying in recordlists)
- * @return String HTML String
- */
- public function display($record, $mode = "list")
- {
- // for the view mode we use the datagrid and load the records ourselves
- if ($mode == 'view' || ($mode == 'edit' && $this->hasFlag(AF_READONLY_EDIT))) {
- $grid = $this->createGrid($record, 'admin', 'view');
- $grid->loadRecords(); // load records early
- $grid->setEmbedded(false);
- // no records
- if ($grid->getCount() == 0) {
- if (!in_array($mode, array("csv", "plain"))) {
- return $this->text("none");
- } else {
- return '';
- }
- }
- $actions = array();
- if (!$this->m_destInstance->hasFlag(EF_NO_VIEW)) {
- $actions['view'] = dispatch_url($this->m_destination, "view", array("atkselector" => "[pk]", "atkfilter" => $this->m_destinationFilter));
- }
- $grid->setDefaultActions($actions);
- return $grid->render();
- }
- // records should be loaded inside the load method
- $records = $record[$this->fieldName()];
- // no records
- if (count($records) == 0) {
- return $this->text('none');
- }
- if ($mode == "list") // list mode
- {
- $result = "<ul>";
- foreach ($records as $current) {
- $result .= sprintf("<li>%s</li>", $this->m_destInstance->descriptor($current));
- }
- $result .= "</ul>";
- return $result;
- } else // cvs / plain mode
- {
- $result = "";
- foreach ($records as $i => $current) {
- $result .= ($i > 0 ? ', ' : '') . $this->m_destInstance->descriptor($current);
- }
- return $result;
- }
- }
- /**
- * Returns a piece of html code that can be used in a form to edit this
- * attribute's value.
- *
- * The Adapto_Relation_OneToMany's edit method returns a recordlist in which
- * detail records can be removed, added and edited.
- *
- * @param array $record The record that holds the value for this attribute.
- * @param String $fieldprefix The fieldprefix to put in front of the name
- * of any html form element for this attribute.
- * @param String $mode The mode we're in ('add' or 'edit')
- *
- * @return String A piece of htmlcode for editing this attribute
- */
- public function edit($record = "", $fieldprefix = "", $mode = '')
- {
- $page = Adapto_ClassLoader::getInstance('atk.ui.atkpage');
- $page->register_script(Adapto_Config::getGlobal("atkroot") . "atk/javascript/tools.js");
- $page->register_script(Adapto_Config::getGlobal("atkroot") . "atk/javascript/class.atkonetomanyrelation.js");
- $grid = $this->createGrid($record, 'admin', $mode);
- $params = array();
- if ($this->m_useFilterForEditLink && $this->m_destinationFilter != "") {
- $params["atkfilter"] = $this->m_destinationFilter;
- }
- if ($mode === 'add') {
- //All actions in the grid should be done in session store mode
- $params['atkstore'] = 'session';
- $params['atkstore_key'] = $this->getSessionStoreKey();
- // Make the grid use the OTM Session Grid Handler
- // which makes the grid get it's records from the session.
- $handler = new Adapto_OneToManyRelationSessionGridHandler($this->getSessionStoreKey());
- $grid->setCountHandler(array($handler, 'countHandlerForAdd'));
- $grid->setSelectHandler(array($handler, 'selectHandlerForAdd'));
- // No searching and sorting on session data... for now...
- $grid->removeFlag(atkDataGrid::SEARCH);
- $grid->removeFlag(atkDataGrid::SORT);
- $grid->removeFlag(atkDataGrid::EXTENDED_SORT);
- }
- $actions = $this->m_destInstance->defaultActions("relation", $params);
- $grid->setDefaultActions($actions);
- $grid->loadRecords(); // force early load of records
- $output = $this->editHeader($record, $grid->getRecords()) . $grid->render() . $this->editFooter($record, $grid->getRecords());
- if ($this->m_destInstance->allowed("add")) {
- $this->_addAddToEditOutput($output, $grid->getRecords(), $record, $mode, $fieldprefix);
- }
- return $output;
- }
- /**
- * Adds the 'add' option to the onetomany, either integrated or as a link
- *
- * @param String $output The HTML output of the edit function
- * @param Array $myrecords The records that are loaded into the recordlist
- * @param array $record The master record that is being edited.
- */
- function _addAddToEditOutput(&$output, $myrecords, $record, $mode = "", $fieldprefix = "")
- {
- $add_link = '';
- if (!$this->getDestination()->hasFlag(EF_NO_ADD)) {
- $add_link = $this->_getAddLink($myrecords, $record, true, $mode, $fieldprefix);
- }
- $add_link .= '<br />';
- if (Adapto_Config::getGlobal("onetomany_addlink_position", "bottom") == "top") {
- $output = $add_link . $output;
- } else if (Adapto_Config::getGlobal("onetomany_addlink_position", "bottom") == "bottom") {
- $output .= $add_link;
- }
- }
- /**
- * Get the buttons for the embedded mode of the onetomany relation.
- * @todo Move this to a template
- * @return String The HTML buttons
- */
- function _getEmbeddedButtons()
- {
- $fname = $this->fieldName();
- $output .= '<input type="submit" class="otm_add" name="' . $fname . '_save" value="' . atktext("add") . '">';
- return $output . '<input type="button" onClick="toggleAddForm(\'' . $fname
- . "_integrated',
- '" . $fname
- . "_integrated_link');\"
- class=\"otm_add\" name=\"" . $fname . "_cancel\" value=\"" . atktext("cancel") . '">';
- }
- /**
- * Internal function to get the add link for a Adapto_Relation_OneToMany
- * @param Array $myrecords The load of all attributes (see comment in edit() code)
- * @param Array $record The record that holds the value for this attribute.
- * @param bool $saveform Save the form values?
- * @return String The link to add records to the onetomany
- */
- function _getAddLink($myrecords, $record, $saveform = true, $mode = "", $fieldprefix = "")
- {
- $params = array();
- if ($mode === 'add') {
- $ownerfields = $this->getOwnerFields();
- foreach ($ownerfields as $ownerfield) {
- $record[$ownerfield] = $this->getSessionAddFakeId();
- }
- $params['atkstore'] = 'session';
- $params['atkstore_key'] = $this->getSessionStoreKey();
- }
- $is_addorcopy_mode = $this->hasFlag(AF_ONETOMANY_ADDORCOPY_DIALOG) || $this->m_destInstance->hasFlag(EF_ADDORCOPY_DIALOG);
- if ($is_addorcopy_mode) {
- $filter = $this->getAddFilterString($record);
- $showDialog = atkAddOrCopyHandler::hasCopyableRecords($this->m_destInstance, $filter);
- if ($showDialog) {
- return $this->_getDialogAddLink($record, 'addorcopy', $params);
- }
- }
- $is_dialog_mode = $this->hasFlag(AF_ONETOMANY_ADD_DIALOG) || $this->m_destInstance->hasFlag(EF_ADD_DIALOG);
- if ($is_dialog_mode)
- return $this->_getDialogAddLink($record, 'add', $params);
- else
- return $this->_getNestedAddLink($myrecords, $record, $saveform, $fieldprefix, $params);
- }
- /**
- * Return a fake ID for adding to the session.
- *
- * We use a high negative number because we have to sneak this in
- * as if it's a REAL id for the owner, tricking MTOs in the destination
- * that point back to us into thinking they already have an id.
- * But we also have to make sure it's recognizable, so when we
- * persist the records from the session to the database, then
- * we can set the proper id.
- *
- * @return string
- */
- public function getSessionAddFakeId()
- {
- return "-999999";
- }
- /**
- * Return the key to use when storing records for the OTM destination
- * in the session if the OTM is used in add mode.
- *
- * @return string
- */
- public function getSessionStoreKey()
- {
- return $this->getOwnerInstance()->atkEntityType() . ':' . $this->fieldName();
- }
- /**
- * Uses the given record to create an add filter string.
- *
- * @param array $record
- * @return string filter string
- */
- function getAddFilterString($record)
- {
- $filterelems = $this->_getFilterElements($record);
- $strfilter = implode(" AND ", $filterelems);
- if ($this->m_useFilterForAddLink && $this->m_destinationFilter != "") {
- $strfilter .= ' AND ' . $this->parseFilter($this->m_destinationFilter, $record);
- }
- return $strfilter;
- }
- /**
- * Get the add link when using a dialog
- *
- * @param array $record
- * @param string $action
- * @return string The dialog add link html-code
- */
- function _getDialogAddLink($record, $action, $params = array())
- {
- $ui = &$this->m_ownerInstance->getUi();
- $filter = $this->getAddFilterString($record);
- if (!empty($filter)) {
- $params['atkfilter'] = $filter;
- }
- $dialog = new Adapto_Dialog($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.' . $action . '_dialog', $params);
- $title = $ui->title($this->m_destInstance->m_module, $this->m_destInstance->m_type, $action);
- $dialog->setTitle($title);
- $dialog->setModifierObject($this->m_destInstance);
- $dialog->setSessionStatus(SESSION_PARTIAL);
- $onClick = $dialog->getCall();
- return '<a href="javascript:void(0)" onclick="' . $onClick . '" class="valignMiddle">' . $this->getAddLabel() . '</a>';
- }
- /**
- * Internal function to get the add link for a Adapto_Relation_OneToMany.
- *
- * @param Array $myrecords The load of all attributes (see comment in edit() code)
- * @param Array $record The record that holds the value for this attribute.
- * @param bool $saveform Save the values of the form?
- * @return String The link to add records to the onetomany
- */
- function _getNestedAddLink($myrecords, $record, $saveform = true, $fieldprefix = '', $params = array())
- {
- $url = "";
- if ((int) $this->m_maxRecords !== 0 && $this->m_maxRecords <= count($myrecords))
- return $url;
- if (!$this->createDestination())
- return $url;
- if ($this->m_destInstance->hasFlag(EF_NO_ADD))
- return $url;
- $filter = $this->getAddFilterString($record);
- if (!empty($filter))
- $params['atkfilter'] = $filter;
- $onchange = '';
- if (count($this->m_onchangecode)) {
- $onchange = 'onChange="' . $this->fieldName() . '_onChange(this);"';
- $this->_renderChangeHandler($fieldprefix);
- }
- $add_url = $this->getAddURL($params);
- $label = $this->getAddLabel();
- return href($add_url, $label, SESSION_NESTED, $saveform, $onchange . ' class="atkonetomanyrelation"');
- }
- /**
- * Get filter elements
- *
- * @param array $record
- * @return array Array with filter elements
- */
- function _getFilterElements($record)
- {
- $filterelems = array();
- $ownerfields = $this->getOwnerFields();
- if ($this->destinationHasRelation()) {
- // we need to set the filter of the record we are going to add.
- // The referential key must be set to the value of the current
- // primary key.
- $this->createDestination();
- for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
- $primkeyattr = &$this->m_ownerInstance->m_attribList[$ownerfields[$i]];
- $value = $primkeyattr->value2db($record);
- if (!$value)
- continue;
- $filterelems[] = $this->m_refKey[0] . "." . $ownerfields[$i] . "='" . $this->escapeSQL($value) . "'";
- }
- } else {
- for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
- $value = $record[$ownerfields[$i]];
- if (!$value)
- continue;
- $filterelems[] = $this->_addTablePrefix($this->m_refKey[$i]) . "='" . $this->escapeSQL($value) . "'";
- }
- }
- return $filterelems;
- }
- /**
- * Prefix the passed column name with the table name if there is no prefix in the column name yet.
- *
- * @param string $columnName
- * @param string $destAlias
- * @return string
- */
- function _addTablePrefix($columnName, $destAlias = '')
- {
- $prefix = '';
- if (strpos($columnName, '.') === false) {
- $prefix = $destAlias ? $destAlias : ($this->m_destInstance->getTable());
- $prefix .= '.';
- }
- return $prefix . $columnName;
- }
- protected function getAddURL($params = array())
- {
- return dispatch_url($this->m_destination, "add", $params);
- }
- /**
- * Attempts to get a translated label which can be used when composing an "add" link
- *
- * @return String Localised "add" label
- */
- function getAddLabel()
- {
- $key = "link_" . $this->fieldName() . "_add";
- $label = atktext($key, $this->m_ownerInstance->m_module, $this->m_ownerInstance->m_type, "", "", true);
- if ($label == "") {
- $key = "link_" . $this->fieldName() . "_add";
- $label = atktext($key, $this->m_destInstance->m_module, "", "", "", true);
- if ($label == "") {
- $key = "link_" . getEntityType($this->m_destination) . "_add";
- $label = atktext($key, $this->m_destInstance->m_module, "", "", "", true);
- if ($label == "") {
- $label = atktext(getEntityType($this->m_destination), $this->m_destInstance->m_module) . " " . strtolower(atktext("add", "atk"));
- }
- }
- }
- return $label;
- }
- /**
- * Retrieve header for the recordlist.
- *
- * The regular Adapto_Relation_OneToMany has no implementation for this method,
- * but it may be overridden in derived classes to add extra information
- * (text, links, whatever) to the top of the attribute, right before the
- * recordlist. This is similar to the adminHeader() method in atkEntity.
- *
- * @param array $record The master record that is being edited.
- * @param array $childrecords The childrecords in this master/detail
- * relationship.
- * @return String a String to be added to the header of the recordlist.
- */
- function editHeader($record = NULL, $childrecords = NULL)
- {
- if (!empty($this->m_headerName)) {
- $methodname = $this->m_headerName;
- return $this->m_ownerInstance->$methodname($record, $childrecords, $this);
- } else
- return "";
- }
- /**
- * Retrieve footer for the recordlist.
- *
- * The regular Adapto_Relation_OneToMany has no implementation for this method,
- * but it may be overridden in derived classes to add extra information
- * (text, links, whatever) to the bottom of the attribute, just after the
- * recordlist. This is similar to the adminFooter() method in atkEntity.
- *
- * @param array $record The master record that is being edited.
- * @param array $childrecords The childrecords in this master/detail
- * relationship.
- * @return String a String to be added at the bottom of the recordlist.
- */
- function editFooter($record = NULL, $childrecords = NULL)
- {
- if (!empty($this->m_footerName)) {
- $methodname = $this->m_footerName;
- return $this->m_ownerInstance->$methodname($record, $childrecords, $this);
- } else
- return "";
- }
- /**
- * Create the where clause for the referential key that is used to
- * retrieve the destination records.
- * @access private
- *
- * @param array $record The master record
- * @return String SQL where clause
- */
- function _getLoadWhereClause($record)
- {
- if (!$this->m_useRefKeyForFilter)
- return '';
- $whereelems = array();
- if (count($this->m_refKey) == 0 || $this->m_refKey[0] == "")
- $this->m_refKey[0] = $this->m_owner;
- $ownerfields = $this->getOwnerFields();
- for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
- $primkeyattr = &$this->m_ownerInstance->m_attribList[$ownerfields[$i]];
- if (!$primkeyattr->isEmpty($record)) {
- $whereelems[] = $this->_addTablePrefix($this->m_refKey[$i]) . "='" . $primkeyattr->value2db($record) . "'";
- }
- }
- $result = implode(" AND ", $whereelems);
- return $result == '' ? '1=0' : $result;
- }
- /**
- * Define a dummy function to use as a dummy handler function in load() below
- */
- public function ___dummyCount()
- {
- }
- /**
- * Retrieve detail records from the database.
- *
- * Called by the framework to load the detail records.
- *
- * @param atkDb $db The database used by the entity.
- * @param array $record The master record
- * @param String $mode The mode for loading (admin, select, copy, etc)
- * @param bool $paging divide the result records on multiple pages ($config_recordsperpage)
- *
- * @return array Recordset containing detailrecords, or NULL if no detail
- * records are present. Note: when $mode is edit, this
- * method will always return NULL. This is a framework
- * optimization because in edit pages, the records are
- * loaded on the fly.
- */
- function load(&$db, $record, $mode = "", $paging = false)
- {
- $result = null;
- // for edit and view mode we don't load any records unless a display override exists
- // we use the grid to load records because it makes things easier
- if (($mode != 'add' && $mode != 'edit' && $mode != 'view')
- || ($mode == 'view' && method_exists($this->getOwnerInstance(), $this->fieldName() . "_display"))
- || ($mode == 'edit' && $this->hasFlag(AF_READONLY_EDIT) && method_exists($this->getOwnerInstance(), $this->fieldName() . "_display"))) {
- $grid = $this->createGrid($record, $mode == 'copy' ? 'copy' : 'admin', $mode, false);
- $grid->setPostvar('atklimit', -1); // all records
- $grid->setCountHandler(array($this, '___dummyCount')); // don't count
- $grid->loadRecords();
- $result = $grid->getRecords();
- $grid->destroy(); // clean-up
- }
- return $result;
- }
- /**
- * Override isEmpty function - in a oneToMany relation we should check if the
- * relation contains any records. When there aren't any, the relation is empty,
- * otherwise it isn't
- *
- * @param array &$record The record to check
- * @return bool true if a destination record is present. False if not.
- */
- function isEmpty($record)
- {
- if (!isset($record[$this->fieldName()]) || (is_array($record[$this->fieldName()]) && count($record[$this->fieldName()]) == 0)) {
- // empty. It might be that the record has not yet been fetched. In this case, we do
- // a forced load to see if it's really empty.
- $recs = $this->load($this->m_ownerInstance->getDb(), $record);
- return (count($recs) == 0);
- }
- return false;
- }
- /**
- * The delete method is called by the framework to inform the attribute
- * that the master record is deleted.
- *
- * Note that the framework only calls the method when the
- * AF_CASCADE_DELETE flag is set. When calling this method, all detail
- * records belonging to the master record are deleted.
- *
- * @param array $record The record that is deleted.
- * @return boolean true if cleanup was successful, false otherwise.
- */
- function delete($record)
- {
- $classname = $this->m_destination;
- $cache_id = $this->m_owner . "." . $this->m_name;
- $rel = &getEntity($classname, $cache_id);
- $ownerfields = $this->getOwnerFields();
- for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
- $primkeyattr = &$this->m_ownerInstance->m_attribList[$ownerfields[$i]];
- $whereelems[] = $this->_addTablePrefix($this->m_refKey[$i]) . "='" . $primkeyattr->value2db($record) . "'";
- }
- $where = implode(" AND ", $whereelems);
- if ($where != "") // double check, so we never by accident delete the entire db
- {
- return $rel->deleteDb($where);
- }
- return true;
- }
- /**
- * Store detail records in the database.
- *
- * For onetomanyrelation, this function does not have much use, since it
- * stores records using its 'add link'.
- * There are however two modes that use this:
- * - 'copy' mode
- * The copyDb function, to clone detail records.
- * - 'add' mode
- * When the OTM was used in add mode, we have to transfer
- * the records stored in the session to the database.
- *
- * other than those this method does not do anything.
- *
- * @param atkDb $db The database used by the entity.
- * @param array $record The master record which has the detail records
- * embedded.
- * @param string $mode The mode we're in ("add", "edit", "copy")
- * @return boolean true if store was successful, false otherwise.
- */
- function store(atkDb $db, $record, $mode)
- {
- switch ($mode) {
- case 'add':
- return $this->storeAdd($db, $record, $mode);
- case 'copy':
- return $this->storeCopy($db, $record, $mode);
- default:
- return true;
- }
- }
- /**
- * Persist records from the session (in add mode) to the database.
- *
- * @param atkDb $db
- * @param array $record
- * @param string $mode
- * @return bool
- */
- private function storeAdd(atkDb $db, $record, $mode)
- {
- if (!$this->createDestination())
- return false;
- $rows = atkSessionStore::getInstance($this->getSessionStoreKey())->getData();
- foreach ($rows as $row) {
- $this->updateSessionAddFakeId($row, $this->getSessionAddFakeId(), $record);
- $this->m_destInstance->addDb($row);
- }
- // after saving the rows, we can clear the sessionstore
- atkSessionStore::getInstance($this->getSessionStoreKey())->setData(null);
- return true;
- }
- /**
- * Recursive method to look for the fake id in the record and replace it
- * with the proper id.
- *
- * @param array $row Destination record
- * @param mixed $id Fake id to look for
- * @param array $record Owner record
- */
- private function updateSessionAddFakeId(&$row, $id, $record)
- {
- $row_keys = array_keys($row);
- foreach ($row_keys as $key) {
- if (is_array($row[$key])) {
- $this->updateSessionAddFakeId($row[$key], $id, $record);
- } else {
- if ($row[$key] === $id)
- $row[$key] = $record[$key];
- }
- }
- }
- /**
- * Copy detail records.
- *
- * @param atkDb $db Datbase connection to use
- * @param array $record Owner record
- * @param string $mode Mode ('copy')
- * @return bool
- */
- private function storeCopy(atkDb $db, $record, $mode)
- {
- $onetomanyrecs = $record[$this->fieldName()];
- if (!is_array($onetomanyrecs) || count($onetomanyrecs) <= 0)
- return true;
- if (!$this->createDestination())
- return true;
- $ownerfields = $this->getOwnerFields();
- for ($i = 0; $i < count($onetomanyrecs); $i++) {
- // original record
- $original = $onetomanyrecs[$i];
- $onetomanyrecs[$i]['atkorgrec'] = $original;
- // the referential key of the onetomanyrecs could be wrong, if we
- // are called for example from a copy function. So just in case,
- // we reset the correct key.
- if (!$this->destinationHasRelation()) {
- for ($j = 0, $_j = count($this->m_refKey); $j < $_j; $j++) {
- $onetomanyrecs[$i][$this->m_refKey[$j]] = $record[$ownerfields[$j]];
- }
- } else {
- for ($j = 0, $_j = count($this->m_refKey); $j < $_j; $j++) {
- $onetomanyrecs[$i][$this->m_refKey[0]][$ownerfields[$j]] = $record[$ownerfields[$j]];
- }
- }
- if (!$this->m_destInstance->addDb($onetomanyrecs[$i], true, $mode)) {
- // error
- return false;
- }
- }
- return true;
- }
- /**
- * Returns a piece of html code for hiding this attribute in an HTML form.
- *
- * Because the oneToMany has nothing to hide, we override the default
- * hide() implementation with a dummy method.
- *
- * @return String An empty string.
- */
- function hide($record = '', $fieldprefix = '')
- {
- //Nothing to hide..
- return "";
- }
- /**
- * Retrieve the list of searchmodes supported by the attribute.
- *
- * Note that not all modes may be supported by the database driver.
- * Compare this list to the one returned by the databasedriver, to
- * determine which searchmodes may be used.
- *
- * @return array List of supported searchmodes
- */
- function getSearchModes()
- {
- return array('substring');
- }
- /**
- * Returns the condition (SQL) that should be used when we want to join an owner
- * entity with the destination entity of the Adapto_Relation_OneToMany.
- *
- * @param atkQuery $query The query object.
- * @param String $ownerAlias The owner table alias.
- * @param String $destAlias The destination table alias.
- *
- * @return String SQL string for joining the owner with the destination.
- */
- function getJoinCondition(&$query, $ownerAlias = "", $destAlias = "")
- {
- if (!$this->createDestination())
- return false;
- if ($ownerAlias == "")
- $ownerAlias = $this->m_ownerInstance->m_table;
- $conditions = array();
- $ownerfields = $this->getOwnerFields();
- for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
- $conditions[] = $this->_addTablePrefix($this->m_refKey[$i], $destAlias) . "=" . $ownerAlias . "." . $ownerfields[$i];
- }
- return implode(" AND ", $conditions);
- }
- /**
- * Creates a smart search condition for a given search value, and adds it
- * to the query that will be used for performing the actual search.
- *
- * @param Integer $id The unique smart search criterium identifier.
- * @param Integer $nr The element number in the path.
- * @param Array $path The remaining attribute path.
- * @param atkQuery $query The query to which the condition will be added.
- * @param String $ownerAlias The owner table alias to use.
- * @param Mixed $value The value the user has entered in the searchbox.
- * @param String $mode The searchmode to use.
- */
- function smartSearchCondition($id, $nr, $path, &$query, $ownerAlias, $value, $mode)
- {
- // one-to-many join means we need to perform a distinct select
- $query->setDistinct(true);
- if (count($path) > 0) {
- $this->createDestination();
- $destAlias = "ss_{$id}_{$nr}_" . $this->fieldName();
- $query
- ->addJoin($this->m_destInstance->m_table, $destAlias, $this->getJoinCondition($query, $ownerAlias, $destAlias), false);
- $attrName = array_shift($path);
- $attr = &$this->m_destInstance->getAttribute($attrName);
- if (is_object($attr)) {
- $attr->smartSearchCondition($id, $nr + 1, $path, $query, $destAlias, $value, $mode);
- }
- } else {
- $this->searchCondition($query, $ownerAlias, $value, $mode);
- }
- }
- /**
- * Adds a search condition for a given search value
- *
- * @param atkQuery $query The query to which the condition will be added.
- * @param String $table The name of the table in which this attribute
- * is stored
- * @param mixed $value The value the user has entered in the searchbox
- * @param String $searchmode The searchmode to use. This can be any one
- * of the supported modes, as returned by this
- * attribute's getSearchModes() method.
- * @param string $fieldaliasprefix optional prefix for the fieldalias in the table
- */
- function searchCondition(&$query, $table, $value, $searchmode, $fieldaliasprefix = '')
- {
- if ($this->createDestination()) {
- $searchcondition = $this->getSearchCondition($query, $table, $value, $searchmode);
- if (!empty($searchcondition)) {
- $query->addSearchCondition($searchcondition);
- $query->setDistinct(true);
- // @todo: is this still needed?
- if ($this->m_ownerInstance->m_postvars["atkselector"]) {
- $query->addTable($this->m_destInstance->m_table);
- $query->addCondition($this->translateSelector($this->m_ownerInstance->m_postvars['atkselector']));
- }
- }
- }
- }
- /**
- * Creates a searchcondition for the field,
- * was once part of searchCondition, however,
- * searchcondition() also immediately adds the search condition.
- *
- * @param atkQuery $query The query object where the search condition should be placed on
- * @param String $table The name of the table in which this attribute
- * is stored
- * @param mixed $value The value the user has entered in the searchbox
- * @param String $searchmode The searchmode to use. This can be any one
- * of the supported modes, as returned by this
- * attribute's getSearchModes() method.
- * @return String The searchcondition to use.
- */
- function getSearchCondition(&$query, $table, $value, $searchmode)
- {
- $usedfields = array();
- $searchconditions = array();
- if (!is_array($value)) {
- foreach ($this->m_destInstance->descriptorFields() as $field) {
- if (!in_array($field, $usedfields)) {
- $sc = $this->_callSearchConditionOnDestField($query, $this->m_destInstance->m_table, $value, $searchmode, $field, $table);
- if (!empty($sc))
- $searchconditions[] = $sc;
- $usedfields[] = $field;
- }
- }
- } else {
- foreach ($value as $key => $val) {
- if ($val) {
- $sc = $this->_callSearchConditionOnDestField($query, $this->m_destInstance->m_table, $val, $searchmode, $key, $table);
- if (!empty($sc))
- $searchconditions[] = $sc;
- }
- }
- }
- if (count($searchconditions) > 0)
- return "(" . implode(" OR ", $searchconditions) . ")";
- else
- return false;
- }
- /**
- * Calls searchCondition on an attribute in the destination
- * To hook the destination attribute on the query
- * @param atkQuery &$query The query object
- * @param String $table The table to search on
- * @param mixed $value The value to search
- * @param mixed $searchmode The mode used when searching
- * @param String $field The name of the attribute
- * @param String $reftable
- */
- function _callSearchConditionOnDestField(&$query, $table, $value, $searchmode, $field, $reftable)
- {
- if ($this->createDestination()) {
- $alias = $this->fieldName() . "_AE_" . $this->m_destInstance->m_table;
- $attr = &$this->m_destInstance->getAttribute($field);
- $query->addJoin($table, $alias, $this->getJoinCondition($query, $reftable, $alias), false);
- return $attr->getSearchCondition($query, $alias, $value, $searchmode);
- }
- }
- /**
- * Determine the type of the foreign key on the other side.
- *
- * On the other side of a oneToManyRelation (in the destination entity),
- * there may be a regular atkAttribute for the referential key, or an
- * atkManyToOneRelation pointing back at the source. This method discovers
- * which of the 2 cases we are dealing with.
- * @return boolean True if the foreign key on the other side is a
- * relation, false if not.
- */
- function destinationHasRelation()
- {
- if ($this->createDestination()) {
- // If there's a relation back, it's in the destination entity under the name of the first refkey element.
- $attrib = $this->m_destInstance->m_attribList[$this->m_refKey[0]];
- if (is_object($attrib) && strpos(get_class($attrib), "elation") !== false)
- return true;
- }
- return false;
- }
- /**
- * Are we allowed to delete a record?
- *
- * @return mixed bool if allowed or string with not allowed message
- */
- function deleteAllowed()
- {
- if ($this->hasFlag(AF_RESTRICTED_DELETE)) {
- // Get the destination entity
- $classname = $this->m_destination;
- $cache_id = $this->m_owner . "." . $this->m_name;
- $rel = &getEntity($classname, $cache_id);
- // Get the current atkselector
- $where = $this->translateSelector($this->m_ownerInstance->m_postvars['atkselector']);
- if ($where) {
- $childrecords = $rel->selectDb($where);
- if (!empty($childrecords))
- return atktext("restricted_delete_error");
- } else
- return;
- }
- return true;
- }
- /**
- * Here we check if the selector is on the owner or on the destination
- * if it's on the destination, we leave it alone.
- * Otherwise we translate it back to the destination.
- *
- * @todo when we translate the selector, we get the last used refKey
- * but how do we know what is the right one?
- * @param string $selector the selector we have to translate
- * @return string the new selector
- */
- function translateSelector($selector)
- {
- // All standard SQL operators
- $sqloperators = array('=', '<>', '>', '<', '>=', '<=', 'BETWEEN', 'LIKE', 'IN');
- $this->createDestination();
- // Check the filter for every SQL operators
- for ($counter = 0; $counter < count($sqloperators); $counter++) {
- if ($sqloperators[$counter]) {
- list($key, $value) = explode($sqloperators[$counter], $selector);
- // if the operator is in the filter
- if ($value) {
- // check if it's on the destination
- for ($refkeycount = 0; $refkeycount < count($this->m_refKey); $refkeycount++) {
- $destinationkey = $this->m_destInstance->m_table . "." . $this->m_refKey[$refkeycount];
- // if the selector is on the destination, we pass it back
- if ($key == $destinationkey || $key == $this->m_refKey[$refkeycount]) {
- return $selector;
- }
- }
- // otherwise we set it on the destination
- return $destinationkey . $sqloperators[$counter] . $value;
- }
- }
- }
- // We never found a value, something is wrong with the filter
- return "";
- }
- /**
- * Add dialog.
- */
- function partial_add_dialog()
- {
- $this->createDestination();
- $this->m_destInstance->m_partial = 'dialog';
- $handler = &$this->m_destInstance->getHandler('add');
- $handler->m_postvars = $this->m_ownerInstance->m_postvars;
- // Reset postvars of ownerinstance because it might interfere with relations
- // which point back to this ownerinstance and it doesn't need them anymore anyway.
- $this->m_ownerInstance->m_postvars = array();
- $handler->setDialogSaveUrl(partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.add_process'));
- $result = $handler->renderAddDialog();
- $page = &$this->m_ownerInstance->getPage();
- $page->addContent($result);
- }
- /**
- * Process add dialog save action.
- */
- function partial_add_process()
- {
- $this->createDestination();
- $handler = &$this->m_destInstance->getHandler('save');
- $handler->m_postvars = $this->m_ownerInstance->m_postvars;
- // Reset postvars of ownerinstance because it might interfere with relations
- // which point back to this ownerinstance and it doesn't need them anymore anyway.
- $this->m_ownerInstance->m_postvars = array();
- $handler->setDialogSaveUrl(partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.add_process'));
- $handler->handleSave($this->getPartialSaveUrl());
- }
- /**
- * assamble the partial save handler url
- * this allows dynamically updating the attribute
- */
- public function getPartialSaveUrl()
- {
- return partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.refresh');
- }
- /**
- * Add or copy dialog.
- */
- function partial_addorcopy_dialog()
- {
- $this->createDestination();
- $this->m_destInstance->addFilter($this->m_ownerInstance->m_postvars['atkfilter']);
- $handler = &$this->m_destInstance->getHandler('addorcopy');
- $handler
- ->setProcessUrl(
- partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.addorcopy_process',
- array('atkfilter' => $this->m_ownerInstance->m_postvars['atkfilter'])));
- $handler->handleDialog();
- }
- /**
- * Process add or copy action.
- */
- function partial_addorcopy_process()
- {
- $this->createDestination();
- $addOrCopy = $this->m_ownerInstance->m_postvars['addorcopy'];
- // user has choosen to copy an existing record, let the copy action
- // be handled by the normal addorcopy handler
- if ($addOrCopy == 'copy') {
- $handler = &$this->m_destInstance->getHandler('addorcopy');
- $handler->m_postvars = $this->m_ownerInstance->m_postvars;
- // Reset postvars of ownerinstance because it might interfere with relations
- // which point back to this ownerinstance and it doesn't need them anymore anyway.
- $this->m_ownerInstance->m_postvars = array();
- $handler->handleCopy(partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.refresh'));
- }
- // user has choosen to add a new record, depending on whatever the AF_ONETOMANY_ADD_DIALOG
- // or the destination instance EF_ADD_DIALOG flags has been set we either show the user an
- // add dialog or redirect him/her to the add page (using an atkSubmit)
- else {
- $script = atkDialog::getCloseCall();
- if ($this->hasFlag(AF_ONETOMANY_ADD_DIALOG) || $this->m_destInstance->hasFlag(EF_ADD_DIALOG)) {
- $ui = &$this->m_ownerInstance->getUi();
- $filter = $this->m_ownerInstance->m_postvars['atkfilter'];
- $dialog = new Adapto_Dialog($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.add_dialog',
- array('atkfilter' => $filter));
- $title = $ui->title($this->m_destInstance->m_module, $this->m_destInstance->m_type, 'add');
- $dialog->setTitle($title);
- $dialog->setSessionStatus(SESSION_PARTIAL);
- $script .= $dialog->getCall(true, false);
- } else {
- $url = dispatch_url($this->m_destInstance->atkEntityType(), 'add');
- $script .= "atkSubmit('" . atkurlencode(session_url($url, SESSION_NESTED)) . "');";
- }
- $page = &$this->m_ownerInstance->getPage();
- $page->register_loadscript($script);
- }
- }
- /**
- * Edit dialog.
- */
- function partial_edit_dialog()
- {
- $this->createDestination();
- $this->m_destInstance->m_partial = 'dialog';
- $handler = &$this->m_destInstance->getHandler('edit');
- $handler->m_postvars = $this->m_ownerInstance->m_postvars;
- // Reset postvars of ownerinstance because it might interfere with relations
- // which point back to this ownerinstance and it doesn't need them anymore anyway.
- $this->m_ownerInstance->m_postvars = array();
- $handler->setDialogSaveUrl(partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.edit_process'));
- $result = $handler->renderEditDialog();
- $page = &$this->m_ownerInstance->getPage();
- $page->addContent($result);
- }
- /**
- * Process edit dialog update action.
- */
- function partial_edit_process()
- {
- $this->createDestination();
- $handler = &$this->m_destInstance->getHandler('update');
- $handler->m_postvars = $this->m_ownerInstance->m_postvars;
- // Reset postvars of ownerinstance because it might interfere with relations
- // which point back to this ownerinstance and it doesn't need them anymore anyway.
- $this->m_ownerInstance->m_postvars = array();
- $handler->setDialogSaveUrl(partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.edit_process'));
- $handler->handleUpdate(partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.refresh'));
- }
- /**
- * Set header generation function name.
- * @param String $name The header generation function name.
- */
- function setHeader($name)
- {
- $this->m_headerName = $name;
- }
- /**
- * Set footer generation function name.
- * @param String $name The footder generation function name.
- */
- function setFooter($name)
- {
- $this->m_footerName = $name;
- }
- /**
- * Set the exclude fields for the grid
- *
- * @param array $excludes
- */
- function setGridExcludes($excludes)
- {
- $this->m_excludes = $excludes;
- }
- /**
- * Get the exclude fields for the grid
- *
- * @return array with exclude fields
- */
- function getGridExcludes()
- {
- return $this->m_excludes;
- }
- }