'model', 'model' => 'Pluf_Model', 'cols' => array(), 'idx' => array(), 'views' => array(), ); /** Storage of the data. * * The object data are stored in an associative array. Each key * corresponds to a column and stores a Pluf_DB_Field_* variable. */ protected $_data = array(); /** * Storage cached data for methods_get */ protected $_cache = array(); // We should use a global cache. /** List of the foreign keys. * * Set by the init() method from the definition of the columns. */ protected $_fk = array(); /** * Methods available, this array is dynamically populated by init * method. */ protected $_m = array('list' => array(), // get_*_list methods 'many' => array(), // many to many 'get' => array(), // foreign keys 'extra' => array(), // added by some fields ); function __construct($pk=null, $values=array()) { $this->_init(); if ((int) $pk > 0) { $this->get($pk); //Should not have a side effect } } function init() { // Define it yourself. } /** * Define the list of methods for the model from the available * model relationship. */ function _init() { $this->_getConnection(); if (isset($GLOBALS['_PX_models_init_cache'][$this->_model])) { $init_cache = $GLOBALS['_PX_models_init_cache'][$this->_model]; $this->_cache = $init_cache['cache']; $this->_m = $init_cache['m']; $this->_a = $init_cache['a']; $this->_fk = $init_cache['fk']; $this->_data = $init_cache['data']; return; } $this->init(); foreach ($this->_a['cols'] as $col => $val) { $field = new $val['type']('', $col); $col_lower = strtolower($col); $type = 'foreignkey'; if ($type === $field->type) { $this->_m['get']['get_'.$col_lower] = array($val['model'], $col); $this->_cache['fk'][$col] = $type; $this->_fk[$col] = $type; } $type = 'manytomany'; if ($type === $field->type) { $this->_m['list']['get_'.$col_lower.'_list'] = $val['model']; $this->_m['many'][$val['model']] = $type; } foreach ($field->methods as $method) { $this->_m['extra'][$method[0]] = array($col_lower, $method[1]); } if (array_key_exists('default', $val)) { $this->_data[$col] = $val['default']; } else { $this->_data[$col] = ''; } } $this->_setupAutomaticListMethods('foreignkey'); $this->_setupAutomaticListMethods('manytomany'); $GLOBALS['_PX_models_init_cache'][$this->_model] = array( 'cache' => $this->_cache, 'm' => $this->_m, 'a' => $this->_a, 'fk' => $this->_fk, 'data' => $this->_data, ); } /** * Retrieve key relationships of a given model. * * @param string $model * @param string $type Relation type: 'foreignkey' or 'manytomany'. * @return array Key relationships. */ public function getRelationKeysToModel($model, $type) { $keys = array(); foreach ($this->_a['cols'] as $col => $val) { if (isset($val['model']) && $model === $val['model']) { $field = new $val['type'](); if ($type === $field->type) { $keys[$col] = $val; } } } return $keys; } /** * Get the foreign keys relating to a given model. * * @deprecated Use {@link self::getRelationKeysToModel()} instead. * @param string Model * @return array Foreign keys */ function getForeignKeysToModel($model) { return $this->getRelationKeysToModel($model, 'foreignkey'); } /** * Get the raw data of the object. * * For the many to many relations, the value is an array of ids. * * @return array Associative array of the data. */ function getData() { foreach ($this->_a['cols'] as $col=>$val) { $field = new $val['type'](); if ($field->type == 'manytomany') { $this->_data[$col] = array(); $method = 'get_'.strtolower($col).'_list'; foreach ($this->$method() as $item) { $this->_data[$col][] = $item->id; } } } return $this->_data; } /** * Set the association of a model to another in many to many. * * @param object Object to associate to the current object */ function setAssoc($model) { if (!$this->delAssoc($model)) { return false; } $hay = array(strtolower($model->_a['model']), strtolower($this->_a['model'])); sort($hay); $table = $hay[0].'_'.$hay[1].'_assoc'; $req = 'INSERT INTO '.$this->_con->pfx.$table."\n"; $req .= '('.$this->_con->qn(strtolower($this->_a['model']).'_id').', ' .$this->_con->qn(strtolower($model->_a['model']).'_id').') VALUES '."\n"; $req .= '('.$this->_toDb($this->_data['id'], 'id').', '; $req .= $this->_toDb($model->id, 'id').')'; $this->_con->execute($req); return true; } /** * Set the association of a model to another in many to many. * * @param object Object to associate to the current object */ function delAssoc($model) { //check if ok to make the association //current model has a many to many key with $model //$model has a many to many key with current model if (!isset($this->_m['many'][$model->_a['model']]) or strlen($this->_data['id']) == 0 or strlen($model->id) == 0) { return false; } $hay = array(strtolower($model->_a['model']), strtolower($this->_a['model'])); sort($hay); $table = $hay[0].'_'.$hay[1].'_assoc'; $req = 'DELETE FROM '.$this->_con->pfx.$table.' WHERE'."\n"; $req .= $this->_con->qn(strtolower($this->_a['model']).'_id').' = '.$this->_toDb($this->_data['id'], 'id'); $req .= ' AND '.$this->_con->qn(strtolower($model->_a['model']).'_id').' = '.$this->_toDb($model->id, 'id'); $this->_con->execute($req); return true; } /** * Bulk association of models to the current one. * * @param string Model name * @param array Ids of Model name * @return bool Success */ function batchAssoc($model_name, $ids) { $currents = $this->getRelated($model_name); foreach ($currents as $cur) { $this->delAssoc($cur); } foreach ($ids as $id) { $m = new $model_name($id); if ($m->id == $id) { $this->setAssoc($m); } } return true; } /** * Get a database connection. */ function _getConnection() { static $con = null; if ($this->_con !== null) { return $this->_con; } if ($con !== null) { $this->_con = $con; return $this->_con; } $this->_con = &Pluf::db($this); $con = $this->_con; return $this->_con; } /** * Get a database connection. */ function getDbConnection() { return $this->_getConnection(); } /** * Get the table of the model. * * Avoid doing the concatenation of the prefix and the table * manually. */ function getSqlTable() { return $this->_con->pfx.$this->_a['table']; } /** * Overloading of the get method. * * @param string Property to get */ function __get($prop) { return (array_key_exists($prop, $this->_data)) ? $this->_data[$prop] : $this->__call($prop, array()); } /** * Overloading of the set method. * * @param string Property to set * @param mixed Value to set */ function __set($prop, $val) { if (null !== $val and isset($this->_cache['fk'][$prop])) { $this->_data[$prop] = $val->id; unset($this->_cache['get_'.$prop]); } else { $this->_data[$prop] = $val; } } /** * Overloading of the method call. * * @param string Method * @param array Arguments */ function __call($method, $args) { // The foreign keys of the current object. if (isset($this->_m['get'][$method])) { if (isset($this->_cache[$method])) { return $this->_cache[$method]; } else { $this->_cache[$method] = Pluf::factory($this->_m['get'][$method][0], $this->_data[$this->_m['get'][$method][1]]); if ($this->_cache[$method]->id == '') $this->_cache[$method] = null; return $this->_cache[$method]; } } // Many to many or foreign keys on the other objects. if (isset($this->_m['list'][$method])) { if (is_array($this->_m['list'][$method])) { $model = $this->_m['list'][$method][0]; } else { $model = $this->_m['list'][$method]; } $args = array_merge(array($model, $method), $args); return call_user_func_array(array($this, 'getRelated'), $args); } // Extra methods added by fields if (isset($this->_m['extra'][$method])) { $args = array_merge(array($this->_m['extra'][$method][0], $method, $this), $args); Pluf::loadFunction($this->_m['extra'][$method][1]); return call_user_func_array($this->_m['extra'][$method][1], $args); } throw new Exception(sprintf('Method "%s" not available.', $method)); } /** * Get a given item. * * @param int Id of the item. * @return mixed Item or false if not found. */ function get($id) { $req = 'SELECT * FROM '.$this->getSqlTable().' WHERE id='.$this->_toDb($id, 'id'); if (false === ($rs = $this->_con->select($req))) { throw new Exception($this->_con->getError()); } if (count($rs) == 0) { return false; } foreach ($this->_a['cols'] as $col => $val) { $field = new $val['type'](); if ($field->type != 'manytomany' && array_key_exists($col, $rs[0])) { $this->_data[$col] = $this->_fromDb($rs[0][$col], $col); } } $this->restore(); return $this; } /** * Get one item. * * The parameters are the same as the ones of the getList method, * but, the return value is either: * * - The object * - null if no match * - Exception if the match results in more than one item. * * Usage: * *
     * $m = Pluf::factory('My_Model')->getOne(array('filter' => 'id=1'));
     * 
