diff options
Diffstat (limited to 'modules/orm')
-rw-r--r-- | modules/orm/classes/orm.php | 579 | ||||
-rw-r--r-- | modules/orm/classes/ormresult.php | 113 |
2 files changed, 692 insertions, 0 deletions
diff --git a/modules/orm/classes/orm.php b/modules/orm/classes/orm.php new file mode 100644 index 0000000..cd09c3b --- /dev/null +++ b/modules/orm/classes/orm.php @@ -0,0 +1,579 @@ +<?php + +/** + * ORM allows you to access database items and their relationships in an OOP manner, + * it is easy to setup and makes a lot of use of naming convention. + * + * @method mixed limit(int $limit = null) Set number of rows to return. + * Without arguments returns current limit, returns self otherwise. + * + * @method mixed offset(int $offset = null) Set the offset for the first row in result. + * Without arguments returns current offset, returns self otherwise. + * + * @method mixed orderby(string $column, string $dir) Adds a column to ordering parameters + * + * @method mixed where(mixed $key, mixed $operator = null, mixed $val = null) behaves just like Query_Database::where() + * + * @see Query_Database::where() + */ +class ORM { + + /** + * Specifies which table the model will use, can be overridden + * @var string + * @access public + */ + public $table = null; + + /** + * Specifies which connection the model will use, can be overridden + * but a model can have relatioships only with models utilizing the same connection + * @var string + * @access public + */ + public $connection = 'default'; + + /** + * Specifies which column is treated as PRIMARY KEY + * @var string + * @access public + */ + public $id_field='id'; + + /** + * You can define 'Belongs to' relationships buy changing this array + * @var array + * @access protected + */ + protected $belongs_to=array(); + + /** + * You can define 'Has one' relationships buy changing this array + * @var array + * @access protected + */ + protected $has_one = array(); + + /** + * You can define 'Has many' relationships buy changing this array + * @var array + * @access protected + */ + protected $has_many = array(); + + /** + * An instance of the database connection + * @var DB + * @access private + */ + private $db; + + /** + * Current row returned by the database + * @var array + * @access protected + */ + protected $_row = array(); + + /** + * Associated query builder + * @var Query_Database + * @access public + */ + public $query; + + /** + * A flag whether the row was loaded from the database + * @var boolean + * @access private + */ + private $_loaded = false; + + /** + * The name of the model + * @var string + * @access public + */ + public $model_name; + + /** + * Cached properties + * @var array + * @access private + */ + private $_cached = array(); + + /** + * Constructs the model. To use ORM it is enough to + * just create a model like this: + * <code> + * class Fairy_Model extends ORM { } + * </code> + * By default it will assume that the name of your table + * is the plural form of the models' name, the PRIMARY KEY is id, + * and will use the 'default' connection. This behaiour is easy to be + * changed by overriding $table, $id and $db properties. + * + * @return void + * @access public + * @ see $table + * @ see $id + * @ see $db + */ + public function __construct() { + $this->query = DB::instance($this->connection)->query('select'); + $this->model_name=strtolower(preg_replace('#_Model$#i', '', get_class($this))); + if ($this->table == null) + $this->table = ORM::plural($this->model_name); + $this->query->table($this->table); + + foreach(array('belongs_to', 'has_one', 'has_many') as $rels) { + $normalized=array(); + foreach($this->$rels as $key => $rel) { + if (!is_array($rel)) { + $key = $rel; + $rel=array(); + } + $normalized[$key]=$rel; + if (!isset($rel['model'])) { + $rel['model']=$normalized[$key]['model']=$rels=='has_many'?ORM::singular($key):$key; + } + + $normalized[$key]['type']=$rels; + if (!isset($rel['key'])) + $normalized[$key]['key'] = $rels != 'belongs_to'?($this->model_name.'_id'):$rel['model'].'_id'; + + if ($rels == 'has_many' && isset($rel['through'])) + if (!isset($rel['foreign_key'])) + $normalized[$key]['foreign_key']=$rel['model'].'_id'; + } + $this->$rels=$normalized; + + } + + } + + /** + * Magic method for call Query_Database methods + * + * @param string $method Method to call + * @param array $arguments Arguments passed to the method + * @return mixed Returns self if parameters were passed. If no parameters weere passed returns + * current value for the associated parameter + * @throws Exception If method doesn't exist + * @access public + */ + public function __call($method, $arguments) { + if (!in_array($method, array('limit', 'offset', 'orderby', 'where'))) + throw new Exception("Method '{$method}' doesn't exist on .".get_class($this)); + $res = call_user_func_array(array($this->query, $method), $arguments); + if(is_subclass_of($res,'Query_Database')) + return $this; + return $res; + } + + /** + * Finds all rows that meet set criteria. + * + * @return ORMResult Returns ORMResult that you can use in a 'foreach' loop. + * @access public + */ + public function find_all() { + return new ORMResult(get_class($this), $res=$this->query->execute()); + } + + /** + * Searches for the first row that meets set criteria. If no rows match it still returns an ORM object + * but with its loaded() flag being False. calling save() on such an object will insert a new row. + * + * @return ORM Found item or new object of the current model but with loaded() flag being False + * @access public + */ + public function find() { + $res=new ORMResult(get_class($this), $res=$this->query->limit(1)->execute()); + return $res->current(); + } + + /** + * Counts all rows that meet set criteria. Ignores limit and offset. + * + * @return int Number of rows + * @access public + */ + public function count_all() { + $query = clone $this->query; + $query->type('count'); + return $query->execute(); + + } + + /** + * Checks if the item is considered to be loaded from the database + * + * @return boolean Returns True if the item was loaded + * @access public + */ + public function loaded() { + return $this->_loaded; + } + + /** + * Returns the row associated with current ORM item as an associative array + * + * @return array Associative array representing the row + * @access public + */ + public function as_array() { + return $this->_row; + } + + /** + * Returns a clone of query builder that is being used to set conditions. + * It is useful for example if you let ORM manage bulding a complex query using it's relationship + * system, then you get the clone of that query and alter it to your liking, + * so there is no need to writing relationship joins yourself. + * + * @return Query_Database A clone of the current query builder + * @access public + */ + public function query() { + return clone $this->query; + } + + /** + * You can override this method to return additional properties that you would like to use + * in your model. One advantage for using this instead of just overriding __get() is that + * in this way the properties also get cached. + * + * @param string $property The name of the property to get + * @return void + * @access public + */ + public function get($property) { + + } + + /** + * Magic method that allows accessing row columns as properties and also facilitates + * access to relationships and custom properties defined in get() method. + * If a relatioship is being accessed, it will return an ORM model of the related table + * and automatically alter its query so that all your previously set conditions will remain + + * @param string $column Name of the column, propert or relationship to get + * @return mixed + * @access public + * @throws Exception If neither property nor a relationship with such name is found + */ + public function __get($column) { + if (array_key_exists($column,$this->_row)) + return $this->_row[$column]; + if (array_key_exists($column,$this->_cached)) + return $this->_cached[$column]; + if (($val = $this->get($column))!==null) { + $this->_cached[$column] = $val; + return $val; + } + $relations = array_merge($this->has_one, $this->has_many, $this->belongs_to); + + if ($target = Misc::arr($relations, $column, false)) { + $model = ORM::factory($target['model']); + $model->query = clone $this->query; + if ($this->loaded()) + $model->query->where($this->id_field,$this->_row[$this->id_field]); + if ($target['type']=='has_many'&&isset($target['through'])) { + $lastAlias = $model->query->lastAlias(); + $throughAlias=$model->query->addAlias(); + $newAlias = $model->query->addAlias(); + $model->query->join(array($target['through'], $throughAlias), array( + $lastAlias.'.'.$this->id_field, + $throughAlias.'.'.$target['key'], + ),'inner'); + $model->query->join(array($model->table, $newAlias), array( + $throughAlias.'.'.$target['foreign_key'], + $newAlias.'.'.$model->id_field, + ),'inner'); + }else{ + $lastAlias = $model->query->lastAlias(); + $newAlias = $model->query->addAlias(); + if ($target['type'] == 'belongs_to') { + $model->query->join(array($model->table, $newAlias), array( + $lastAlias.'.'.$target['key'], + $newAlias.'.'.$model->id_field, + ),'inner'); + }else { + $model->query->join(array($model->table, $newAlias), array( + $lastAlias.'.'.$this->id_field, + $newAlias.'.'.$target['key'], + ), 'inner'); + } + } + $model->query->fields(array("$newAlias.*")); + if ($target['type'] != 'has_many' && $model->loaded() ) { + $model = $model->find(); + $this->_cached[$column]=$model; + } + return $model; + } + + throw new Exception("Property {$column} not found on {$this->model_name} model."); + } + + /** + * Magic method to update record values when set as properties or to add an ORM item to + * a relation. By assigning an ORM object to a relationship a relationship is created between the + * current item and the passed one Using properties this way is a shortcut to the add() method. + * + * @param string $column Column or relationship name + * @param mixed $val Column value or an ORM item to be added to a relation + * @return void + * @access public + * @see add() + */ + public function __set($column, $val) { + $relations = array_merge($this->has_one, $this->has_many, $this->belongs_to); + if (array_key_exists($column,$relations)){ + $this->add($column, $val); + }else{ + $this->_row[$column] = $val; + } + $this->_cached=array(); + } + + /** + * Create a relationship between current item and an other one + * + * @param string $relation Name of the relationship + * @param ORM $model ORM item to create a relationship with + * @return void + * @access public + * @throws Exception Exception If realtionship is not defined + * @throws Exception Exception If current item is not in the database yet (isn't considered loaded()) + * @throws Exception Exception If passed item is not in the database yet (isn't considered loaded()) + */ + public function add($relation, $model) { + + if (!$this->loaded()) + throw new Exception("Model must be loaded before you try adding relationships to it. Probably you haven't saved it."); + if (!$model->loaded()) + throw new Exception("Model must be loaded before added to a relationship. Probably you haven't saved it."); + + $rels = array_merge($this->has_one, $this->has_many,$this->belongs_to); + $rel = Misc::arr($rels, $relation, false); + if (!$rel) + throw new Exception("Model doesn't have a '{$relation}' relation defined"); + + if ($rel['type']=='belongs_to') { + $key=$rel['key']; + $this->$key = $model->_row[$this->id_field]; + $this->save(); + }elseif (isset($rel['through'])) { + $exists = DB::instance($this->connection)->query('count') + ->table($rel['through']) + ->where(array( + array($rel['key'],$this->_row[$this->id_field]), + array($rel['foreign_key'],$model->_row[$model->id_field]) + )) + ->execute(); + if(!$exists) + DB::instance($this->connection)->query('insert') + ->table($rel['through']) + ->data(array( + $rel['key'] => $this->_row[$this->id_field], + $rel['foreign_key'] =>$model->_row[$model->id_field] + )) + ->execute(); + }else { + $key=$rel['key']; + $model->$key = $this->_row[$this->id_field]; + $model->save(); + } + $this->_cached=array(); + } + + /** + * Removes a relationship between current item and the passed one + * + * @param string $relation Name of the relationship + * @param ORM $model ORM item to remove relationship with. Can be omitted for 'belongs_to' relationships + * @return void + * @access public + * @throws Exception Exception If realtionship is not defined + * @throws Exception Exception If current item is not in the database yet (isn't considered loaded()) + * @throws Exception Exception If passed item is not in the database yet (isn't considered loaded()) + */ + public function remove($relation, $model=false) { + + if (!$this->loaded()) + throw new Exception("Model must be loaded before you try removing relationships from it."); + + $rels = array_merge($this->has_one, $this->has_many,$this->belongs_to); + $rel = Misc::arr($rels, $relation, false); + if (!$rel) + throw new Exception("Model doesn't have a '{$relation}' relation defined"); + + if ($rel['type']!='belongs_to'&&(!$model||!$model->loaded())) + throw new Exception("Model must be loaded before being removed from a has_one or has_many relationship."); + if ($rel['type']=='belongs_to') { + $key=$rel['key']; + $this->$key = null; + $this->save(); + }elseif (isset($rel['through'])) { + $exists = DB::instance($this->connection)->query('delete') + ->table($rel['through']) + ->where(array( + array($rel['key'],$this->_row[$this->id_field]), + array($rel['foreign_key'],$model->_row[$model->id_field]) + )) + ->execute(); + }else { + $key=$rel['key']; + $model->$key = null; + $model->save(); + } + $this->_cached=array(); + } + + /** + * Deletes current item from the database + * + * @return void + * @access public + * @throws Exception If the item is not in the database, e.g. is not loaded() + */ + public function delete() { + if (!$this->loaded()) + throw new Exception("Cannot delete an item that wasn't selected from database"); + DB::instance($this->connection)->query('delete') + ->table($this->table) + ->where($this->id_field, $this->_row[$this->id_field]) + ->execute(); + $this->_cached=array(); + } + + /** + * Deletes all items that meet set conditions. Use in the same way + * as you would a find_all() method. + * + * @return ORM Returns self + * @access public + */ + public function delete_all() { + $query = clone $this->query; + $query->type('delete'); + $query->execute(); + return $this; + } + + /** + * Saves the item back to the database. If item is loaded() it will result + * in an update, otherwise a new row will be inserted + * + * @return ORM Returns self + * @access public + */ + public function save() { + if (isset($this->_row[$this->id_field])) { + $query = DB::instance($this->connection)->query('update') + ->table($this->table) + ->where($this->id_field,$this->_row[$this->id_field]); + }else { + $query = DB::instance($this->connection)->query('insert') + ->table($this->table); + } + $query->data($this->_row); + $query->execute(); + + if (isset($this->_row[$this->id_field])) { + $id=$this->_row[$this->id_field]; + }else { + $id=DB::instance($this->connection)->get_insert_id(); + } + $row =(array) DB::instance($this->connection)->query('select') + ->table($this->table) + ->where($this->id_field, $id)->execute()->current(); + $this->values($row,true); + return $this; + } + + + /** + * Batch updates item columns using an associative array + * + * @param array $row Associative array of key => value pairs + * @param boolean $set_loaded Flag to consider the ORM item loaded. Useful if you selected + * the row from the database and want to wrap it in ORM + * @return ORM Returns self + * @access public + */ + public function values($row, $set_loaded = false) { + $this->_row = array_merge($this->_row, $row); + if ($set_loaded) + $this->_loaded = true; + $this->_cached=array(); + return $this; + } + + /** + * Initializes ORM model by name, and optionally fetches an item by id + * + * @param string $name Model name + * @param mixed $id If set ORM will try to load the item with this id from the database + * @return ORM ORM model, either empty or preloaded + * @access public + * @static + */ + public static function factory($name,$id=null){ + $model = $name.'_Model'; + $model=new $model; + if ($id != null) + return $model->where($model->id_field, $id)->find() + ->data(array($model->id_field,$id)); + return $model; + } + + /** + * Gets plural form of a noun + * + * @param string $str Noun to get a plural form of + * @return string Plural form + * @access private + * @static + */ + private static function plural($str){ + $regexes=array( + '/^(.*?[sxz])$/i' => '\\1es', + '/^(.*?[^aeioudgkprt]h)$/i' => '\\1es', + '/^(.*?[^aeiou])y$/i'=>'\\1ies', + ); + foreach($regexes as $key=>$val){ + $str = preg_replace($key, $val, $str,-1, $count); + if ($count) + return $str; + } + return $str.'s'; + } + + /** + * Gets singular form of a noun + * + * @param string $str Noun to get singular form of + * @return string Singular form of the noun + * @access private + * @static + */ + private static function singular($str){ + $regexes=array( + '/^(.*?us)$/i' => '\\1', + '/^(.*?[sxz])es$/i' => '\\1', + '/^(.*?[^aeioudgkprt]h)es$/i' => '\\1', + '/^(.*?[^aeiou])ies$/i' => '\\1y', + '/^(.*?)s$/'=>'\\1' + ); + foreach($regexes as $key=>$val){ + $str = preg_replace($key, $val, $str,-1, $count); + if ($count) + return $str; + } + return $str; + } +}
\ No newline at end of file diff --git a/modules/orm/classes/ormresult.php b/modules/orm/classes/ormresult.php new file mode 100644 index 0000000..5dcada0 --- /dev/null +++ b/modules/orm/classes/ormresult.php @@ -0,0 +1,113 @@ +<?php + +/** + * Allows iterating over ORM objects inside loops lie 'foreach', + * while preserving performanceby working with only a single row + * at a time. It wraps conveniently wraps around Database_Result class + * returning ORM object instead of just data object. + * + * @see Database_Result + */ +class ORMResult implements Iterator { + + /** + * Name of the model that the rows belong to + * @var string + * @access private + */ + private $_model; + + /** + * Database result + * @var Result_Database + * @access private + */ + private $_dbresult; + + /** + * Initialized an ORMResult with which model to use and which result to + * iterate over + * + * @param string $model Model name + * @param Result_Database $dbresult Database result + * @return void + * @access public + */ + public function __construct($model,$dbresult){ + $this->_model=$model; + $this->_dbresult = $dbresult; + } + + /** + * Rewinds database cursor to the first row + * + * @return void + * @access public + */ + function rewind() { + $this->_dbresult->rewind(); + } + + /** + * Gets an ORM Model of the current row + * + * @return ORM Model of the current row of the result set + * @access public + */ + function current() { + $model = new $this->_model; + if (!$this->_dbresult->valid()) + return $model; + $model->values((array)$this->_dbresult->current(),true); + return $model; + } + + /** + * Gets current rows' index number + * + * @return int Row number + * @access public + */ + function key() { + return $this->_dbresult->key(); + } + + /** + * Iterates to the next row in the result + * + * @return void + * @access public + */ + function next() { + $this->_dbresult->next(); + } + + /** + * Checks if current row is valid. + * + * @return bool returns false if we reach the end of the result set. + * @access public + */ + function valid() { + return $this->_dbresult->valid(); + } + + /** + * Returns an array of all rows as ORM objects if $rows is False, + * or just an array of result rows with each row being a standart object, + * this can be useful for functions like json_encode. + * + * @param boolean $rows Whether to return just rows and not ORM objects + * @return array Array of ORM objects or standart objects representing rows + * @access public + */ + public function as_array($rows = false) { + if ($rows) + return $this->_dbresult->as_array(); + $arr = array(); + foreach($this as $row) + $arr[] = $row; + return $arr; + } + +}
\ No newline at end of file |