Added Canvas 1.1.0, originally not under SCM so no historical development records...
[canvas.git] / library / Model2.php
blob3c5bda5b184023b3d214be4eb9f3cf67b4cdddd7
1 <?php
3 // @title Model Abstract Class
4 // @author Matt Todd
5 // @date 2005-11-26 5:26PM
6 // @desc Handles database interactivity for the specific Model (this
7 // is the abstract class that all the other Models are based on).
9 // load StdException
10 include_once 'stdexception.php';
12 // get adapter name for this environment
13 $adapter = new Config2();
14 $adapter = $adapter->database;
15 $adapter = strtolower($adapter[Config2::$environment]['adapter']);
17 // load adapter
18 include_once Conventions::adapter_path($adapter);
20 // classes
21 abstract class Model {
22 protected static $adapter = array(); // the current, active adapter
23 protected static $columns = array(); // columns for this table
24 public $table, $scope; // table name and scope (helps refine queries, usually for security purposes)
25 protected $rows = array(); // the row(s) being operated on or saught
26 protected $has_one, $has_many, // column associations
27 $has_property, $has_properties,
28 $belongs_to_many;
29 protected $validates; // data validation
31 // associations default values
32 public $default_order = '';
34 // constructor and loader
35 public function __construct() {
36 // load configuration
37 $config = new Config2();
39 // get all database config data
40 $config = $config->database;
42 // load environment-specific database connection information into the adapter
43 self::$adapter['config'] = $config[Config2::$environment];
45 // get the table name (usually the plural form of the model name, eg: shoe=>shoes or person=>people)
46 if(empty($this->table)) $this->table = Inflector::pluralize(get_class($this));
48 // make sure adapter is loaded
49 self::load_adapter();
51 // __constructor static-aliases
52 public static function begin($model) {
53 return new $model();
56 protected static function load_adapter() {
57 // get the adapter
58 $adapter = strtolower(self::$adapter['config']['adapter']);
60 // create an instance of the adapter if it doesn't already exist
61 if(empty(self::$adapter['handle']))
62 self::$adapter['handle'] = new $adapter(self::$adapter['config']);
64 // should this be here? do we want it to automatically connect?
65 self::$adapter['handle']->connect();
66 self::$adapter['handle']->select_db(self::$adapter['config']['database']);
69 protected function map_associations($params) {
70 if(array_search('non_greedy', $params) !== false) return $params['join'];
72 @use
73 Internally, find() calls this to set up joins for any immediate
74 associations (has_one/has_propert(y/ies)) or greedy associations
75 (has_many)
78 // check for has_property
79 if(!empty($this->has_property))
80 $this->has_properties[] = $this->has_property;
81 $this->has_property = null;
82 // and check for has_properties and then parse them
83 if(!empty($this->has_properties)) {
84 foreach(self::has_properties($this->has_properties) as $association) {
85 $joins[] = $association;
87 // $joins = array_unique($joins); // handle accidental duplication // deprecated of removing the data from $this->has_property instead
90 // handle many-to-many relationships
91 if(!empty($this->belongs_to_many)) {
92 $joins[] = array('table'=>$this->belongs_to_many[0], 'on'=>':@table.:singular_table_id=:table.id');
95 /*// handle has_one relationships when they are specified as 'greedy'
96 if(false && !empty($this->has_one)) {
97 foreach(self::has_one($this->has_one) as $association) {
98 $joins[] = $association;
102 // handle has_many relationships when they are specified as 'greedy'
103 if(false && !empty($this->has_many)) {
104 foreach(self::has_many($this->has_many as $association) {
105 $joins[] = $association;
109 // build up the joins into the proper format and return it
110 return self::build_joins_up($joins);
112 protected static function build_joins_up($all_joins) {
113 if(is_array($all_joins)) $joins = array_shift($all_joins); else return $all_joins;
114 if(!empty($all_joins)) $joins['join'] = self::build_joins_up($all_joins);
115 return $joins;
118 // database action functions
119 public function find($params = null) {
120 $this->before_find(); // callback
122 // reset results and iteration
123 $this->rows = null;
124 $this->rows = array();
126 // add the table to the parameters of the query and process all other automatic properties
127 if(empty($this->belongs_to_many)) {
128 $params['table'] = $this->table;
129 } else { // many-to-many relationship
130 $params['table'] = $this->belongs_to_many['through'];
132 // map associations
133 $params['join'] = $this->map_associations($params);
135 // execute select
136 $this->rows = self::$adapter['handle']->find($params); // include $scope as third param to give even more definition to select queries (for security)
138 if(!empty($this->rows)) foreach($this->rows as $id=>$row) {
139 if(empty($id) || empty($row)) continue;
140 $this->rows[$id] = $this->after_find_for_each($row);
143 $this->after_find(); // callback
145 // return $this to allow for stacked calls (like '$model->find()->all()')
146 return $this;
148 // deprecated to keep from having to do tedious error checking when a find() is called...
149 // return $this when number of rows is greater than 0, false if 0
150 // if(self::$adapter['handle']->rows_found() > 0) return $this; else return false;
153 public function find_all($params = null) {
154 if(!$this->find($params)) return false;
155 return $this->all();
158 public function find_by($params) {
159 // $params will be an associative array of ids and values as well as the where clause (eg: 'id=:id')
160 if(!$this->find(array('where'=>$params))) return false;
161 return $this->all();
164 public function find_by_id($id) {
165 return $this->find(array("where"=>array(':table.id=":id"', 'id'=>$id)));
168 public function save() {
169 $this->before_save(); // callback
171 // set up the params
172 $params = array('table'=>$this->table, 'values'=>$this->remove_non_columns($this->current()), 'where'=>array('id=":id"', 'id'=>$this->id), 'limit'=>'1');
174 // save the current row and update the reference within the collection
175 if($row = self::$adapter['handle']->save($params)) {
176 if(key($this->rows) == 'new') unset($this->rows['new']);
177 $this->rows[$row->id] = $row;
178 } else throw new ModelException();
180 $this->after_save(); // callback
182 // return $this when number of affected rows is greater than 0, false if 0
183 if(self::$adapter['handle']->affected_rows() > 0) return $this; else return false;
186 public function delete($params = null) {
187 $this->before_delete(); // callback
189 if(!$this->id) return false; // either find() and then delete() or delete() with parameters! duh!
191 // if $params is empty, give it a default selector of the current element
192 if(empty($params)) $params = array('table'=>$this->table, 'where'=>array('id=":id"', 'id'=>$this->id), 'limit'=>'1'); // delete the currently selected ID (and only the one)
194 $result = self::$adapter['handle']->delete($params);
196 // callback // passing in the parameters
197 // and the result/return value for good
198 // measure (for verbosity)
199 $this->after_delete($params, $result);
201 // return $this when number of affected rows is greater than 0, false if 0
202 if(self::$adapter['handle']->affected_rows() > 0) return $this; else return false;
204 public function delete_all($params = null) {
205 $this->before_delete(); // callback
207 // perform a find if there aren't any results already
208 if($this->is_empty()) $this->find($params);
210 // loop through results and delete away
211 foreach($this->rows as $rows) {
212 // delete this row
213 $this->delete(array('table'=>$this->table, 'where'=>array('id=":id"', 'id'=>$row->id), 'limit'=>'1'));
216 $this->after_delete($params, $result); // callback
218 // return true when number of affected rows is greater than 0, false if 0
219 if($this->db->affected_rows() > 0) return true; else return false;
222 // transactional functionality
223 public function start() {
224 // begin transaction
225 if($row = self::$adapter['handle']->begin()) {
226 // return on success
227 return true;
228 } else throw new ModelException();
230 public function commit() {
231 // commit and end transaction
232 if($row = self::$adapter['handle']->commit()) {
233 // return on success
234 return true;
235 } else throw new ModelException();
237 public function end() {
238 // alias of commit
239 return $this->commit();
241 public function rollback() {
242 // rollback transactions if there were problems
243 if($row = self::$adapter['handle']->rollback()) {
244 // return on success
245 return true;
246 } else throw new ModelException();
249 // facilitates the 'find_by_blah_and_blah' dynamic find functions
250 public function __call($name, $params) {
251 if(substr_count($name, "find_by_") > 0) { // find by
252 // take out function name
253 $keys = str_replace("find_by_", "", $name);
255 $params = $this->process_find_call($keys, $params);
257 return $this->find(array("where"=>$params));
258 } elseif(substr_count($name, "find_or_create_by_") > 0) { // find or create by
259 // take out function name
260 $keys = str_replace("find_or_create_by_", "", $name);
262 $params = $this->process_find_call($keys, $params);
264 if($this->find(array("where"=>$params))->is_empty()) {
265 foreach($params as $key=>$value) {
266 $this->$key = $value;
268 $this->save();
271 return true;
272 } else {
273 // something totally different and unexpected
276 protected function process_find_call($name, $func_params) {
277 // add support in for column1_or_column2 parsing
278 $columns = explode("_and_", $name);
279 while((list(, $key) = each($columns)) && (list(, $value) = each($func_params))) {
280 $params[$key] = $value;
282 return $params;
285 public function __set($key, $value) {
286 if(empty($this->rows)) {
287 $this->rows['new'] = new row();
288 $id = 'new';
289 } else $id = ($this->current()->id) ? $this->current()->id : 'new';
290 return $this->rows[$id]->$key = $value;
292 public function __get($key) {
293 if(empty($this->rows)) return false;
295 $id = $this->current()->id;
297 // handle associations if applicable
298 if($associate = $this->has_one($key)) {
299 $this->rows[$id]->$key = $associate;
300 return $associate;
302 if($associates = $this->has_many($key)) {
303 $this->rows[$id]->$key = $associates;
304 return $associates;
307 // handle has_* requests (for tests if associations have been set or exist)
308 if(substr($key, 0, 4) == 'has_') {
309 $key = substr($key, 4); // get out the keyword (minus the has_ prefix)
310 $singular_key = Inflector::singularize(get_class($this));
311 $count = $this->$key->count(array("{$singular_key}_id=':id'", 'id'=>$this->id));
312 if(!empty($count)) return true;
313 // if(!empty($this->$key)) return true;
314 // if(!$this->$key->is_empty()) return true;
315 else return false;
318 // handle quantity requests
319 if($key == 'count') return count($this->rows);
321 // calls the current row object
322 // (important to remain this way because just doing $this->id would recursively call itself)
323 return stripslashes($this->rows[$this->current()->id]->$key);
326 // iteration support methods
327 // returns the current row object (vital for __get() and __set() functionality)
328 public function current() {
329 if(empty($this->rows)) return false;
330 return current($this->rows);
332 // this returns $this->rows (an array of row objects)
333 public function all() {
334 if(empty($this->rows)) return false;
335 return $this->rows;
337 // the rest of these return $this to allow for $model->next()->id or such.
338 public function next() {
339 if(empty($this->rows)) return false;
340 next($this->rows);
341 return $this;
343 public function previous() {
344 if(empty($this->rows)) return false;
345 prev($this->rows);
346 return $this;
348 public function first() {
349 if(empty($this->rows)) return false;
350 reset($this->rows);
351 return $this;
353 public function last() {
354 if(empty($this->rows)) return false;
355 end($this->rows);
356 return $this;
359 // get the data in an array form
360 public function all_as_array() {
361 if(empty($this->rows)) return false;
362 foreach($this->rows as $row) {
363 $rows[$row->id] = $row->as_array();
365 return $rows;
368 // check for the presence of data
369 public function is_empty() {
370 return empty($this->rows);
373 // return number of entries
374 public function count($conditions = null) {
375 if($conditions === null) $conditions = array();
376 $model = get_class($this);
377 $model = new $model();
378 return $model->find(array('where'=>$conditions, 'columns'=>'count(id) as row_count', 'non_greedy'))->row_count;
380 public function count_rows() {
381 return count($this->rows);
384 // verification and data integrity support methods
385 protected function remove_non_columns($row) {
386 // get columns (and cache them if they aren't cached already)
387 if(empty(self::$columns[$this->table])) self::$columns[$this->table] = self::$adapter['handle']->columns($this->table);
388 $row->remove_non_columns(self::$columns[$this->table]);
389 return $row;
392 // assocation methods
393 protected static function has_properties($properties) {
394 // get {$with}
395 if($properties['with'] !== null) {
396 $with = $properties['with'];
397 unset($properties['with']);
400 foreach($properties as $property) {
401 $property = explode('.', $property);
402 $table = $property[0];
403 $columns = $property[1];
404 $single_table_id = ((empty($with)) ? ":singular_table_id" : $with);
405 $joins[] = array('table'=>$table, 'on'=>":@table.{$single_table_id}=:table.id", 'columns'=>$columns);
408 return $joins;
410 protected function has_one($association) {
411 if(!empty($this->rows[$this->current()->id]->$association)) return $this->rows[$this->current()->id]->$association;
412 if(!empty($this->has_one) && array_search($association, $this->has_one) !== false) {
413 $associate = new $association();
414 $associating_column = "{$association}_id";
415 $associate->find_by_id($this->$associating_column);
416 return $associate;
417 } else return false;
419 protected function has_many($association) {
420 if(!empty($this->rows[$this->current()->id]->$association)) return $this->rows[$this->current()->id]->$association;
421 if(!empty($this->has_many) && array_search($association, $this->has_many) !== false) {
422 $association_class = Inflector::singularize($association);
423 $associating_column = get_class($this);
424 $associate = new $association_class();
425 $associate->find(array('where'=>array($associating_column . '_id=":id"', 'id'=>$this->id), 'order_by'=>$associate->default_order));
426 return $associate;
427 } else return false;
430 // events/callbacks
431 protected function before_find() {}
432 protected function after_find() {}
433 protected function before_save() {}
434 protected function after_save() {}
435 protected function before_delete() {}
436 protected function after_delete() {}
437 protected function before_create() {}
438 protected function after_create() {}
439 protected function before_update() {}
440 protected function after_update() {}
441 protected function before_next() {}
442 protected function after_next() {}
444 // special events/callbacks
445 protected function after_find_for_each($row) {
446 return $row;
449 // descructor
450 public function __destruct() {
451 // possibly put some serialization code in here?
452 // end;
456 class ModelException extends StdException {}