Index: branches/5.2.x/core/kernel/db/dbitem.php =================================================================== --- branches/5.2.x/core/kernel/db/dbitem.php (revision 16708) +++ branches/5.2.x/core/kernel/db/dbitem.php (revision 16709) @@ -1,1634 +1,1634 @@ validator) ) { $validator_class = $this->Application->getUnitOption($this->Prefix, 'ValidatorClass', 'kValidator'); $this->validator = $this->Application->makeClass($validator_class); } $this->validator->setDataSource($this); } public function SetDirtyField($field_name, $field_value) { $this->DirtyFieldValues[$field_name] = $field_value; } public function GetDirtyField($field_name) { return $this->DirtyFieldValues[$field_name]; } public function GetOriginalField($field_name, $formatted = false, $format=null) { if (array_key_exists($field_name, $this->OriginalFieldValues)) { // item was loaded before $value = $this->OriginalFieldValues[$field_name]; } else { // no original fields -> use default field value $value = $this->Fields[$field_name]['default']; } if (!$formatted) { return $value; } $res = $value; $formatter = $this->GetFieldOption($field_name, 'formatter'); if ( $formatter ) { - /** @var kFormatter $formatter */ - $formatter = $this->Application->recallObject($formatter); + $formatter = $this->getFormatter($formatter); if ( $formatter instanceof kMultiLanguage && strpos((string)$format, 'no_default') === false ) { $format = rtrim('no_default;' . $format, ';'); } $res = $formatter->Format($value, $field_name, $this, $format); } return $res; } /** * Sets original field value (useful for custom virtual fields) * * @param string $field_name * @param string $field_value */ public function SetOriginalField($field_name, $field_value) { $this->OriginalFieldValues[$field_name] = $field_value; } /** * Set's default values for all fields * * @access public */ public function SetDefaultValues() { parent::SetDefaultValues(); if ($this->populateMultiLangFields) { $this->PopulateMultiLangFields(); } foreach ($this->Fields as $field => $field_options) { $default_value = isset($field_options['default']) ? $field_options['default'] : NULL; $this->SetDBField($field, $default_value); } } /** * Sets current item field value * (applies formatting) * * @access public * @param string $name Name of the field * @param mixed $value Value to set the field to * @return void */ public function SetField($name,$value) { $options = $this->GetFieldOptions($name); $parsed = $value; if ($value == '') { $parsed = NULL; } - // kFormatter is always used, to make sure, that numeric value is converted to normal representation - // according to regional format, even when formatter is not set (try seting format to 1.234,56 to understand why) - /** @var kFormatter $formatter */ - $formatter = $this->Application->recallObject(isset($options['formatter']) ? $options['formatter'] : 'kFormatter'); - - $parsed = $formatter->Parse($value, $name, $this); + /* + * kFormatter is always used, to make sure, that numeric value is converted to normal representation + * according to regional format, even when formatter is not set (try setting format to 1.234,56 to + * understand why). + */ + $parsed = $this + ->getFormatter(isset($options['formatter']) ? $options['formatter'] : 'kFormatter') + ->Parse($value, $name, $this); $this->SetDBField($name,$parsed); } /** * Sets current item field value * (doesn't apply formatting) * * @access public * @param string $name Name of the field * @param mixed $value Value to set the field to * @return void */ public function SetDBField($name,$value) { $this->FieldValues[$name] = $value; } /** * Set's field error, if pseudo passed not found then create it with message text supplied. * Don't overwrite existing pseudo translation. * * @param string $field * @param string $pseudo * @param string $error_label * @param Array $error_params * * @return bool * @access public */ public function SetError($field, $pseudo, $error_label = null, $error_params = null) { $this->initValidator(); return $this->validator->SetError($field, $pseudo, $error_label, $error_params); } /** * Removes error on field * * @param string $field * @access public */ public function RemoveError($field) { if ( !is_object($this->validator) ) { return ; } $this->validator->RemoveError($field); } /** * Returns error pseudo * * @param string $field * @return string */ public function GetErrorPseudo($field) { if ( !is_object($this->validator) ) { return ''; } return $this->validator->GetErrorPseudo($field); } /** * Return current item' field value by field name * (doesn't apply formatter) * * @param string $name field name to return * @return mixed * @access public */ public function GetDBField($name) { /*if (!array_key_exists($name, $this->FieldValues) && defined('DEBUG_MODE') && DEBUG_MODE) { $this->Application->Debugger->appendTrace(); }*/ return $this->FieldValues[$name]; } public function HasField($name) { return array_key_exists($name, $this->FieldValues); } public function GetFieldValues() { return $this->FieldValues; } /** * Sets item' fields corresponding to elements in passed $hash values. * The function sets current item fields to values passed in $hash, by matching $hash keys with field names * of current item. If current item' fields are unknown {@link kDBItem::PrepareFields()} is called before actually setting the fields * * @param Array $hash Fields hash. * @param Array $set_fields Optional param, field names in target object to set, other fields will be skipped * * @return void */ public function SetFieldsFromHash($hash, $set_fields = Array ()) { if ( !$set_fields ) { $set_fields = array_keys($hash); } $skip_fields = $this->getRequestProtectedFields($hash); if ( $skip_fields ) { $set_fields = array_diff($set_fields, $skip_fields); } $set_fields = array_intersect($set_fields, array_keys($this->Fields)); // used in formatter which work with multiple fields together foreach ($set_fields as $field_name) { $this->SetDirtyField($field_name, $hash[$field_name]); } // formats all fields using associated formatters foreach ($set_fields as $field_name) { $this->SetField($field_name, $hash[$field_name]); } } /** * Returns fields, that are not allowed to be changed from request. * * @param array $fields_hash Fields hash. * * @return array */ protected function getRequestProtectedFields(array $fields_hash) { // don't allow changing ID $fields = Array (); $fields[] = $this->Application->getUnitOption($this->Prefix, 'IDField'); $parent_prefix = $this->Application->getUnitOption($this->Prefix, 'ParentPrefix'); if ( $parent_prefix && $this->isLoaded() && !$this->Application->isAdmin ) { // don't allow changing foreign key of existing item from request $foreign_key = $this->Application->getUnitOption($this->Prefix, 'ForeignKey'); $fields[] = is_array($foreign_key) ? $foreign_key[$parent_prefix] : $foreign_key; } return $fields; } /** * Sets object fields from $hash array * @param Array $hash * @param Array|null $set_fields * @return void * @access public */ public function SetDBFieldsFromHash($hash, $set_fields = Array ()) { if ( !$set_fields ) { $set_fields = array_keys($hash); } $set_fields = array_intersect($set_fields, array_keys($this->Fields)); foreach ($set_fields as $field_name) { $this->SetDBField($field_name, $hash[$field_name]); } } /** * Returns part of SQL WHERE clause identifying the record, ex. id = 25 * * @param string $method Child class may want to know who called GetKeyClause, Load(), Update(), Delete() send its names as method * @param Array $keys_hash alternative, then item id, keys hash to load item by * @see kDBItem::Load() * @see kDBItem::Update() * @see kDBItem::Delete() * @return string * @access protected */ protected function GetKeyClause($method = null, $keys_hash = null) { if ( !isset($keys_hash) ) { $keys_hash = Array ($this->IDField => $this->ID); } $ret = ''; foreach ($keys_hash as $field => $value) { $value_part = is_null($value) ? ' IS NULL' : ' = ' . $this->Conn->qstr($value); $ret .= '(' . (strpos($field, '.') === false ? '`' . $this->TableName . '`.' : '') . $field . $value_part . ') AND '; } return substr($ret, 0, -5); } /** * Loads item from the database by given id * * @access public * @param mixed $id item id of keys->values hash to load item by * @param string $id_field_name Optional parameter to load item by given Id field * @param bool $cachable cache this query result based on it's prefix serial * @return bool True if item has been loaded, false otherwise */ public function Load($id, $id_field_name = null, $cachable = false) { $this->Clear(); if ( isset($id_field_name) ) { $this->IDField = $id_field_name; // set new IDField } $keys_sql = ''; if (is_array($id)) { $keys_sql = $this->GetKeyClause('load', $id); } else { $this->setID($id); $keys_sql = $this->GetKeyClause('load'); } if ( isset($id_field_name) ) { // restore original IDField from unit config $this->IDField = $this->Application->getUnitOption($this->Prefix, 'IDField'); } if (($id === false) || !$keys_sql) { return false; } if (!$this->raiseEvent('OnBeforeItemLoad', $id)) { return false; } $q = $this->GetSelectSQL() . ' WHERE ' . $keys_sql; if ($cachable && $this->Application->isCachingType(CACHING_TYPE_MEMORY)) { $serial_name = $this->Application->incrementCacheSerial($this->Prefix == 'st' ? 'c' : $this->Prefix, isset($id_field_name) ? null : $id, false); $cache_key = 'kDBItem::Load_' . crc32(serialize($id) . '-' . $this->IDField) . '[%' . $serial_name . '%]'; $field_values = $this->Application->getCache($cache_key, false); if ($field_values === false) { $field_values = $this->Conn->GetRow($q); if ($field_values !== false) { // only cache, when data was retrieved $this->Application->setCache($cache_key, $field_values); } } } else { $field_values = $this->Conn->GetRow($q); } if ($field_values) { $this->FieldValues = array_merge($this->FieldValues, $field_values); $this->OriginalFieldValues = $this->FieldValues; $this->Loaded = true; } else { return false; } if (is_array($id) || isset($id_field_name)) { $this->setID($this->FieldValues[$this->IDField]); } $this->UpdateFormattersSubFields(); // used for updating separate virtual date/time fields from DB timestamp (for example) $this->raiseEvent('OnAfterItemLoad', $this->GetID()); return true; } /** * Loads object from hash (not db) * * @param Array $fields_hash * @param string $id_field */ public function LoadFromHash($fields_hash, $id_field = null) { if (!isset($id_field)) { $id_field = $this->IDField; } $this->Clear(); if (!$fields_hash || !array_key_exists($id_field, $fields_hash)) { // no data OR id field missing return false; } $id = $fields_hash[$id_field]; if ( !$this->raiseEvent('OnBeforeItemLoad', $id) ) { return false; } $this->FieldValues = array_merge($this->FieldValues, $fields_hash); $this->OriginalFieldValues = $this->FieldValues; $this->setID($id); $this->UpdateFormattersSubFields(); // used for updating separate virtual date/time fields from DB timestamp (for example) $this->raiseEvent('OnAfterItemLoad', $id); $this->Loaded = true; return true; } /** * Builds select sql, SELECT ... FROM parts only * * @access public * @return string */ /** * Returns SELECT part of list' query * * @param string $base_query * @param bool $replace_table * @return string * @access public */ public function GetSelectSQL($base_query = null, $replace_table = true) { if (!isset($base_query)) { $base_query = $this->SelectClause; } $base_query = $this->addCalculatedFields($base_query); return parent::GetSelectSQL($base_query, $replace_table); } public function UpdateFormattersMasterFields() { $this->initValidator(); // used, when called not from kValidator::Validate method foreach ($this->Fields as $field => $options) { if ( isset($options['formatter']) ) { - /** @var kFormatter $formatter */ - $formatter = $this->Application->recallObject($options['formatter']); - - $formatter->UpdateMasterFields($field, $this->GetDBField($field), $options, $this); + $this + ->getFormatter($options['formatter']) + ->UpdateMasterFields($field, $this->GetDBField($field), $options, $this); } } } /** * Returns variable name, used to store pending file actions * * @return string * @access protected */ protected function _getPendingActionVariableName() { $window_id = $this->Application->GetTopmostWid($this->Prefix); return $this->Prefix . '_file_pending_actions' . $window_id; } /** * Returns pending actions * * @param mixed $id * @return Array * @access public */ public function getPendingActions($id = null) { if ( !isset($id) ) { $id = $this->GetID(); } $pending_actions = $this->Application->RecallVar($this->_getPendingActionVariableName()); $pending_actions = $pending_actions ? unserialize($pending_actions) : Array (); if ( is_numeric($id) ) { // filter by given/current id $ret = Array (); foreach ($pending_actions as $pending_action) { if ( $pending_action['id'] == $id ) { $ret[] = $pending_action; } } return $ret; } return $pending_actions; } /** * Sets new pending actions * * @param Array|null $new_pending_actions * @param mixed $id * @return void * @access public */ public function setPendingActions($new_pending_actions = null, $id = null) { if ( !isset($new_pending_actions) ) { $new_pending_actions = Array (); } if ( !isset($id) ) { $id = $this->GetID(); } $pending_actions = Array (); $old_pending_actions = $this->getPendingActions(true); if ( is_numeric($id) ) { // remove old actions for this id foreach ($old_pending_actions as $pending_action) { if ( $pending_action['id'] != $id ) { $pending_actions[] = $pending_action; } } // add new actions for this id $pending_actions = array_merge($pending_actions, $new_pending_actions); } else { $pending_actions = $new_pending_actions; } // save changes $var_name = $this->_getPendingActionVariableName(); if ( !$pending_actions ) { $this->Application->RemoveVar($var_name); } else { $this->Application->StoreVar($var_name, serialize($this->sortPendingActions($pending_actions))); } } /** * Sorts pending actions the way, that `delete` action will come before other actions. * * @param array $pending_actions Pending actions. * * @return array */ protected function sortPendingActions(array $pending_actions) { usort($pending_actions, array($this, 'comparePendingActions')); return $pending_actions; } protected function comparePendingActions($pending_action_a, $pending_action_b) { if ( $pending_action_a['action'] == $pending_action_b['action'] ) { return 0; } return $pending_action_a['action'] == 'delete' ? -1 : 1; } /** * Allows to skip certain fields from getting into sql queries * * @param string $field_name * @param mixed $force_id * @return bool */ public function skipField($field_name, $force_id = false) { $skip = false; // 1. skipping 'virtual' field $skip = $skip || array_key_exists($field_name, $this->VirtualFields); // 2. don't write empty field value to db, when "skip_empty" option is set $field_value = array_key_exists($field_name, $this->FieldValues) ? $this->FieldValues[$field_name] : false; if (array_key_exists($field_name, $this->Fields)) { $skip_empty = array_key_exists('skip_empty', $this->Fields[$field_name]) ? $this->Fields[$field_name]['skip_empty'] : false; } else { // field found in database, but not declared in unit config $skip_empty = false; } $skip = $skip || (!$field_value && $skip_empty); // 3. skipping field not in Fields (nor virtual, nor real) $skip = $skip || !array_key_exists($field_name, $this->Fields); return $skip; } /** * Updates previously loaded record with current item' values * * @access public * @param int $id Primary Key Id to update * @param Array $update_fields * @param bool $system_update * @return bool * @access public */ public function Update($id = null, $update_fields = null, $system_update = false) { if ( isset($id) ) { $this->setID($id); } if ( !$this->raiseEvent('OnBeforeItemUpdate') ) { return false; } if ( !isset($this->ID) ) { // ID could be set inside OnBeforeItemUpdate event, so don't combine this check with previous one return false; } // validate before updating if ( !$this->Validate() ) { return false; } if ( !$this->FieldValues ) { // nothing to update return true; } $sql = ''; if ( isset($update_fields) ) { $set_fields = $update_fields; $forgotten_fields = array_diff(array_keys($this->GetChangedFields()), $set_fields); if ( $forgotten_fields ) { trigger_error( sprintf( 'These fields weren\'t updated for #%s record in "%s" unit: %s', $this->GetID(), $this->getPrefixSpecial(), implode(', ', $forgotten_fields) ), E_USER_WARNING ); } } else { $set_fields = array_keys($this->FieldValues); } foreach ($set_fields as $field_name) { if ( $this->skipField($field_name) ) { continue; } $field_value = $this->FieldValues[$field_name]; if ( is_null($field_value) ) { if ( array_key_exists('not_null', $this->Fields[$field_name]) && $this->Fields[$field_name]['not_null'] ) { // "kFormatter::Parse" methods converts empty values to NULL and for // not-null fields they are replaced with default value here $field_value = $this->Fields[$field_name]['default']; } } $sql .= '`' . $field_name . '` = ' . $this->Conn->qstr($field_value) . ', '; } $sql = 'UPDATE ' . $this->TableName . ' SET ' . substr($sql, 0, -2) . ' WHERE ' . $this->GetKeyClause('update'); if ( $this->Conn->ChangeQuery($sql) === false ) { // there was and sql error $this->SetError($this->IDField, 'sql_error', '#' . $this->Conn->getErrorCode() . ': ' . $this->Conn->getErrorMsg()); return false; } $affected_rows = $this->Conn->getAffectedRows(); if ( !$system_update && ($affected_rows > 0) ) { $this->setModifiedFlag(ChangeLog::UPDATE, $update_fields); } $this->saveCustomFields(); $this->raiseEvent('OnAfterItemUpdate'); // Preserve OriginalFieldValues during recursive Update() method calls. $this->Loaded = true; if ( !$this->IsTempTable() ) { $this->Application->resetCounters($this->TableName); } return true; } /** * Validates given field * * @param string $field * @return bool * @access public */ public function ValidateField($field) { $this->initValidator(); return $this->validator->ValidateField($field); } /** * Validate all item fields based on * constraints set in each field options * in config * * @return bool * @access private */ public function Validate() { if ( $this->IgnoreValidation ) { return true; } $this->initValidator(); // will apply any custom validation to the item $this->raiseEvent('OnBeforeItemValidate'); if ( $this->validator->Validate() ) { // no validation errors $this->raiseEvent('OnAfterItemValidate'); return true; } return false; } /** * Check if item has errors * * @param Array $skip_fields fields to skip during error checking * @return bool */ public function HasErrors($skip_fields = Array ()) { if ( !is_object($this->validator) ) { return false; } return $this->validator->HasErrors($skip_fields); } /** * Check if value is set for required field * * @param string $field field name * @param Array $params field options from config * @return bool * @access public * @todo Find a way to get rid of direct call from kMultiLanguage::UpdateMasterFields method */ public function ValidateRequired($field, $params) { return $this->validator->ValidateRequired($field, $params); } /** * Return error message for field * * @param string $field * @param bool $force_escape * @return string * @access public */ public function GetErrorMsg($field, $force_escape = null) { if ( !is_object($this->validator) ) { return ''; } return $this->validator->GetErrorMsg($field, $force_escape); } /** * Returns field errors * * @return Array * @access public */ public function GetFieldErrors() { if ( !is_object($this->validator) ) { return Array (); } return $this->validator->GetFieldErrors(); } /** * Creates a record in the database table with current item' values * * @param mixed $force_id Set to TRUE to force creating of item's own ID or to value to force creating of passed id. Do not pass 1 for true, pass exactly TRUE! * @param bool $system_create * @return bool * @access public */ public function Create($force_id = false, $system_create = false) { if (!$this->raiseEvent('OnBeforeItemCreate')) { return false; } // Validating fields before attempting to create record if (!$this->Validate()) { return false; } if (is_int($force_id)) { $this->FieldValues[$this->IDField] = $force_id; } elseif (!$force_id || !is_bool($force_id)) { $this->FieldValues[$this->IDField] = $this->generateID(); } $fields_sql = ''; $values_sql = ''; foreach ($this->FieldValues as $field_name => $field_value) { if ($this->skipField($field_name, $force_id)) { continue; } if (is_null($field_value)) { if (array_key_exists('not_null', $this->Fields[$field_name]) && $this->Fields[$field_name]['not_null']) { // "kFormatter::Parse" methods converts empty values to NULL and for // not-null fields they are replaced with default value here $values_sql .= $this->Conn->qstr($this->Fields[$field_name]['default']); } else { $values_sql .= $this->Conn->qstr($field_value); } } else { if (($field_name == $this->IDField) && ($field_value == 0) && !is_int($force_id)) { // don't skip IDField in INSERT statement, just use DEFAULT keyword as it's value $values_sql .= 'DEFAULT'; } else { $values_sql .= $this->Conn->qstr($field_value); } } $fields_sql .= '`' . $field_name . '`, '; //Adding field name to fields block of Insert statement $values_sql .= ', '; } $sql = 'INSERT INTO ' . $this->TableName . ' (' . substr($fields_sql, 0, -2) . ') VALUES (' . substr($values_sql, 0, -2) . ')'; //Executing the query and checking the result if ($this->Conn->ChangeQuery($sql) === false) { $this->SetError($this->IDField, 'sql_error', '#' . $this->Conn->getErrorCode() . ': ' . $this->Conn->getErrorMsg()); return false; } $insert_id = $this->Conn->getInsertID(); if ($insert_id == 0) { // insert into temp table (id is not auto-increment field) $insert_id = $this->FieldValues[$this->IDField]; } $temp_id = $this->GetID(); $this->setID($insert_id); $this->OriginalFieldValues = $this->FieldValues; if (!$system_create){ $this->setModifiedFlag(ChangeLog::CREATE); } $this->saveCustomFields(); if (!$this->IsTempTable()) { $this->Application->resetCounters($this->TableName); } if ($this->IsTempTable() && ($this->Application->GetTopmostPrefix($this->Prefix) != $this->Prefix) && !is_int($force_id)) { // temp table + subitem = set negative id $this->setTempID(); } $this->raiseEvent('OnAfterItemCreate', null, array('temp_id' => $temp_id)); $this->Loaded = true; return true; } /** * Deletes the record from database * * @param int $id * @return bool * @access public */ public function Delete($id = null) { if ( isset($id) ) { $this->setID($id); } if ( !$this->raiseEvent('OnBeforeItemDelete') ) { return false; } $sql = 'DELETE FROM ' . $this->TableName . ' WHERE ' . $this->GetKeyClause('Delete'); $ret = $this->Conn->ChangeQuery($sql); $affected_rows = $this->Conn->getAffectedRows(); if ( $affected_rows > 0 ) { $this->setModifiedFlag(ChangeLog::DELETE); // will change affected rows, so get it before this line // something was actually deleted $this->raiseEvent('OnAfterItemDelete'); } if ( !$this->IsTempTable() ) { $this->Application->resetCounters($this->TableName); } return $ret; } public function PopulateMultiLangFields() { foreach ($this->Fields as $field => $options) { // master field is set only for CURRENT language $formatter = array_key_exists('formatter', $options) ? $options['formatter'] : false; if ( ($formatter == 'kMultiLanguage') && isset($options['master_field']) && isset($options['error_field']) ) { // MuliLanguage formatter sets error_field to master_field, but in PopulateMlFields mode, // we display ML fields directly so we set it back to itself, otherwise error won't be displayed unset( $this->Fields[$field]['error_field'] ); } } } /** * Sets new name for item in case if it is being copied in same table * * @param array $master Table data from TempHandler * @param int $foreign_key ForeignKey value to filter name check query by * @param string $title_field FieldName to alter, by default - TitleField of the prefix * @param string $format sprintf-style format of renaming pattern, by default Copy %1$s of %2$s which makes it Copy [Number] of Original Name * @access public */ public function NameCopy($master=null, $foreign_key=null, $title_field=null, $format='Copy %1$s of %2$s') { if (!isset($title_field)) { $title_field = $this->Application->getUnitOption($this->Prefix, 'TitleField'); if (!$title_field || isset($this->CalculatedFields[$title_field]) ) return; } $new_name = $this->GetDBField($title_field); $original_checked = false; do { if ( preg_match('/'.sprintf($format, '([0-9]*) *', '(.*)').'/', $new_name, $regs) ) { $new_name = sprintf($format, ($regs[1]+1), $regs[2]); } elseif ($original_checked) { $new_name = sprintf($format, '', $new_name); } // if we are cloning in temp table this will look for names in temp table, // since object' TableName contains correct TableName (for temp also!) // if we are cloning live - look in live $query = 'SELECT '.$title_field.' FROM '.$this->TableName.' WHERE '.$title_field.' = '.$this->Conn->qstr($new_name); $foreign_key_field = getArrayValue($master, 'ForeignKey'); $foreign_key_field = is_array($foreign_key_field) ? $foreign_key_field[ $master['ParentPrefix'] ] : $foreign_key_field; if ($foreign_key_field && isset($foreign_key)) { $query .= ' AND '.$foreign_key_field.' = '.$foreign_key; } $res = $this->Conn->GetOne($query); /*// if not found in live table, check in temp table if applicable if ($res === false && $object->Special == 'temp') { $query = 'SELECT '.$name_field.' FROM '.$this->GetTempName($master['TableName']).' WHERE '.$name_field.' = '.$this->Conn->qstr($new_name); $res = $this->Conn->GetOne($query); }*/ $original_checked = true; } while ($res !== false); $this->SetDBField($title_field, $new_name); } protected function raiseEvent($name, $id = null, $additional_params = Array()) { $additional_params['id'] = isset($id) ? $id : $this->GetID(); $event = new kEvent($this->getPrefixSpecial() . ':' . $name, $additional_params); if ( is_object($this->parentEvent) ) { $event->MasterEvent = $this->parentEvent; } $this->Application->HandleEvent($event); return $event->status == kEvent::erSUCCESS; } /** * Set's new ID for item * * @param int $new_id * @access public */ public function setID($new_id) { $this->ID = $new_id; $this->SetDBField($this->IDField, $new_id); } /** * Generate and set new temporary id * * @access private */ public function setTempID() { $new_id = (int)$this->Conn->GetOne('SELECT MIN('.$this->IDField.') FROM '.$this->TableName); if($new_id > 0) $new_id = 0; --$new_id; $this->Conn->Query('UPDATE '.$this->TableName.' SET `'.$this->IDField.'` = '.$new_id.' WHERE `'.$this->IDField.'` = '.$this->GetID()); if ($this->ShouldLogChanges(true)) { // Updating TempId in ChangesLog, if changes are disabled $ses_var_name = $this->Application->GetTopmostPrefix($this->Prefix) . '_changes_' . $this->Application->GetTopmostWid($this->Prefix); $changes = $this->Application->RecallVar($ses_var_name); $changes = $changes ? unserialize($changes) : Array (); if ($changes) { foreach ($changes as $key => $rec) { if ($rec['Prefix'] == $this->Prefix && $rec['ItemId'] == $this->GetID()) { // change log for record, that's ID was just updated -> update in change log record too $changes[$key]['ItemId'] = $new_id; } if ($rec['MasterPrefix'] == $this->Prefix && $rec['MasterId'] == $this->GetID()) { // master item id was changed $changes[$key]['MasterId'] = $new_id; } if (in_array($this->Prefix, $rec['ParentPrefix']) && $rec['ParentId'][$this->Prefix] == $this->GetID()) { // change log record of given item's sub item -> update changed id's in dependent fields $changes[$key]['ParentId'][$this->Prefix] = $new_id; if (array_key_exists('DependentFields', $rec)) { // these are fields from table of $rec['Prefix'] table! // when one of dependent fields goes into idfield of it's parent item, that was changed $parent_table_key = $this->Application->getUnitOption($rec['Prefix'], 'ParentTableKey'); $parent_table_key = is_array($parent_table_key) ? $parent_table_key[$this->Prefix] : $parent_table_key; if ($parent_table_key == $this->IDField) { $foreign_key = $this->Application->getUnitOption($rec['Prefix'], 'ForeignKey'); $foreign_key = is_array($foreign_key) ? $foreign_key[$this->Prefix] : $foreign_key; $changes[$key]['DependentFields'][$foreign_key] = $new_id; } } } } } $this->Application->StoreVar($ses_var_name, serialize($changes)); } $old_id = $this->GetID(); $this->SetID($new_id); $pending_actions = $this->getPendingActions($old_id); foreach ( array_keys($pending_actions) as $key ) { $pending_actions[$key]['id'] = $new_id; } $this->setPendingActions($pending_actions, $new_id); } /** * Set's modification flag for main prefix of current prefix to true * * @param integer $mode Mode. * @param array $update_fields Update fields. * * @return void */ public function setModifiedFlag($mode = null, array $update_fields = null) { $main_prefix = $this->Application->GetTopmostPrefix($this->Prefix); $this->Application->StoreVar($main_prefix . '_modified', '1', true); // true for optional if ( $this->ShouldLogChanges(true) ) { $this->LogChanges($main_prefix, $mode, $update_fields); if (!$this->IsTempTable()) { /** @var kDBEventHandler $handler */ $handler = $this->Application->recallObject($this->Prefix . '_EventHandler'); $ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix); $handler->SaveLoggedChanges($ses_var_name, $this->ShouldLogChanges()); } } } /** * Determines, that changes made to this item should be written to change log * * @param bool $log_changes * @return bool */ public function ShouldLogChanges($log_changes = null) { if (!isset($log_changes)) { // specific logging mode no forced -> use global logging settings $log_changes = $this->Application->getUnitOption($this->Prefix, 'LogChanges') || $this->Application->ConfigValue('UseChangeLog'); } return $log_changes && !$this->Application->getUnitOption($this->Prefix, 'ForceDontLogChanges'); } protected function LogChanges($main_prefix, $mode, $update_fields = null) { if ( !$mode ) { return ; } $ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix); $changes = $this->Application->RecallVar($ses_var_name); $changes = $changes ? unserialize($changes) : Array (); $fields_hash = Array ( 'Prefix' => $this->Prefix, 'ItemId' => $this->GetID(), 'OccuredOn' => adodb_mktime(), 'MasterPrefix' => $main_prefix, 'Action' => $mode, ); if ( $this->Prefix == $main_prefix ) { // main item $fields_hash['MasterId'] = $this->GetID(); $fields_hash['ParentPrefix'] = Array ($main_prefix); $fields_hash['ParentId'] = Array ($main_prefix => $this->GetID()); } else { // sub item // collect foreign key values (for serial reset) $foreign_keys = $this->Application->getUnitOption($this->Prefix, 'ForeignKey', Array ()); $dependent_fields = $fields_hash['ParentId'] = $fields_hash['ParentPrefix'] = Array (); /** @var Array $foreign_keys */ if ( is_array($foreign_keys) ) { foreach ($foreign_keys as $prefix => $field_name) { $dependent_fields[$field_name] = $this->GetDBField($field_name); $fields_hash['ParentPrefix'][] = $prefix; $fields_hash['ParentId'][$prefix] = $this->getParentId($prefix); } } else { $dependent_fields[$foreign_keys] = $this->GetDBField($foreign_keys); $fields_hash['ParentPrefix'] = Array ( $this->Application->getUnitOption($this->Prefix, 'ParentPrefix') ); $fields_hash['ParentId'][ $fields_hash['ParentPrefix'][0] ] = $this->getParentId('auto'); } $fields_hash['DependentFields'] = $dependent_fields; // works only, when main item is present in url, when sub-item is changed $master_id = $this->Application->GetVar($main_prefix . '_id'); if ( $master_id === false ) { // works in case of we are not editing topmost item, when sub-item is created/updated/deleted $master_id = $this->getParentId('auto', true); } $fields_hash['MasterId'] = $master_id; } switch ( $mode ) { case ChangeLog::UPDATE: $changed_fields = $this->GetChangedFields(); if ( $update_fields ) { $changed_fields = array_intersect_key( $changed_fields, array_combine($update_fields, $update_fields) ); } $to_save = array_merge($this->GetTitleField(), $changed_fields); break; case ChangeLog::CREATE: $to_save = $this->GetTitleField(); break; case ChangeLog::DELETE: $to_save = array_merge($this->GetTitleField(), $this->GetRealFields()); break; default: $to_save = Array (); break; } $fields_hash['Changes'] = serialize($to_save); $changes[] = $fields_hash; $this->Application->StoreVar($ses_var_name, serialize($changes)); } /** * Returns current item parent's ID * * @param string $parent_prefix * @param bool $top_most return topmost parent, when used * @return int * @access public */ public function getParentId($parent_prefix, $top_most = false) { $current_id = $this->GetID(); $current_prefix = $this->Prefix; if ($parent_prefix == 'auto') { $parent_prefix = $this->Application->getUnitOption($current_prefix, 'ParentPrefix'); } if (!$parent_prefix) { return $current_id; } do { // field in this table $foreign_key = $this->Application->getUnitOption($current_prefix, 'ForeignKey'); $foreign_key = is_array($foreign_key) ? $foreign_key[$parent_prefix] : $foreign_key; // get foreign key value for $current_prefix if ($current_prefix == $this->Prefix) { $foreign_key_value = $this->GetDBField($foreign_key); } else { $id_field = $this->Application->getUnitOption($current_prefix, 'IDField'); $table_name = $this->Application->getUnitOption($current_prefix, 'TableName'); if ($this->IsTempTable()) { $table_name = $this->Application->GetTempName($table_name, 'prefix:' . $current_prefix); } $sql = 'SELECT ' . $foreign_key . ' FROM ' . $table_name . ' WHERE ' . $id_field . ' = ' . $current_id; $foreign_key_value = $this->Conn->GetOne($sql); } // field in parent table $parent_table_key = $this->Application->getUnitOption($current_prefix, 'ParentTableKey'); $parent_table_key = is_array($parent_table_key) ? $parent_table_key[$parent_prefix] : $parent_table_key; $parent_id_field = $this->Application->getUnitOption($parent_prefix, 'IDField'); $parent_table_name = $this->Application->getUnitOption($parent_prefix, 'TableName'); if ($this->IsTempTable()) { $parent_table_name = $this->Application->GetTempName($parent_table_name, 'prefix:' . $current_prefix); } if ($parent_id_field == $parent_table_key) { // sub-item is related by parent item idfield $current_id = $foreign_key_value; } else { // sub-item is related by other parent item field $sql = 'SELECT ' . $parent_id_field . ' FROM ' . $parent_table_name . ' WHERE ' . $parent_table_key . ' = ' . $foreign_key_value; $current_id = $this->Conn->GetOne($sql); } $current_prefix = $parent_prefix; if (!$top_most) { break; } } while ( $parent_prefix = $this->Application->getUnitOption($current_prefix, 'ParentPrefix') ); return $current_id; } /** * Returns title field (if any) * * @return Array */ public function GetTitleField() { $title_field = $this->Application->getUnitOption($this->Prefix, 'TitleField'); if ($title_field) { $value = $this->GetField($title_field); return $value ? Array ($title_field => $value) : Array (); } return Array (); } /** * Returns only fields, that are present in database (no virtual and no calculated fields) * * @return Array */ public function GetRealFields() { return array_diff_key($this->FieldValues, $this->VirtualFields, $this->CalculatedFields); } /** * Returns only changed database field * * @param bool $include_virtual_fields * @return Array */ public function GetChangedFields($include_virtual_fields = false) { $changes = Array (); $fields = $include_virtual_fields ? $this->FieldValues : $this->GetRealFields(); $diff = array_diff_assoc($fields, $this->OriginalFieldValues); foreach ($diff as $field => $new_value) { $old_value = $this->GetOriginalField($field, true); $new_value = $this->GetField($field); if ($old_value != $new_value) { // "0.00" and "0.0000" are stored as strings and will differ. Double check to prevent that. $changes[$field] = Array ('old' => $old_value, 'new' => $new_value); } } return $changes; } /** * Returns ID of currently processed record * * @return int * @access public */ public function GetID() { return $this->ID; } /** * Generates ID for new items before inserting into database * * @return int * @access private */ protected function generateID() { return 0; } /** * Returns true if item was loaded successfully by Load method * * @return bool */ public function isLoaded() { return $this->Loaded; } /** * Checks if field is required * * @param string $field * @return bool */ public function isRequired($field) { return isset($this->Fields[$field]['required']) && $this->Fields[$field]['required']; } /** * Sets new required flag to field * * @param mixed $fields * @param bool $is_required */ public function setRequired($fields, $is_required = true) { if ( !is_array($fields) ) { $fields = explode(',', $fields); } foreach ($fields as $field) { $this->Fields[$field]['required'] = $is_required; } } /** * Removes all data from an object * * @param int $new_id * @return bool * @access public */ public function Clear($new_id = null) { $this->Loaded = false; $this->FieldValues = $this->OriginalFieldValues = Array (); $this->SetDefaultValues(); // will wear off kDBItem::setID effect, so set it later if ( is_object($this->validator) ) { $this->validator->reset(); } $this->setID($new_id); return $this->Loaded; } public function Query($force = false) { throw new Exception('Query method is called in class ' . get_class($this) . ' for prefix ' . $this->getPrefixSpecial() . ''); } protected function saveCustomFields() { if ( !$this->customFields || $this->inCloning ) { return true; } $cdata_key = rtrim($this->Prefix . '-cdata.' . $this->Special, '.'); /** @var kDBItem $cdata */ $cdata = $this->Application->recallObject($cdata_key, null, Array ('skip_autoload' => true)); $resource_id = $this->GetDBField('ResourceId'); $cdata->Load($resource_id, 'ResourceId'); $cdata->SetDBField('ResourceId', $resource_id); /** @var kMultiLanguage $ml_formatter */ - $ml_formatter = $this->Application->recallObject('kMultiLanguage'); + $ml_formatter = $this->getFormatter('kMultiLanguage'); /** @var kMultiLanguageHelper $ml_helper */ $ml_helper = $this->Application->recallObject('kMultiLanguageHelper'); $languages = $ml_helper->getLanguages(); foreach ($this->customFields as $custom_id => $custom_name) { $force_primary = $cdata->GetFieldOption('cust_' . $custom_id, 'force_primary'); if ( $force_primary ) { $cdata->SetDBField($ml_formatter->LangFieldName('cust_' . $custom_id, true), $this->GetDBField('cust_' . $custom_name)); } else { foreach ($languages as $language_id) { $cdata->SetDBField('l' . $language_id . '_cust_' . $custom_id, $this->GetDBField('l' . $language_id . '_cust_' . $custom_name)); } } } return $cdata->isLoaded() ? $cdata->Update() : $cdata->Create(); } /** * Returns specified field value from all selected rows. * Don't affect current record index * * @param string $field * @param bool $formatted * @param string $format * @return Array */ public function GetCol($field, $formatted = false, $format = null) { if ($formatted) { return Array (0 => $this->GetField($field, $format)); } return Array (0 => $this->GetDBField($field)); } /** * Set's loaded status of object * * @param bool $is_loaded * @access public * @todo remove this method, since item can't be marked as loaded externally */ public function setLoaded($is_loaded = true) { $this->Loaded = $is_loaded; } /** * Returns item's first status field * * @return string * @access public */ public function getStatusField() { $status_fields = $this->Application->getUnitOption($this->Prefix, 'StatusField'); return array_shift($status_fields); } } Index: branches/5.2.x/core/kernel/kbase.php =================================================================== --- branches/5.2.x/core/kernel/kbase.php (revision 16708) +++ branches/5.2.x/core/kernel/kbase.php (revision 16709) @@ -1,1190 +1,1208 @@ Application =& kApplication::Instance(); $this->Conn =& $this->Application->GetADODBConnection(); } /** * Set's prefix and special * * @param string $prefix * @param string $special * @access public */ public function Init($prefix, $special) { $prefix = explode('_', $prefix, 2); $this->Prefix = $prefix[0]; $this->Special = $special; $this->prefixSpecial = rtrim($this->Prefix . '.' . $this->Special, '.'); } /** * Returns prefix and special (when present) joined by a "." * * @return string * @access public */ public function getPrefixSpecial() { return $this->prefixSpecial; } /** * Creates string representation of a class (for logging) * * @return string * @access public */ public function __toString() { $ret = 'ClassName: ' . get_class($this); try { $ret .= '; PrefixSpecial: ' . $this->getPrefixSpecial(); } catch (Exception $e) {} return $ret; } } class kHelper extends kBase { /** * Performs helper initialization * * @access public */ public function InitHelper() { } /** * Append prefix and special to tag * params (get them from tagname) like * they were really passed as params * * @param string $prefix_special * @param Array $tag_params * @return Array * @access protected */ protected function prepareTagParams($prefix_special, $tag_params = Array()) { $parts = explode('.', $prefix_special); $ret = $tag_params; $ret['Prefix'] = $parts[0]; $ret['Special'] = count($parts) > 1 ? $parts[1] : ''; $ret['PrefixSpecial'] = $prefix_special; return $ret; } } abstract class kDBBase extends kBase { /** * Name of primary key field for the unit * * @var string * @access public * @see kDBBase::TableName */ public $IDField = ''; /** * Unit's database table name * * @var string * @access public */ public $TableName = ''; /** * Form name, used for validation * * @var string */ protected $formName = ''; /** * Final form configuration * * @var Array */ protected $formConfig = Array (); /** * SELECT, FROM, JOIN parts of SELECT query (no filters, no limit, no ordering) * * @var string * @access protected */ protected $SelectClause = ''; /** * Unit fields definitions (fields from database plus virtual fields) * * @var Array * @access protected */ protected $Fields = Array (); /** * Fields, that have current time as their default value. * * @var array */ protected $currentTimeFields = array(); /** * Mapping between unit custom field IDs and their names * * @var Array * @access protected */ protected $customFields = Array (); /** * Unit virtual field definitions * * @var Array * @access protected * @see kDBBase::getVirtualFields() * @see kDBBase::setVirtualFields() */ protected $VirtualFields = Array (); /** * Fields that need to be queried using custom expression, e.g. IF(...) AS value * * @var Array * @access protected */ protected $CalculatedFields = Array (); /** * Fields that contain aggregated functions, e.g. COUNT, SUM, etc. * * @var Array * @access protected */ protected $AggregatedCalculatedFields = Array (); /** * Tells, that multilingual fields sould not be populated by default. * Can be overriden from kDBBase::Configure method * * @var bool * @access protected */ protected $populateMultiLangFields = false; /** * Event, that was used to create this object * * @var kEvent * @access protected */ protected $parentEvent = null; /** + * Formatters cache. + * + * @var kFormatter[] + */ + static private $_formattersCache = array(); + + /** * Sets new parent event to the object * * @param kEvent $event * @return void * @access public */ public function setParentEvent($event) { $this->parentEvent = $event; } /** * Set object' TableName to LIVE table, defined in unit config * * @access public */ public function SwitchToLive() { $this->TableName = $this->Application->getUnitOption($this->Prefix, 'TableName'); } /** * Set object' TableName to TEMP table created based on table, defined in unit config * * @access public */ public function SwitchToTemp() { $table_name = $this->Application->getUnitOption($this->Prefix, 'TableName'); $this->TableName = $this->Application->GetTempName($table_name, 'prefix:' . $this->Prefix); } /** * Checks if object uses temp table * * @return bool * @access public */ public function IsTempTable() { return $this->Application->IsTempTable($this->TableName); } /** * Sets SELECT part of list' query * * @param string $sql SELECT and FROM [JOIN] part of the query up to WHERE * @access public */ public function SetSelectSQL($sql) { $this->SelectClause = $sql; } /** * Returns object select clause without any transformations * * @return string * @access public */ public function GetPlainSelectSQL() { return $this->SelectClause; } /** * Returns SELECT part of list' query. * 1. Occurrences of "%1$s" and "%s" are replaced to kDBBase::TableName * 2. Occurrences of "%3$s" are replaced to temp table prefix (only for table, using TABLE_PREFIX) * * @param string $base_query given base query will override unit default select query * @param bool $replace_table replace all possible occurrences * @return string * @access public * @see kDBBase::replaceModePrefix */ public function GetSelectSQL($base_query = null, $replace_table = true) { if (!isset($base_query)) { $base_query = $this->SelectClause; } if (!$replace_table) { return $base_query; } $query = str_replace(Array('%1$s', '%s'), $this->TableName, $base_query); return $this->replaceModePrefix($query); } /** * Allows sub-stables to be in same mode as main item (e.g. LEFT JOINED ones) * * @param string $query * @return string * @access protected */ protected function replaceModePrefix($query) { $live_table = substr($this->Application->GetLiveName($this->TableName), strlen(TABLE_PREFIX)); if (preg_match('/'.preg_quote(TABLE_PREFIX, '/').'(.*)'.preg_quote($live_table, '/').'/', $this->TableName, $rets)) { // will only happen, when table has a prefix (like in K4) return str_replace('%3$s', $rets[1], $query); } // will happen, when K3 table without prefix is used return $query; } /** * Sets calculated fields * * @param Array $fields * @access public */ public function setCalculatedFields($fields) { $this->CalculatedFields = $fields; } /** * Adds calculated field declaration to object. * * @param string $name * @param string $sql_clause * @access public */ public function addCalculatedField($name, $sql_clause) { $this->CalculatedFields[$name] = $sql_clause; } /** * Returns required mixing of aggregated & non-aggregated calculated fields * * @param int $aggregated 0 - having + aggregated, 1 - having only, 2 - aggregated only * @return Array * @access public */ public function getCalculatedFields($aggregated = 1) { switch ($aggregated) { case 0: $fields = array_merge($this->CalculatedFields, $this->AggregatedCalculatedFields); break; case 1: $fields = $this->CalculatedFields; break; case 2: $fields = $this->AggregatedCalculatedFields; // TODO: never used break; default: $fields = Array(); break; } return $fields; } /** * Checks, that given field is a calculated field * * @param string $field * @return bool * @access public */ public function isCalculatedField($field) { return array_key_exists($field, $this->CalculatedFields); } /** * Insert calculated fields sql into query in place of %2$s, * return processed query. * * @param string $query * @param int $aggregated 0 - having + aggregated, 1 - having only, 2 - aggregated only * @return string * @access protected */ protected function addCalculatedFields($query, $aggregated = 1) { $fields = $this->getCalculatedFields($aggregated); if ($fields) { $sql = Array (); $fields = str_replace('%2$s', $this->Application->GetVar('m_lang'), $fields); foreach ($fields as $field_name => $field_expression) { $sql[] = '('.$field_expression.') AS `'.$field_name.'`'; } $sql = implode(',',$sql); return $this->Application->ReplaceLanguageTags( str_replace('%2$s', ','.$sql, $query) ); } return str_replace('%2$s', '', $query); } /** * Performs initial object configuration, which includes setting the following: * - primary key and table name * - field definitions (including field modifiers, formatters, default values) * * @param bool $populate_ml_fields create all ml fields from db in config or not * @param string $form_name form name for validation * @access public */ public function Configure($populate_ml_fields = null, $form_name = null) { if ( isset($populate_ml_fields) ) { $this->populateMultiLangFields = $populate_ml_fields; } $this->IDField = $this->Application->getUnitOption($this->Prefix, 'IDField'); $this->TableName = $this->Application->getUnitOption($this->Prefix, 'TableName'); $this->initForm($form_name); $this->defineFields(); $this->ApplyFieldModifiers(null, true); // should be called only after all fields definitions been set $this->prepareConfigOptions(); // this should go last, but before setDefaultValues, order is significant! // only set on first call of method if ( isset($populate_ml_fields) ) { $this->SetDefaultValues(); } } /** * Adjusts object according to given form name * * @param string $form_name * @return void * @access protected */ protected function initForm($form_name = null) { $forms = $this->Application->getUnitOption($this->Prefix, 'Forms', Array ()); $this->formName = $form_name; $this->formConfig = isset($forms['default']) ? $forms['default'] : Array (); if ( !$this->formName ) { return ; } if ( !array_key_exists($this->formName, $forms) ) { trigger_error('Form "' . $this->formName . '" isn\'t declared in "' . $this->Prefix . '" unit config.', E_USER_NOTICE); } else { $this->formConfig = kUtil::array_merge_recursive($this->formConfig, $forms[$this->formName]); } } /** * Add field definitions from all possible sources * Used field sources: database fields, custom fields, virtual fields, calculated fields, aggregated calculated fields * * @access protected */ protected function defineFields() { $this->Fields = $this->getFormOption('Fields', Array ()); $this->customFields = $this->getFormOption('CustomFields', Array()); $this->setVirtualFields( $this->getFormOption('VirtualFields', Array ()) ); $calculated_fields = $this->getFormOption('CalculatedFields', Array()); $this->CalculatedFields = $this->getFieldsBySpecial($calculated_fields); $aggregated_calculated_fields = $this->getFormOption('AggregatedCalculatedFields', Array()); $this->AggregatedCalculatedFields = $this->getFieldsBySpecial($aggregated_calculated_fields); } /** * Returns form name, used for validation * * @return string */ public function getFormName() { return $this->formName; } /** * Reads unit (specified by $prefix) option specified by $option and applies form change to it * * @param string $option * @param mixed $default * @return string * @access public */ public function getFormOption($option, $default = false) { $ret = $this->Application->getUnitOption($this->Prefix, $option, $default); if ( isset($this->formConfig[$option]) ) { $ret = kUtil::array_merge_recursive($ret, $this->formConfig[$option]); } return $ret; } /** * Only exteracts fields, that match current object Special * * @param Array $fields * @return Array * @access protected */ protected function getFieldsBySpecial($fields) { if ( array_key_exists($this->Special, $fields) ) { return $fields[$this->Special]; } return array_key_exists('', $fields) ? $fields[''] : Array(); } /** * Sets aggeregated calculated fields * * @param Array $fields * @access public */ public function setAggregatedCalculatedFields($fields) { $this->AggregatedCalculatedFields = $fields; } /** * Set's field names from table from config * * @param Array $fields * @access public */ public function setCustomFields($fields) { $this->customFields = $fields; } /** * Returns custom fields information from table from config * * @return Array * @access public */ public function getCustomFields() { return $this->customFields; } /** * Set's fields information from table from config * * @param Array $fields * @access public */ public function setFields($fields) { $this->Fields = $fields; } /** * Returns fields information from table from config * * @return Array * @access public */ public function getFields() { return $this->Fields; } /** * Checks, that given field exists * * @param string $field * @return bool * @access public */ public function isField($field) { return array_key_exists($field, $this->Fields); } /** * Override field options with ones defined in submit via "field_modfiers" array (common for all prefixes) * * @param Array $field_modifiers * @param bool $from_submit * @return void * @access public * @author Alex */ public function ApplyFieldModifiers($field_modifiers = null, $from_submit = false) { $allowed_modifiers = Array ('required', 'multiple'); if ( $this->Application->isAdminUser ) { // can change upload dir on the fly (admin only!) $allowed_modifiers[] = 'upload_dir'; } if ( !isset($field_modifiers) ) { $field_modifiers = $this->Application->GetVar('field_modifiers'); if ( !$field_modifiers ) { // no field modifiers return; } $field_modifiers = getArrayValue($field_modifiers, $this->getPrefixSpecial()); } if ( !$field_modifiers ) { // no field modifiers for current prefix_special return; } $fields = $this->Application->getUnitOption($this->Prefix, 'Fields', Array ()); $virtual_fields = $this->Application->getUnitOption($this->Prefix, 'VirtualFields', Array ()); foreach ($field_modifiers as $field => $field_options) { foreach ($field_options as $option_name => $option_value) { if ( !in_array(strtolower($option_name), $allowed_modifiers) ) { continue; } if ( $from_submit ) { // there are no "lN_FieldName" fields, since ApplyFieldModifiers is // called before PrepareOptions method, which creates them $field = preg_replace('/^l[\d]+_(.*)/', '\\1', $field); } if ( $this->isVirtualField($field) ) { $virtual_fields[$field][$option_name] = $option_value; $this->SetFieldOption($field, $option_name, $option_value, true); } $fields[$field][$option_name] = $option_value; $this->SetFieldOption($field, $option_name, $option_value); } } $this->Application->setUnitOption($this->Prefix, 'Fields', $fields); $this->Application->setUnitOption($this->Prefix, 'VirtualFields', $virtual_fields); } /** * Set fields (+options) for fields that physically doesn't exist in database * * @param Array $fields * @access public */ public function setVirtualFields($fields) { if ($fields) { $this->VirtualFields = $fields; $this->Fields = array_merge($this->VirtualFields, $this->Fields); } } /** * Returns virtual fields * * @return Array * @access public */ public function getVirtualFields() { return $this->VirtualFields; } /** * Checks, that given field is a virtual field * * @param string $field * @return bool * @access public */ public function isVirtualField($field) { return array_key_exists($field, $this->VirtualFields); } /** * Performs additional initialization for field default values * * @access protected */ protected function SetDefaultValues() { foreach ( $this->Fields as $field => $options ) { if ( array_key_exists('default', $options) ) { if ( $options['default'] === '#NOW#' ) { $this->currentTimeFields[] = $field; } if ( in_array($field, $this->currentTimeFields) ) { $this->Fields[$field]['default'] = adodb_mktime(); } } } } /** * Overwrites field definition in unit config * * @param string $field * @param Array $options * @param bool $is_virtual * @access public */ public function SetFieldOptions($field, $options, $is_virtual = false) { if ($is_virtual) { $this->VirtualFields[$field] = $options; $this->Fields = array_merge($this->VirtualFields, $this->Fields); } else { $this->Fields[$field] = $options; } } /** * Changes/sets given option's value in given field definiton * * @param string $field * @param string $option_name * @param mixed $option_value * @param bool $is_virtual * @access public */ public function SetFieldOption($field, $option_name, $option_value, $is_virtual = false) { if ($is_virtual) { $this->VirtualFields[$field][$option_name] = $option_value; } $this->Fields[$field][$option_name] = $option_value; } /** * Returns field definition from unit config. * Also executes sql from "options_sql" field option to form "options" field option * * @param string $field * @param bool $is_virtual * @return Array * @access public */ public function GetFieldOptions($field, $is_virtual = false) { $property_name = $is_virtual ? 'VirtualFields' : 'Fields'; if ( !array_key_exists($field, $this->$property_name) ) { return Array (); } if (!$is_virtual) { if (!array_key_exists('options_prepared', $this->Fields[$field]) || !$this->Fields[$field]['options_prepared']) { // executes "options_sql" from field definition, only when field options are accessed (late binding) $this->PrepareFieldOptions($field); $this->Fields[$field]['options_prepared'] = true; } } return $this->{$property_name}[$field]; } /** * Returns field option * * @param string $field * @param string $option_name * @param bool $is_virtual * @param mixed $default * @return mixed * @access public */ public function GetFieldOption($field, $option_name, $is_virtual = false, $default = false) { $field_options = $this->GetFieldOptions($field, $is_virtual); if ( !$field_options && strpos($field, '#') === false ) { // we use "#FIELD_NAME#" as field for InputName tag in JavaScript, so ignore it $form_name = $this->getFormName(); trigger_error('Field "' . $field . '" is not defined' . ($form_name ? ' on "' . $this->getFormName() . '" form' : '') . ' in "' . $this->Prefix . '" unit config', E_USER_WARNING); return false; } return array_key_exists($option_name, $field_options) ? $field_options[$option_name] : $default; } /** * Returns formatted field value * * @param string $name * @param string $format * * @return string * @access protected */ public function GetField($name, $format = null) { $formatter_class = $this->GetFieldOption($name, 'formatter'); if ( $formatter_class ) { $value = ($formatter_class == 'kMultiLanguage') && !preg_match('/^l[0-9]+_/', $name) ? '' : $this->GetDBField($name); - /** @var kFormatter $formatter */ - $formatter = $this->Application->recallObject($formatter_class); - - return $formatter->Format($value, $name, $this, $format); + return $this->getFormatter($formatter_class)->Format($value, $name, $this, $format); } return $this->GetDBField($name); } /** * Returns unformatted field value * * @param string $field * @return string * @access public */ abstract public function GetDBField($field); /** * Checks of object has given field * * @param string $name * @return bool * @access public */ abstract public function HasField($name); /** * Returns field values * * @return Array * @access public */ abstract public function GetFieldValues(); /** * Populates values of sub-fields, based on formatters, set to mater fields * * @param Array $fields * @access public * @todo Maybe should not be publicly accessible */ public function UpdateFormattersSubFields($fields = null) { if ( !is_array($fields) ) { $fields = array_keys($this->Fields); } foreach ($fields as $field) { if ( isset($this->Fields[$field]['formatter']) ) { - /** @var kFormatter $formatter */ - $formatter = $this->Application->recallObject($this->Fields[$field]['formatter']); - - $formatter->UpdateSubFields($field, $this->GetDBField($field), $this->Fields[$field], $this); + $this + ->getFormatter($this->Fields[$field]['formatter']) + ->UpdateSubFields($field, $this->GetDBField($field), $this->Fields[$field], $this); } } } /** * Use formatters, specified in field declarations to perform additional field initialization in unit config * * @access protected */ protected function prepareConfigOptions() { $field_names = array_keys($this->Fields); foreach ($field_names as $field_name) { if ( !array_key_exists('formatter', $this->Fields[$field_name]) ) { continue; } - /** @var kFormatter $formatter */ - $formatter = $this->Application->recallObject( $this->Fields[$field_name]['formatter'] ); + $this + ->getFormatter($this->Fields[$field_name]['formatter']) + ->PrepareOptions($field_name, $this->Fields[$field_name], $this); + } + } - $formatter->PrepareOptions($field_name, $this->Fields[$field_name], $this); + /** + * Returns formatter. + * + * @param string $name Name. + * + * @return kFormatter + */ + protected function getFormatter($name) + { + if ( !isset(self::$_formattersCache[$name]) ) { + self::$_formattersCache[$name] = $this->Application->recallObject($name); } + + return self::$_formattersCache[$name]; } /** * Escapes fields only, not expressions * * @param string $field_expr * @return string * @access protected */ protected function escapeField($field_expr) { return preg_match('/[.(]/', $field_expr) ? $field_expr : '`' . $field_expr . '`'; } /** * Replaces current language id in given field options * * @param string $field_name * @param Array $field_option_names * @access protected */ protected function _replaceLanguageId($field_name, $field_option_names) { // don't use GetVar('m_lang') since it's always equals to default language on editing form in admin $current_language_id = $this->Application->Phrases->LanguageId; $primary_language_id = $this->Application->GetDefaultLanguageId(); $field_options =& $this->Fields[$field_name]; foreach ($field_option_names as $option_name) { $field_options[$option_name] = str_replace('%2$s', $current_language_id, $field_options[$option_name]); $field_options[$option_name] = str_replace('%3$s', $primary_language_id, $field_options[$option_name]); } } /** * Transforms "options_sql" field option into valid "options" array for given field * * @param string $field_name * @access protected */ protected function PrepareFieldOptions($field_name) { $field_options =& $this->Fields[$field_name]; if (array_key_exists('options_sql', $field_options) ) { // get options based on given sql $replace_options = Array ('option_title_field', 'option_key_field', 'options_sql'); $this->_replaceLanguageId($field_name, $replace_options); $select_clause = $this->escapeField($field_options['option_title_field']) . ',' . $this->escapeField($field_options['option_key_field']); $sql = sprintf($field_options['options_sql'], $select_clause); if (array_key_exists('serial_name', $field_options)) { // try to cache option sql on serial basis $cache_key = 'sql_' . crc32($sql) . '[%' . $field_options['serial_name'] . '%]'; $dynamic_options = $this->Application->getCache($cache_key); if ($dynamic_options === false) { $this->Conn->nextQueryCachable = true; $dynamic_options = $this->Conn->GetCol($sql, preg_replace('/^.*?\./', '', $field_options['option_key_field'])); $this->Application->setCache($cache_key, $dynamic_options); } } else { // don't cache options sql $dynamic_options = $this->Conn->GetCol($sql, preg_replace('/^.*?\./', '', $field_options['option_key_field'])); } $options_hash = array_key_exists('options', $field_options) ? $field_options['options'] : Array (); $field_options['options'] = kUtil::array_merge_recursive($options_hash, $dynamic_options); // because of numeric keys } } /** * Returns ID of currently processed record * * @return int * @access public */ public function GetID() { return $this->GetDBField($this->IDField); } /** * Allows kDBTagProcessor.SectionTitle to detect if it's editing or new item creation * * @return bool * @access public */ public function IsNewItem() { return $this->GetID() ? false : true; } /** * Returns parent table information * * @param string $special special of main item * @param bool $guess_special if object retrieved with specified special is not loaded, then try not to use special * @return Array * @access public */ public function getLinkedInfo($special = '', $guess_special = false) { $parent_prefix = $this->Application->getUnitOption($this->Prefix, 'ParentPrefix'); if ($parent_prefix) { // if this is linked table, then set id from main table $table_info = Array ( 'TableName' => $this->Application->getUnitOption($this->Prefix,'TableName'), 'IdField' => $this->Application->getUnitOption($this->Prefix,'IDField'), 'ForeignKey' => $this->Application->getUnitOption($this->Prefix,'ForeignKey'), 'ParentTableKey' => $this->Application->getUnitOption($this->Prefix,'ParentTableKey'), 'ParentPrefix' => $parent_prefix ); if (is_array($table_info['ForeignKey'])) { $table_info['ForeignKey'] = getArrayValue($table_info, 'ForeignKey', $parent_prefix); } if (is_array($table_info['ParentTableKey'])) { $table_info['ParentTableKey'] = getArrayValue($table_info, 'ParentTableKey', $parent_prefix); } /** @var kDBItem $main_object */ $main_object = $this->Application->recallObject($parent_prefix.'.'.$special, null, Array ('raise_warnings' => 0)); if (!$main_object->isLoaded() && $guess_special) { $main_object = $this->Application->recallObject($parent_prefix); } return array_merge($table_info, Array('ParentId'=> $main_object->GetDBField( $table_info['ParentTableKey'] ) ) ); } return false; } /** * Returns true, when list/item was queried/loaded * * @return bool * @access public */ abstract public function isLoaded(); /** * Returns specified field value from all selected rows. * Don't affect current record index * * @param string $field * @return Array * @access public */ abstract public function GetCol($field); } /** * Base class for exceptions, that trigger redirect action once thrown */ class kRedirectException extends Exception { /** * Redirect template * * @var string * @access protected */ protected $template = ''; /** * Redirect params * * @var Array * @access protected */ protected $params = Array (); /** * Creates redirect exception * * @param string $message * @param int $code * @param Exception $previous */ public function __construct($message = '', $code = 0, $previous = NULL) { parent::__construct($message, $code, $previous); } /** * Initializes exception * * @param string $template * @param Array $params * @return void * @access public */ public function setup($template, $params = Array ()) { $this->template = $template; $this->params = $params; } /** * Display exception details in debugger (only useful, when DBG_REDIRECT is enabled) and performs redirect * * @return void * @access public */ public function run() { $application =& kApplication::Instance(); if ( $application->isDebugMode() ) { $application->Debugger->appendException($this); } $application->Redirect($this->template, $this->params); } } /** * Exception, that is thrown when user don't have permission to perform requested action */ class kNoPermissionException extends kRedirectException { }