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");
+ }
}