/ Published in: PHP
It's fun to read what I wrote ages ago. An ORM that was heavily influenced (or should I say misguided) by CakePHP, lol.
Expand |
Embed | Plain Text
Copy this code and paste it in your HTML
/*===== Subversion ==================================================================== Rev : $ $Date: 2007-05-14 22:13:55 +1000 (Mon, 14 May 2007) $ $ Date : $ $Rev: 19 $ $ =====================================================================================*/ /** * Object Relational Mapper * Maps database tables with associations to programming objects * Please read the requirements below carefully! * * Database table requirements: * - there should be a table called 'orm_schemas' for storing table schema data * - MySQL query for the 'orm_schemas' table: * CREATE TABLE orm_schemas ( * id mediumint(3) unsigned NOT NULL auto_increment, * model varchar(20) NOT NULL default '', * recursion tinyint(1) unsigned default NULL, * conjTables text, * schemaData text, * PRIMARY KEY (id) * ) ENGINE=MyISAM; * - delete the model records if the corresponding table structure have been altered * * Database table convention requirements: * - table name should be one plural word * - conjunction table name should be one table name followed by an underscore (_) followed by another table name * - model name is the singularised form of the table name * * Database fields convention requirements: * - primary key should be the model name followed by an underscore (_) followed by 'id', e.g. PK of 'posts' should be 'post_id' * - other fields should be the model name followed by an underscore (_) followed by descriptive words, e.g. 'post_date' and 'post_updated' * - foreign keys should be the foreign model name followed by an underscore (_) followed by 'id', e.g. FK of 'posts' could be 'user_id' and 'category_id' * - self reference keys should be one single word (noun) followed by an underscore (_) followed by 'id', e.g. SRK of 'posts' could be 'parent_id' or 'thread_id' * * Shortcut find methods: * $this->find() has two convenient shortcut methods: $this->findField() and $this->searchField() * where 'Field' is the actual name of the table field, with OR without the preceding model name and underscore * e.g. if you want to find 'post_title' from 'posts': $this->findTitle('post title to look for', 'posts') * same for $this->searchTitle(), the difference between these two methods is the former looks for exact matches, the latter look for partial matches * you can think of the former as 'something = value' and the latter as 'something LIKE value' in SQL queries * Use $this->find() with $this->where() if you want to search by foreign keys * * Shortened field names (optional): * When performing CRUD actions (e.g. $this->findField() and $this->modelName->field = 'field value') * field names can be shortened if and only if they're NOT foreign keys * e.g. 'post_id' and 'post_title' in 'posts' table can be shortened as 'id' and 'title' * however, since this is optional, you can always refer them by their full names * foreign key fields can NOT be shortened !! * * Active record proxies: * You may use CodeIginiter's built in active record methods before calling $this->find() or the shortcut methods * e.g. * $this->where('post_title', 'the post I want'); * $this->find('posts'); * equals * $this->findTitle('the post I want', 'posts'); * * @package CodeIgniter * @subpackage library * @author Fred Wu * @version 0.1.8 * @license GNU Lesser General Public License (LGPL) http://www.gnu.org/licenses/lgpl.html **/ class ORM { /** * CI instance * * @access protected * @var object **/ var $_CI; /** * Current table name * * @access public * @var string **/ var $table; /** * Conjunction table names * we need it because one conjunction table has two potential name conventions * array['parentTableName']['tableName'] * * @access private * @var array **/ var $__conjunctionTables; /** * Conditional queries * * @access private * @var array **/ var $__conditions; /** * Record offset number * * @access private * @var integer **/ var $__offset; /** * Record limit number * * @access private * @var integer **/ var $__limit; /** * Total number of records in a table * * @access public * @var integer **/ var $tableCount; /** * Number of records by find condition * * @access public * @var integer **/ var $findCount; /** * Modified model * * @access private * @var string **/ var $__modifiedModel; /** * Modified model record id * * @access private * @var integer **/ var $__modifiedModelId; /** * Constructor **/ function ORM() { $this->_CI =& get_instance(); $this->_CI->load->helper('inflector'); { } } /** * Find * finds the ORMed data of the specified table * * @access public * @param string $table table name * @param integer $limit record limit number * @param integer $page page number (for pagination, must be positive) * @param integer $recursion recursion level (0 for none) * @return array table with associated data **/ function find($table, $limit = null, $page = 1, $recursion = null) { $this->table = $table; $model = singular($table); { $page = 1; } { $recursion = 1; } $this->__limit = $limit; $this->__offset = $limit * ($page - 1); // builds table relationship structure $schemaQuery = $this->_CI->db->getwhere('orm_schemas', array('model' => $model, 'recursion' => $recursion)); $schemaResult = $schemaQuery->result_array(); if (!$schemaResult) { $tableStructure[$table] = $this->__build_table_structure($table, $recursion); // stores model schema $this->_CI->db->set('model', $model); $this->_CI->db->set('recursion', $recursion); $this->_CI->db->insert('orm_schemas'); } else { } // populates data according to association $tableData = $this->__populate_table_data($tableStructure); // record count $this->tableCount = $this->_CI->db->count_all($table); return $tableData[$table]; } /** * Number of records * can use proxy active record methods before calling * e.g. $this->orm->where('post_title', 'my_post'); * * @access public * @param string $table table name * @return integer number of records **/ function recordsCount($table) { $this->_CI->db->select('COUNT(*)'); $this->__query_conditions(); $query = $this->_CI->db->get($table); $result = $query->result_array(); return $result[0]['COUNT(*)']; } /** * Build table structure * constructs an array according to the table associations * * @access private * @param string $table table name * @param integer $recursion recursion level (0 for none) * @return array table associations (structure only) **/ function __build_table_structure($table, $recursion) { $model = singular($table); // table names $tables = $this->_CI->db->list_tables(); // fields in each table $fields = $this->_CI->db->list_fields($table); $structure = null; if ($recursion > 0) { // determines associations according to the fields foreach ($fields as $field) { $fieldTable = plural($fieldModel); // relationships based on foreign keys { // belongsTo { $structure['belongsTo'][$fieldTable] = $this->__build_table_structure($fieldTable, $recursion - 1); } // belongsToSelf (is_xxxxx_of) else { $structure['belongsToSelf']["is_{$fieldModel}_of"] = $this->__build_table_structure($table, $recursion - 1); } } } // relationships based on other tables foreach ($tables as $tableName) { // hasOneMany { $tableFields = $this->_CI->db->list_fields($tableName); foreach ($tableFields as $tableField) { { $structure['hasOneMany'][$tableName] = $this->__build_table_structure($tableName, $recursion - 1); } } } // hasAndBelongsToMany { $this->__conjunctionTables[$table][$assocTable] = $tableName; $structure['hasAndBelongsToMany'][$assocTable] = $this->__build_table_structure($assocTable, $recursion - 1); } { $this->__conjunctionTables[$table][$assocTable] = $tableName; $structure['hasAndBelongsToMany'][$assocTable] = $this->__build_table_structure($assocTable, $recursion - 1); } } } return $structure; } /** * Populate table data * populates table data according to the table structure * * @access private * @param array $tableStructure table structure returned by $this->__build_table_structure() *OR* to be generated in the function * @param string $parentTable table name of the parent * @param string $relationship (belongsTo | belongsToSelf | hasOneMany | hasAndBelongsToMany) * @return array table with associated data **/ function __populate_table_data($tableStructure, $parentTable = null, $relationship = null, $parentIDValues = null) { // for some reason this part has to be separated from the same foreach followed by, a PHP bug perhaps? foreach ($tableStructure as $table => $relationships) { $tableData[$table] = $this->__select_table_data($table, $parentTable, $relationship, $parentIDValues); } foreach ($tableStructure as $table => $relationships) { if ($relationships) { { // primary and foreign key values of the parent table record // this is for use in the associated tables { $fields = $this->_CI->db->list_fields($parentTable); } else { $fields = $this->_CI->db->list_fields($table); } foreach ($fields as $field) { { $fieldTable = plural($fieldModel); $parentIDValues[$fieldTable] = $tableData[$table][$i][$field]; } } // iterates through relationships foreach ($relationships as $relationship => $assocTables) { // always passes the parent table name to all of the belongsToSelf associated tables (on every recursive level) { $tableData[$table][$i]['assocData'] += $this->__populate_table_data($assocTables, $parentTable, $relationship, $parentIDValues); } else { $tableData[$table][$i]['assocData'] += $this->__populate_table_data($assocTables, $table, $relationship, $parentIDValues); } } } } } return $tableData; } /** * Select table data * retrieves individual table data (to be used in $this->__populate_table_data()) * * @access private * @param string $table table name * @param string $parentTable table name of the parent * @param string $relationship (belongsTo | belongsToSelf | hasOneMany | hasAndBelongsToMany) * @return array table data **/ function __select_table_data($table, $parentTable = null, $relationship = null, $parentIDValues = null) { $offset = $this->__offset; $limit = $this->__limit; // initial table data (whatever the user specifies in $this->find()) if (!$parentTable) { $this->__query_conditions(); $query = $this->_CI->db->get($table, $limit, $offset); } else { // changes the table name to its parent table name if it's belongsToSelf (is_xxxxx_of) { // model name used in the array (e.g. 'parent' for 'is_parent_of') $table = $parentTable; } $model = singular($table); $parentModel = singular($parentTable); switch ($relationship) { case 'belongsToSelf': $query = $this->_CI->db->getwhere($table, array("{$isOfName}_id" => $parentIDValues[$table]), $limit, $offset); break; case 'belongsTo': $query = $this->_CI->db->getwhere($table, array("{$model}_id" => $parentIDValues[$table]), $limit, $offset); break; case 'hasOneMany': $query = $this->_CI->db->getwhere($table, array("{$parentModel}_id" => $parentIDValues[$parentTable]), $limit, $offset); break; case 'hasAndBelongsToMany': $conjTable = $this->__conjunctionTables[$parentTable][$table]; $query = $this->_CI->db->getwhere($table, array("{$conjTable}.{$parentModel}_id" => $parentIDValues[$parentTable]), $limit, $offset); break; default: break; } } $result = $query->result_array(); return $result; } /** * Find neighbours * finds the specified record data with its neighbours * useful for pagination (previous, next), etc * * @access public * @param integer $id record id * @param string $table table name * @param integer $deep record limit number (both sides, so $deep = 1 means fetching 3 records all together) * @param mixed $matchingFields array: fields to be matched OR string: the field to be matched * @param integer $assocLevel recursion level (0 for none) for the records * @return array table with associated data **/ function findNeighbours($id, $table, $deep = 1, $matchingFields = null, $assocLevel = 0) { $model = singular($table); // self (source record data) $self = $this->findId($id, $table, $deep, 1, $assocLevel); // neighbours on the left side $this->_CI->db->where($model.'_id <', $id); $this->__matchingFields($matchingFields, $table, $self); $this->_CI->db->orderby($model.'_id', 'DESC'); $neighboursLeft = $this->find($table, $deep, 1, $assocLevel); // neighbours on the right side $this->_CI->db->where($model.'_id >', $id); $this->__matchingFields($matchingFields, $table, $self); $neighboursRight = $this->find($table, $deep, 1, $assocLevel); // resorts data order if ($deep > 1) { } return $result; } /** * Query conditions * * @access private * @return void **/ function __query_conditions() { { foreach ($this->__conditions as $condition) { foreach ($condition as $method => $args) { @$this->_CI->db->$method($args[0], $args[1]); } } } } /** * Matching fields * takes the fields to be matched and executes 'where' queries * * @access private * @param mixed $matchingFields array: fields to be matched OR string: the field to be matched * @param string $table table name * @param array $self source record data * @return void **/ function __matchingFields($matchingFields, $table, $self) { if ($matchingFields) { { foreach ($matchingFields as $matchingField) { $this->_CI->db->where($matchingField, $self[$table][0][$matchingField]); } } else { $this->_CI->db->where($matchingFields, $self[$table][0][$matchingFields]); } } } /** * Create * inserts a new record to the specified table * * @access public * @param string $model model name * @return void **/ function create($model = null) { { $table = $this->table; $model = singular($table); } else { $table = plural($model); } $this->__rebuildFieldNames($table); $this->_CI->db->insert($table, $this->$model); // removes the object variable } /** * Delete * deletes a record * * @access public * @param integer $id record id * @param string $model model name * @return void **/ function delete($id, $model = null) { { $table = $this->table; $model = singular($table); } else { $table = plural($model); } } /** * Edit * passes $id and $model data to $this->save() * * @access public * @param integer $id record id * @param string $model model name * @return void **/ function edit($id, $model = null) { { $table = $this->table; $model = singular($table); } $this->__modifiedModel = $model; $this->__modifiedModelId = $id; } /** * Save * saves / updates data! * * @access public * @return object modified fields **/ function save() { $model = $this->__modifiedModel; $id = $this->__modifiedModelId; $table = plural($model); $this->__rebuildFieldNames($table); $this->_CI->db->where($model.'_id', $id); $this->_CI->db->update($table, $this->$model); $return = $this->$model; // removes the object variable return $return; } /** * Rebuild field names * rebuilds shortened field names to full field names * * @access private * @return void **/ function __rebuildFieldNames($table = null) { $model = singular($table); foreach ($this->$model as $field => $value) { // processes shortcut field names { // removes old field name $fieldOld = $field; // builds new field name $field = $model.'_'.$field; $this->$model->$field = $value; } } } /** * Method overloading * * @access private * @param string $method method name * @param array $args method arguments in array * @return void **/ function __call($method, $args) { // finds data according to a field value { // $args[0] = matching content // $args[1] = table name // $args[2] and onwards check $this->find(); $model = singular($args[1]); // $substrlen is the string length of the keywords (find, search, etc) // find (WHERE) { $substrlen = 4; // strlen('find') $function = 'where'; } // search (LIKE) else { $substrlen = 6; // strlen('search') $function = 'like'; } // completes field name // foreign key retain its field name // otherwise prepend it with model name and underscore { } else { } $argsCon[] = $field; $argsCon[] = $args[0]; return @$this->find($args[1], $args[2], $args[3], $args[4]); } { $this->create($model); } { $this->create($model); } { $this->create($args[0], $model); } { $this->delete($args[0], $model); } { $this->delete($args[0], $model); } else { switch ($method) { // proxies to CI Active Record functions case 'where': case 'orwhere': case 'like': case 'orlike': case 'groupby': case 'having': case 'orderby': case 'limit': break; default: break; } } } } ?>