*
     * $m = Pluf::factory('My_Model')->getOne('id=1');
     * 
* * @param array|string Filter string or array given to getList * @see self::getList */ public function getOne($p=array()) { if (!is_array($p)) { $p = array('filter' => $p); } $items = $this->getList($p); if ($items->count() == 1) { return $items[0]; } if ($items->count() == 0) { return null; } throw new Exception(__('Error: More than one matching item found.')); } /** * Get a list of items. * * The filter should be used only for simple filtering. If you want * a complex query, you should create a new view. * Both filter and order accept an array or a string in case of multiple * parameters: * Filter: * array('col1=toto', 'col2=titi') will be used in a AND query * or simply 'col1=toto' * Order: * array('col1 ASC', 'col2 DESC') or 'col1 ASC' * * This is modelled on the DB_Table pear module interface. * * @param array Associative array with the possible following * keys: * 'view': The view to use * 'filter': The where clause to use * 'order': The ordering of the result set * 'start': The number of skipped rows in the result set * 'nb': The number of items to get in the result set * 'count': Run a count query and not a select if set to true * @return ArrayObject of items or through an exception if * database failure */ function getList($p=array()) { $default = array('view' => null, 'filter' => null, 'order' => null, 'start' => null, 'select' => null, 'nb' => null, 'count' => false); $p = array_merge($default, $p); if (!is_null($p['view']) && !isset($this->_a['views'][$p['view']])) { throw new Exception(sprintf(__('The view "%s" is not defined.'), $p['view'])); } $query = array( 'select' => $this->getSelect(), 'from' => $this->_a['table'], 'join' => '', 'where' => '', 'group' => '', 'having' => '', 'order' => '', 'limit' => '', 'props' => array(), ); if (!is_null($p['view'])) { $query = array_merge($query, $this->_a['views'][$p['view']]); } if (!is_null($p['select'])) { $query['select'] = $p['select']; } if (!is_null($p['filter'])) { if (is_array($p['filter'])) { $p['filter'] = implode(' AND ', $p['filter']); } if (strlen($query['where']) > 0) { $query['where'] .= ' AND '; } $query['where'] .= ' ('.$p['filter'].') '; } if (!is_null($p['order'])) { if (is_array($p['order'])) { $p['order'] = implode(', ', $p['order']); } if (strlen($query['order']) > 0 and strlen($p['order']) > 0) { $query['order'] .= ', '; } $query['order'] .= $p['order']; } if (!is_null($p['start']) && is_null($p['nb'])) { $p['nb'] = 10000000; } if (!is_null($p['start'])) { if ($p['start'] != 0) { $p['start'] = (int) $p['start']; } $p['nb'] = (int) $p['nb']; $query['limit'] = 'LIMIT '.$p['nb'].' OFFSET '.$p['start']; } if (!is_null($p['nb']) && is_null($p['start'])) { $p['nb'] = (int) $p['nb']; $query['limit'] = 'LIMIT '.$p['nb']; } if ($p['count'] == true) { if (isset($query['select_count'])) { $query['select'] = $query['select_count']; } else { $query['select'] = 'COUNT(*) as nb_items'; } $query['order'] = ''; $query['limit'] = ''; } $req = 'SELECT '.$query['select'].' FROM ' .$this->_con->pfx.$query['from'].' '.$query['join']; if (strlen($query['where'])) { $req .= "\n".'WHERE '.$query['where']; } if (strlen($query['group'])) { $req .= "\n".'GROUP BY '.$query['group']; } if (strlen($query['having'])) { $req .= "\n".'HAVING '.$query['having']; } if (strlen($query['order'])) { $req .= "\n".'ORDER BY '.$query['order']; } if (strlen($query['limit'])) { $req .= "\n".$query['limit']; } if (false === ($rs=$this->_con->select($req))) { throw new Exception($this->_con->getError()); } if (count($rs) == 0) { return new ArrayObject(); } if ($p['count'] == true) { return $rs; } $res = new ArrayObject(); foreach ($rs as $row) { $this->_reset(); foreach ($this->_a['cols'] as $col => $val) { if (isset($row[$col])) $this->_data[$col] = $this->_fromDb($row[$col], $col); } // FIXME: The associated properties need to be converted too. foreach ($query['props'] as $prop => $key) { $this->_data[$key] = (isset($row[$prop])) ? $row[$prop] : null; } $this->restore(); $res[] = clone($this); } return $res; } /** * Get the number of items. * * @see getList() for definition of the keys * * @param array with associative keys 'view' and 'filter' * @return int The number of items */ function getCount($p=array()) { $p['count'] = true; $count = $this->getList($p); if (empty($count) or count($count) == 0) { return 0; } else { return (int) $count[0]['nb_items']; } } /** * Get a list of related items. * * See the getList() method for usage of the view and filters. * * @param string Class of the related items * @param string Method call in a many to many related * @param array Parameters, see getList() for the definition of * the keys * @return array Array of items */ function getRelated($model, $method=null, $p=array()) { $default = array('view' => null, 'filter' => null, 'order' => null, 'start' => null, 'nb' => null, 'count' => false); $p = array_merge($default, $p); if ('' == $this->_data['id']) { return new ArrayObject(); } $m = new $model(); if (isset($this->_m['list'][$method]) and is_array($this->_m['list'][$method])) { $foreignkey = $this->_m['list'][$method][1]; if (strlen($foreignkey) == 0) { throw new Exception(sprintf(__('No matching foreign key found in model: %s for model %s'), $model, $this->_a['model'])); } if (!is_null($p['filter'])) { if (is_array($p['filter'])) { $p['filter'] = implode(' AND ', $p['filter']); } $p['filter'] .= ' AND '; } else { $p['filter'] = ''; } $p['filter'] .= $this->_con->qn($foreignkey).'='.$this->_toDb($this->_data['id'], 'id'); } else { // Many to many: We generate a special view that is making // the join $hay = array(strtolower(Pluf::factory($model)->_a['model']), strtolower($this->_a['model'])); sort($hay); $table = $hay[0].'_'.$hay[1].'_assoc'; if (isset($m->_a['views'][$p['view']])) { $m->_a['views'][$p['view'].'__manytomany__'] = $m->_a['views'][$p['view']]; if (!isset($m->_a['views'][$p['view'].'__manytomany__']['join'])) { $m->_a['views'][$p['view'].'__manytomany__']['join'] = ''; } if (!isset($m->_a['views'][$p['view'].'__manytomany__']['where'])) { $m->_a['views'][$p['view'].'__manytomany__']['where'] = ''; } } else { $m->_a['views']['__manytomany__'] = array('join' => '', 'where' => ''); $p['view'] = ''; } $m->_a['views'][$p['view'].'__manytomany__']['join'] .= ' LEFT JOIN '.$this->_con->pfx.$table.' ON ' .$this->_con->qn(strtolower($m->_a['model']).'_id').' = '.$this->_con->pfx.$m->_a['table'].'.id'; $m->_a['views'][$p['view'].'__manytomany__']['where'] = $this->_con->qn(strtolower($this->_a['model']).'_id').'='.$this->_data['id']; $p['view'] = $p['view'].'__manytomany__'; } return $m->getList($p); } /** * Generate the SQL select from the columns */ function getSelect() { if (isset($this->_cache['getSelect'])) return $this->_cache['getSelect']; $select = array(); $table = $this->getSqlTable(); foreach ($this->_a['cols'] as $col=>$val) { if ($val['type'] != 'Pluf_DB_Field_Manytomany') { $select[] = $table.'.'.$this->_con->qn($col).' AS '.$this->_con->qn($col); } } $this->_cache['getSelect'] = implode(', ', $select); return $this->_cache['getSelect']; } /** * Update the model into the database. * * If no where clause is provided, the index definition is used to * find the sequence. These are used to limit the update * to the current model. * * @param string Where clause to update specific items. ('') * @return bool Success */ function update($where='') { $this->preSave(); $req = 'UPDATE '.$this->getSqlTable().' SET'."\n"; $fields = array(); $assoc = array(); foreach ($this->_a['cols'] as $col=>$val) { $field = new $val['type'](); if ($col == 'id') { continue; } elseif ($field->type == 'manytomany') { if (is_array($this->$col)) { $assoc[$val['model']] = $this->$col; } continue; } $fields[] = $this->_con->qn($col).' = '.$this->_toDb($this->$col, $col); } $req .= implode(','."\n", $fields); if (strlen($where) > 0) { $req .= ' WHERE '.$where; } else { $req .= ' WHERE id = '.$this->_toDb($this->_data['id'], 'id'); } $this->_con->execute($req); if (false === $this->get($this->_data['id'])) { return false; } foreach ($assoc as $model=>$ids) { $this->batchAssoc($model, $ids); } $this->postSave(); return true; } /** * Create the model into the database. * * If raw insert is requested, the preSave/postSave methods are * not called and the current id of the object is directly * used. This is particularily used when doing backup/restore of * data. * * @param bool Raw insert (false) * @return bool Success */ function create($raw=false) { if (!$raw) { $this->preSave(true); } $req = 'INSERT INTO '.$this->getSqlTable()."\n"; $icols = array(); $ivals = array(); $assoc = array(); foreach ($this->_a['cols'] as $col=>$val) { $field = new $val['type'](); if ($col == 'id' and !$raw) { continue; } elseif ($field->type == 'manytomany') { // If is a defined array, we need to associate. if (is_array($this->_data[$col])) { $assoc[$val['model']] = $this->_data[$col]; } continue; } $icols[] = $this->_con->qn($col); $ivals[] = $this->_toDb($this->_data[$col], $col); } $req .= '('.implode(', ', $icols).') VALUES '; $req .= '('.implode(','."\n", $ivals).')'; $this->_con->execute($req); if (!$raw) { if (false === ($id=$this->_con->getLastID())) { throw new Exception($this->_con->getError()); } $this->_data['id'] = $id; } foreach ($assoc as $model=>$ids) { $this->batchAssoc($model, $ids); } if (!$raw) { $this->postSave(true); } return true; } /** * Get models affected by delete. * * @return array Models deleted if deleting current model. */ function getDeleteSideEffect() { $affected = array(); foreach ($this->_m['list'] as $method=>$details) { if (is_array($details)) { // foreignkey $related = $this->$method(); $affected = array_merge($affected, (array) $related); foreach ($related as $rel) { if ($details[0] == $this->_a['model'] and $rel->id == $this->_data['id']) { continue; // $rel == $this } $affected = array_merge($affected, (array) $rel->getDeleteSideEffect()); } } } return Pluf_Model_RemoveDuplicates($affected); } /** * Delete the current model from the database. * * If another model link to the current model through a foreign * key, find it and delete it. If this model is linked to other * through a many to many, delete the association. * * FIXME: No real test of circular references. It can break. */ function delete() { if (false === $this->get($this->_data['id'])) { return false; } $this->preDelete(); // Drop the row level permissions if we are using them if (Pluf::f('pluf_use_rowpermission', false)) { $_rpt = Pluf::factory('Pluf_RowPermission')->getSqlTable(); $sql = new Pluf_SQL('model_class=%s AND model_id=%s', array($this->_a['model'], $this->_data['id'])); $this->_con->execute('DELETE FROM '.$_rpt.' WHERE '.$sql->gen()); } // Find the models linking to the current one through a foreign key. foreach ($this->_m['list'] as $method=>$details) { if (is_array($details)) { // foreignkey $related = $this->$method(); foreach ($related as $rel) { if ($details[0] == $this->_a['model'] and $rel->id == $this->_data['id']) { continue; // $rel == $this } // We do not really control if it can be deleted // as we can find many times the same to delete. $rel->delete(); } } else { // manytomany $related = $this->$method(); foreach ($related as $rel) { $this->delAssoc($rel); } } } $req = 'DELETE FROM '.$this->getSqlTable().' WHERE id = '.$this->_toDb($this->_data['id'], 'id'); $this->_con->execute($req); $this->_reset(); return true; } /** * Reset the fields to default values. */ function _reset() { foreach ($this->_a['cols'] as $col => $val) { if (isset($val['default'])) { $this->_data[$col] = $val['default']; } elseif (isset($val['is_null'])) { $this->_data[$col] = null; } else { $this->_data[$col] = ''; } } } /** * Represents the model in auto generated lists. * * You need to overwrite this method to have a nice display of * your objects in the select boxes, logs. */ function __toString() { return $this->_a['model'].'('.$this->_data['id'].')'; } /** * Hook run just after loading a model from the database. * * Just overwrite it into your model to perform custom actions. */ function restore() { } /** * Hook run just before saving a model in the database. * * Just overwrite it into your model to perform custom actions. * * @param bool Create. */ function preSave($create=false) { } function postSave($create=false) { } /** * Hook run just before deleting a model from the database. * * Just overwrite it into your model to perform custom actions. */ function preDelete() { } /** * Set the values from form data. */ function setFromFormData($cleaned_values) { foreach ($cleaned_values as $key=>$val) { $this->_data[$key] = $val; } } /** * Set a view. * * @param string Name of the view. * @param array Definition of the view. */ function setView($view, $def) { $this->_a['views'][$view] = $def; } /** * Prepare the value to be put in the DB. * * @param mixed Value. * @param string Column name. * @return string SQL ready string. */ function _toDb($val, $col) { $m = $this->_con->type_cast[$this->_a['cols'][$col]['type']][1]; return $m($val, $this->_con); } /** * Get the value from the DB. * * @param mixed Value. * @param string Column name. * @return mixed Value. */ function _fromDb($val, $col) { $m = $this->_con->type_cast[$this->_a['cols'][$col]['type']][0]; return ($m == 'Pluf_DB_IdentityFromDb') ? $val : $m($val); } /** * Display value. * * When you have a list of choices for a field and you want to get * the display value of the current stored value. * * @param string Field to display the value. * @return mixed Display value, if not available default to the value. */ function displayVal($col) { if (!isset($this->_a['cols'][$col]['choices'])) { return $this->_data[$col]; // will on purposed failed if not set } $val = array_search($this->_data[$col], $this->_a['cols'][$col]['choices']); if ($val !== false) { return $val; } return $this->_data[$col]; } /** * Build the automatic methods for the relations of given type. * * Adds the get_xx_list method when the methods of the model * contains custom names. * * @param string $type Relation type: 'foreignkey' or 'manytomany'. */ protected function _setupAutomaticListMethods($type) { $current_model = $this->_a['model']; if (isset($GLOBALS['_PX_models_related'][$type][$current_model])) { $relations = $GLOBALS['_PX_models_related'][$type][$current_model]; foreach ($relations as $related) { if ($related != $current_model) { $model = new $related(); } else $model = clone $this; $fkeys = $model->getRelationKeysToModel($current_model, $type); foreach ($fkeys as $fkey => $val) { $mname = (isset($val['relate_name'])) ? $val['relate_name'] : $related; $mname = 'get_'.strtolower($mname).'_list'; if ('foreignkey' === $type) { $this->_m['list'][$mname] = array($related, $fkey); } else { $this->_m['list'][$mname] = $related; $this->_m['many'][$related] = $type; } } } } } } /** * Check if a model is already in an array of models. * * It is not possible to override the == function in PHP to directly * use in_array. * * @param Pluf_Model The model to test * @param Array The models * @return bool */ function Pluf_Model_InArray($model, $array) { if ($model->id == '') { return false; } foreach ($array as $modelin) { if ($modelin->_a['model'] == $model->_a['model'] and $modelin->id == $model->id) { return true; } } return false; } /** * Return a list of unique models. * * @param array Models with duplicates * @return array Models with duplicates. */ function Pluf_Model_RemoveDuplicates($array) { $res = array(); foreach ($array as $model) { if (!Pluf_Model_InArray($model, $res)) { $res[] = $model; } } return $res; }