Index: trunk/core/kernel/db/dbitem.php =================================================================== diff -u -r932 -r1339 --- trunk/core/kernel/db/dbitem.php (.../dbitem.php) (revision 932) +++ trunk/core/kernel/db/dbitem.php (.../dbitem.php) (revision 1339) @@ -15,6 +15,11 @@ */ var $FieldValues; + + var $FieldErrors; + + var $ErrorMsgs = Array(); + /** * Holds item' primary key value * @@ -23,34 +28,34 @@ */ var $ID; - /** - * Fields allowed to be set (from table + virtual) - * - * @var Array - * @access private - */ - var $Fields=Array(); + function kDBItem() + { + parent::kDBBase(); + $this->ErrorMsgs['required'] = 'Field is required'; + $this->ErrorMsgs['unique'] = 'Field value must be unique'; + $this->ErrorMsgs['value_out_of_range'] = 'Field is out of range, possible values from %s to %s'; + $this->ErrorMsgs['length_out_of_range'] = 'Field is out of range'; + $this->ErrorMsgs['bad_type'] = 'Incorrect data format, please use %s'; + $this->ErrorMsgs['bad_date_format'] = 'Incorrect date format, please use (%s) ex. (%s)'; + } /** - * All virtual field names + * Set's default values for all fields * - * @var Array - * @access private - */ - var $VirtualFields=Array(); - - /** - * Set's field names from table - * from config - * - * @param Array $fields * @access public */ - function setConfigFields($fields) + function SetDefaultValues() { - $this->Fields=$fields; + foreach ($this->Fields as $field => $params) { + if ( isset($params['default']) ) { + $this->SetDBField($field, $params['default']); + } + else { + $this->SetDBField($field, NULL); + } + } } - + /** * Sets current item field value * (applies formatting) @@ -62,7 +67,15 @@ */ function SetField($name,$value) { - $this->SetDBField($name,$value); + $options = $this->GetFieldOptions($name); + $parsed = $value; + if ($value == '') $parsed = NULL; + if (isset($options['formatter'])) { + $formatter =& $this->Application->recallObject($options['formatter']); +// $parsed = $formatter->Parse($value, $options, $err); + $parsed = $formatter->Parse($value, $name, $this); + } + $this->SetDBField($name,$parsed); } /** @@ -79,22 +92,8 @@ $this->FieldValues[$name] = $value; } - /** * Return current item' field value by field name - * (apply formatter) - * - * @access public - * @param string $name field name to return - * @return mixed - */ - function GetField($name) - { - return $this->GetDBField($name); - } - - /** - * Return current item' field value by field name * (doesn't apply formatter) * * @access public @@ -106,6 +105,16 @@ return $this->FieldValues[$name]; } + function HasField($name) + { + return isset($this->FieldValues[$name]); + } + + function GetFieldValues() + { + return $this->FieldValues; + } + /** * Sets item' fields corresponding to elements in passed $hash values. * @@ -114,17 +123,29 @@ * * @access public * @param Array $hash + * @param Array $set_fields Optional param, field names in target object to set, other fields will be skipped * @return void */ - function SetFieldsFromHash($hash) + function SetFieldsFromHash($hash, $set_fields=null) { foreach ($hash as $field_name => $field_value) { - if( eregi("^[0-9]+$", $field_name) || !in_array($field_name,$this->Fields) ) continue; + if( eregi("^[0-9]+$", $field_name) || !array_key_exists($field_name,$this->Fields) ) continue; + if ( is_array($set_fields) && !in_array($field_name, $set_fields) ) continue; $this->SetField($field_name,$field_value); } } + function SetDBFieldsFromHash($hash, $set_fields=null) + { + foreach ($hash as $field_name => $field_value) + { + if( eregi("^[0-9]+$", $field_name) || !array_key_exists($field_name,$this->Fields) ) continue; + if ( is_array($set_fields) && !in_array($field_name, $set_fields) ) continue; + $this->SetDBField($field_name,$field_value); + } + } + /** * Returns part of SQL WHERE clause identifing the record, ex. id = 25 * @@ -137,7 +158,7 @@ */ function GetKeyClause($method=null) { - return $this->IDField.' = '.$this->Conn->qstr($this->ID); + return '`'.$this->TableName.'`.'.$this->IDField.' = '.$this->Conn->qstr($this->ID); } /** @@ -150,36 +171,94 @@ */ function Load($id, $id_field_name=null) { + if (is_array($id)) { + $keys = $id; + foreach ($keys as $field => $value) { + $sqls[] = '`'.$this->TableName.'`.'.$field.' = '.$this->Conn->qstr($value); + } + $keys_sql = '('.implode(') AND (', $sqls).')'; + } + if (isset($id_field_name)) $this->SetIDField($id_field_name); + if (!isset($id) && !isset($keys_sql)) return false; - if (!isset($id)) return false; + if( !$this->raiseEvent('OnBeforeItemLoad',$id) ) return false; $this->ID = $id; - $q = $this->GetSelectSQL().' WHERE '.$this->GetKeyClause('load'); + $q = $this->GetSelectSQL().' WHERE '.(isset($keys_sql) ? $keys_sql : $this->GetKeyClause('load')); if ($this->DisplayQueries) { echo get_class($this)." Load SQL: $q
"; } - $this->FieldValues = $this->Conn->GetRow($q); - + $this->FieldValues = array_merge_recursive2( $this->FieldValues, $this->Conn->GetRow($q) ); + if ($this->FieldValues === false) { //Error handling could be here return false; } - $this->setID($id); + if (isset($keys_sql)) { + $this->setID($this->FieldValues[$this->IDField]); + } + else { + $this->setID($id); + } + + $this->UpdateFormattersSubFields(); // used for updating separate virtual date/time fields from DB timestamp (for example) + + $this->raiseEvent('OnAfterItemLoad'); return true; } + + /** + * Builds select sql, SELECT ... FROM parts only + * + * @access public + * @return string + */ + function GetSelectSQL() + { + $sql = $this->addCalculatedFields($this->SelectClause); + return parent::GetSelectSQL($sql); + } + function UpdateFormattersMasterFields() + { + foreach ($this->Fields as $field => $options) { + if (isset($options['formatter'])) { + $formatter =& $this->Application->recallObject($options['formatter']); + $formatter->UpdateMasterFields($field, $this->GetDBField($field), $options, $this); + } + } + } + + function SkipField($field_name, $force_id=false) + { + $skip = false; + $skip = $skip || ( isset($this->VirtualFields[$field_name]) ); //skipping 'virtual' field + $skip = $skip || ( !getArrayValue($this->FieldValues, $field_name) && getArrayValue($this->Fields[$field_name], 'skip_empty') ); //skipping 'virtual' field + $skip = $skip || ($field_name == $this->IDField && !$force_id); //skipping Primary Key + +// $table_name = preg_replace("/^(.*)\./", "$1", $field_name); +// $skip = $skip || ($table_name && ($table_name != $this->TableName)); //skipping field from other tables + + $skip = $skip || ( !isset($this->Fields[$field_name]) ); //skipping field not in Fields (nor virtual, nor real) + + return $skip; + } + /** * Updates previously loaded record with current item' values * * @access public * @param int Primery Key Id to update - * @return void + * @return bool */ - function Update($id=null) + function Update($id=null, $system_update=false) { - if( isset($id) ) $this->ID=$id; + if( isset($id) ) $this->setID($id); + + if( !$this->raiseEvent('OnBeforeItemUpdate') ) return false; + if( !isset($this->ID) ) return false; // Validate before updating @@ -191,13 +270,22 @@ $sql = sprintf('UPDATE %s SET ',$this->TableName); foreach ($this->FieldValues as $field_name => $field_value) { - if ( isset($this->VirtualFields[$field_name]) ) continue; //skipping 'virtual' field - if ($field_name == $this->IDField) continue; //skipping Primary Key + if ($this->SkipField($field_name)) continue; $real_field_name = eregi_replace("^.*\.", '',$field_name); //removing table names from field names //Adding part of SET clause for current field, escaping data with ADODB' qstr - $sql.= sprintf('%s=%s, ',$real_field_name,$this->Conn->qstr($this->FieldValues[$field_name], 0)); + if (is_null( $this->FieldValues[$field_name] )) { + if (isset($this->Fields[$field_name]['not_null']) && $this->Fields[$field_name]['not_null']) { + $sql .= '`'.$real_field_name.'` = '.$this->Conn->qstr($this->Fields[$field_name]['default']).', '; + } + else { + $sql .= '`'.$real_field_name.'` = NULL, '; + } + } + else { + $sql.= sprintf('`%s`=%s, ', $real_field_name, $this->Conn->qstr($this->FieldValues[$field_name], 0)); + } } $sql = ereg_replace(", $", '', $sql); //Removing last comma and space @@ -212,34 +300,256 @@ } return false; } + + $affected = $this->Conn->getAffectedRows(); + if (!$system_update && $affected == 1){ + $this->setModifiedFlag(); + } + $this->raiseEvent('OnAfterItemUpdate'); return true; } + /** + * Validate all item fields based on + * constraints set in each field options + * in config + * + * @return bool + * @access private + */ function Validate() { - return true; + $this->UpdateFormattersMasterFields(); //order is critical - should be called BEFORE checking errors + $global_res = true; + foreach ($this->Fields as $field => $params) { + $res = true; + $res = $res && $this->ValidateRequired($field, $params); + $res = $res && $this->ValidateType($field, $params); + $res = $res && $this->ValidateRange($field, $params); + $res = $res && $this->ValidateUnique($field, $params); + + // If Formatter has set some error messages during values parsing + if (isset($this->FieldErrors[$field]['pseudo']) && $this->FieldErrors[$field] != '') { + $global_res = false; + } + + $global_res = $global_res && $res; + } + + if (!$global_res && $this->Application->isDebugMode() ) + { + global $debugger; + $error_msg = "Validation failed in prefix ".$this->Prefix.", FieldErrors follow (look at items with 'pseudo' key set)
+ You may ignore this notice if submitted data really has a validation error "; + trigger_error( $error_msg, E_USER_NOTICE); + $debugger->dumpVars($this->FieldErrors); + } + + return $global_res; } /** + * Check if value in field matches field type specified in config + * + * @param string $field field name + * @param Array $params field options from config + * @return bool + */ + function ValidateType($field, $params) + { + $res = true; + $val = $this->FieldValues[$field]; + if ( $val != '' && + isset($params['type']) && + preg_match("#int|integer|double|float|real|numeric|string#", $params['type']) + ) { + $res = is_numeric($val); + if($params['type']=='string' || $res) + { + $f = 'is_'.$params['type']; + settype($val, $params['type']); + $res = $f($val) && ($val==$this->FieldValues[$field]); + } + if (!$res) + { + $this->FieldErrors[$field]['pseudo'] = 'bad_type'; + $this->FieldErrors[$field]['params'] = $params['type']; + } + } + return $res; + } + + /** + * Check if value is set for required field + * + * @param string $field field name + * @param Array $params field options from config + * @return bool + * @access private + */ + function ValidateRequired($field, $params) + { + $res = true; + if ( getArrayValue($params,'required') ) + { + $res = ( (string) $this->FieldValues[$field] != ''); + } + if (!$res) $this->FieldErrors[$field]['pseudo'] = 'required'; + return $res; + } + + /** + * Validates that current record has unique field combination among other table records + * + * @param string $field field name + * @param Array $params field options from config + * @return bool + * @access private + */ + function ValidateUnique($field, $params) + { + $res = true; + $unique_fields = getArrayValue($params,'unique'); + if($unique_fields !== false) + { + $where = Array(); + array_push($unique_fields,$field); + foreach($unique_fields as $unique_field) + { + $where[] = '`'.$unique_field.'` = '.$this->Conn->qstr( $this->GetDBField($unique_field) ); + } + + $sql = 'SELECT COUNT(*) FROM %s WHERE ('.implode(') AND (',$where).') AND ('.$this->IDField.' <> '.(int)$this->ID.')'; + + $res_temp = $this->Conn->GetOne( sprintf($sql, $this->TableName ) ); + $res_live = $this->Conn->GetOne( sprintf($sql, kTempTablesHandler::GetLiveName($this->TableName) ) ); + $res = ($res_temp == 0) && ($res_live == 0); + + if(!$res) $this->FieldErrors[$field]['pseudo'] = 'unique'; + } + return $res; + } + + /** + * Check if field value is in range specified in config + * + * @param string $field field name + * @param Array $params field options from config + * @return bool + * @access private + */ + function ValidateRange($field, $params) + { + $res = true; + $val = $this->FieldValues[$field]; + if ( isset($params['type']) && preg_match("#int|integer|double|float|real#", $params['type']) && strlen($val) > 0 ) { + if ( isset($params['max_value_inc'])) { + $res = $res && $val <= $params['max_value_inc']; + $max_val = $params['max_value_inc'].' (inclusive)'; + } + if ( isset($params['min_value_inc'])) { + $res = $res && $val >= $params['min_value_inc']; + $min_val = $params['min_value_inc'].' (inclusive)'; + } + if ( isset($params['max_value_exc'])) { + $res = $res && $val < $params['max_value_exc']; + $max_val = $params['max_value_exc'].' (exclusive)'; + } + if ( isset($params['min_value_exc'])) { + $res = $res && $val > $params['min_value_exc']; + $min_val = $params['min_value_exc'].' (exclusive)'; + } + } + if (!$res) { + $this->FieldErrors[$field]['pseudo'] = 'value_out_of_range'; + + if ( !isset($min_val) ) $min_val = '-∞'; + if ( !isset($max_val) ) $max_val = '∞'; + + $this->FieldErrors[$field]['params'] = Array( $min_val, $max_val ); + return $res; + } + if ( isset($params['max_len'])) { + $res = $res && strlen($val) <= $params['max_len']; + } + if ( isset($params['min_len'])) { + $res = $res && strlen($val) >= $params['min_len']; + } + if (!$res) { + $this->FieldErrors[$field]['pseudo'] = 'length_out_of_range'; + $this->FieldErrors[$field]['params'] = Array($params['min_len'], $params['max_len']); + return $res; + } + return $res; + } + + /** + * Return error message for field + * + * @param string $field + * @return string + * @access public + */ + function GetErrorMsg($field) + { + if( !isset($this->FieldErrors[$field]) ) return ''; + + $err = getArrayValue($this->FieldErrors[$field], 'pseudo'); + if( isset($this->Fields[$field]['error_msgs'][$err]) ) + { + $msg = $this->Fields[$field]['error_msgs'][$err]; + } + else + { + if( !isset($this->ErrorMsgs[$err]) ) return $err; + $msg = $this->ErrorMsgs[$err]; + } + + if ( isset($this->FieldErrors[$field]['params']) ) + { + return vsprintf($msg, $this->FieldErrors[$field]['params']); + } + return $msg; + } + + /** * 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! * @access public - * @return void + * @return bool */ - function Create() + function Create($force_id=false, $system_create=false) { + if( !$this->raiseEvent('OnBeforeItemCreate') ) return false; + if(!$this->Validate()) //Validating fields before attempting to create record return false; + if (is_int($force_id)) { + $this->FieldValues[$this->IDField] = $force_id; + } + $fields_sql = ''; $values_sql = ''; foreach ($this->FieldValues as $field_name => $field_value) { - if ( isset($this->VirtualFields[$field_name]) ) continue; //skipping 'virtual' field + if ($this->SkipField($field_name, $force_id)) continue; $fields_sql .= sprintf('%s, ',$field_name); //Adding field name to fields block of Insert statement //Adding field' value to Values block of Insert statement, escaping it with ADODB' qstr - $values_sql .= sprintf('%s, ',$this->Conn->qstr($this->FieldValues[$field_name], 0)); + if (is_null( $this->FieldValues[$field_name] )) { + if (isset($this->Fields[$field_name]['not_null']) && $this->Fields[$field_name]['not_null']) { + $values_sql .= $this->Conn->qstr($this->Fields[$field_name]['default']).', '; + } + else { + $values_sql .= 'NULL, '; + } + } + else { + $values_sql .= sprintf('%s, ',$this->Conn->qstr($this->FieldValues[$field_name], 0)); + } + } //Cutting last commas and spaces $fields_sql = ereg_replace(", $", '', $fields_sql); @@ -256,27 +566,100 @@ return false; } $this->setID( $this->Conn->getInsertID() ); - //$this->SetInsertID(); //Setting Primary Key ($this->id) for futher using the object + + if (!$system_create){ + $this->setModifiedFlag(); + } + + $this->raiseEvent('OnAfterItemCreate'); return true; } /** * Deletes the record from databse * * @access public - * @return void + * @return bool */ - function Delete() + function Delete($id=null) { + if( isset($id) ) { + $this->setID($id); + } + + if( !$this->raiseEvent('OnBeforeItemDelete') ) return false; + $q = 'DELETE FROM '.$this->TableName.' WHERE '.$this->GetKeyClause('Delete'); if ($this->DisplayQueries) { echo get_class($this).' Delete SQL: '.$q.'
'; } - return $this->Conn->ChangeQuery($q); + $ret = $this->Conn->ChangeQuery($q); + + $this->setModifiedFlag(); + + $this->raiseEvent('OnAfterItemDelete'); + + return $ret; } /** + * Sets new name for item in case if it is beeing copied + * in same table + * + * @param array $master Table data from TempHandler + * @param int $foreign_key ForeignKey value to filter name check query by + * @access private + */ + function NameCopy($master=null, $foreign_key=null) + { + $title_field = $this->Application->getUnitOption($this->Prefix, 'TitleField'); + if (!$title_field) return; + + $new_name = $this->GetDBField($title_field); + $original_checked = false; + do { + if ( preg_match("/Copy ([0-9]*)[ ]*of(.*)/", $new_name, $regs) ) { + $new_name = 'Copy '.($regs[1]+1).' of '.$regs[2]; + } + elseif ($original_checked) { + $new_name = 'Copy of '.$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); + + if (getArrayValue($master, 'ForeignKey') && isset($foreign_key)) { + $query .= ' AND '.$master['ForeignKey'].' = '.$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); + } + + function raiseEvent($name, $id=null) + { + if( !isset($id) ) $id = $this->GetID(); + $event = new kEvent( Array('name'=>$name,'prefix'=>$this->Prefix,'special'=>$this->Special) ); + $event->setEventParam('id', $id); + $this->Application->HandleEvent($event); + return $event->status == erSUCCESS ? true : false; + } + + /** * Set's new ID for item * * @param int $new_id @@ -287,6 +670,26 @@ $this->ID=$new_id; $this->SetDBField($this->IDField,$new_id); } + + /** + * Generate and set new temporary id + * + * @access private + */ + 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()); + $this->SetID($new_id); + } + + function setModifiedFlag(){ + + $this->Application->StoreVar($this->Application->GetTopmostPrefix($this->Prefix).'_modified', "1"); + } }