root/trunk/system/libraries/ORM.php

Revision 2975, 25.6 kB (checked in by Shadowhand, 2 days ago)

Fixing #661, thanks Nodren.

  • Property svn:eol-style set to LF
  • Property copyright set to Copyright (c) 2007-2008 Kohana Team
  • Property svn:keywords set to Id
Line 
1 <?php defined('SYSPATH') or die('No direct script access.');
2  /**
3  * Object Relational Mapping (ORM) is a method of abstracting database
4  * access to standard PHP calls. All table rows are represented as a model.
5  *
6  * @see http://en.wikipedia.org/wiki/Active_record
7  * @see http://en.wikipedia.org/wiki/Object-relational_mapping
8  *
9  * $Id$
10  *
11  * @package    Core
12  * @author     Kohana Team
13  * @copyright  (c) 2007-2008 Kohana Team
14  * @license    http://kohanaphp.com/license.html
15  */
16 class ORM_Core {
17
18     // Database field caching
19     protected static $fields = array();
20
21     // Database instance
22     protected static $db;
23
24     // Automatic saving on model destruction
25     protected $auto_save = FALSE;
26
27     // This table
28     protected $class;
29     protected $table;
30
31     // SQL building status
32     protected $select = FALSE;
33     protected $where = FALSE;
34     protected $from = FALSE;
35     protected $order = FALSE;
36
37     // Default sorting
38     protected $sort = array('id', 'asc');
39
40     // Currently loaded object
41     protected $object;
42     protected $object_related;
43     protected $loaded = FALSE;
44     protected $saved = FALSE;
45
46     // Changed object keys
47     protected $changed = array();
48     protected $changed_related = array();
49
50     // Object Relationships
51     protected $has_one = array();
52     protected $has_many = array();
53     protected $belongs_to = array();
54     protected $belongs_to_many = array();
55     protected $has_and_belongs_to_many = array();
56
57     /**
58      * Factory method. Creates an instance of an ORM model and returns it.
59      *
60      * @param   string   model name
61      * @param   mixed    id to load
62      * @return  object
63      */
64     public static function factory($model = FALSE, $id = FALSE)
65     {
66         $model = empty($model) ? __CLASS__ : ucfirst($model).'_Model';
67         return new $model($id);
68     }
69
70     /**
71      * Initialize database, setup internal variables, find requested object.
72      *
73      * @return  void
74      */
75     public function __construct($id = FALSE)
76     {
77         // Fetch table name
78         empty($this->class) and $this->class = strtolower(substr(get_class($this), 0, -6));
79         empty($this->table) and $this->table = inflector::plural($this->class);
80
81         // Connect to the database
82         $this->connect();
83
84         if (is_object($id))
85         {
86             // Preloaded object
87             $this->object = $id;
88
89             // Convert the value to the correct type
90             $this->load_object_types();
91
92             // Object is loaded
93             $this->loaded = $this->saved = TRUE;
94         }
95         else
96         {
97             if (empty($id))
98             {
99                 // Load an empty object
100                 $this->clear();
101             }
102             else
103             {
104                 // Query and load object
105                 $this->find($id);
106             }
107         }
108     }
109
110     /**
111      * Enables automatic saving of the object when the model is destroyed.
112      *
113      * @return  void
114      */
115     public function __destruct()
116     {
117         if ($this->auto_save == TRUE)
118         {
119             try
120             {
121                 // Automatically save the model
122                 $this->save();
123             }
124             catch (Exception $e)
125             {
126                 /// Log the error, rather than trying to display it, to avoid
127                 // "stack frame" errors. http://bugs.php.net/bug.php?id=33598
128                 Log::add('error', $e->getMessage());
129             }
130         }
131     }
132
133     /**
134      * Reloads the database when the object is unserialized.
135      *
136      * @return  void
137      */
138     public function __wakeup()
139     {
140         // Connect to the database
141         $this->connect();
142     }
143
144     /**
145      * Magic method for getting object and model keys.
146      *
147      * @param   string  key name
148      * @return  mixed
149      */
150     public function __get($key)
151     {
152         if (isset($this->object->$key))
153         {
154             return $this->object->$key;
155         }
156         elseif (in_array($key, $this->has_one) OR in_array($key, $this->belongs_to))
157         {
158             // Set the model name
159             $model = ucfirst($key).'_Model';
160
161             // Set the child id name
162             $child_id = $key.'_id';
163
164             $this->object->$key = new $model
165             (
166                 isset($this->object->$child_id)
167                 // Get the foreign object using the key defined in this object
168                 ? $this->object->$child_id
169                 // Get the foreign object using the primary key of this object
170                 : array($this->class.'_id' => $this->object->id)
171             );
172
173             // Return the model
174             return $this->object->$key;
175         }
176         elseif (in_array($key, $this->has_and_belongs_to_many))
177         {
178             if (empty($this->object_related[$key]['cur']))
179             {
180                 // Create an array
181                 $this->object_related[$key]['cur'] = array();
182
183                 // Foreign key
184                 $fk = inflector::singular($key).'_id';
185
186                 // Query to find relationships
187                 $query = self::$db
188                     ->select($fk)
189                     ->from($this->related_table($key))
190                     ->where($this->class.'_id', $this->id)
191                     ->get();
192
193                 foreach ($query as $row)
194                 {
195                     // Set relationships
196                     $this->object_related[$key]['cur'][] = $row->$fk;
197                 }
198             }
199
200             // Return the current relationships
201             return $this->object_related[$key]['cur'];
202         }
203         else
204         {
205             switch ($key)
206             {
207                 case 'table_name':
208                     return $this->table;
209                 break;
210                 case 'class_name':
211                     return $this->class;
212                 break;
213                 case 'auto_save':
214                     return $this->auto_save;
215                 break;
216             }
217         }
218     }
219
220     /**
221      * Magic method for setting object and model keys.
222      *
223      * @param   string  key name
224      * @param   mixed   value to set
225      * @return  void
226      */
227     public function __set($key, $value)
228     {
229         if ($key != 'id' AND isset(self::$fields[$this->table][$key]))
230         {
231             // Force the value to the correct type
232             $value = $this->set_value_type($key, $value);
233
234             if ($this->object->$key !== $value)
235             {
236                 // Set new value
237                 $this->object->$key = $value;
238
239                 // Data has changed
240                 $this->changed[$key] = $key;
241
242                 // Data has been changed
243                 $this->saved = FALSE;
244             }
245         }
246         elseif (in_array($key, $this->has_and_belongs_to_many) AND is_array($value))
247         {
248             if (empty($this->object_related[$key]))
249             {
250                 // Force a load of the relationship
251                 $this->$key;
252             }
253
254             // Set the new relationships
255             $this->object_related[$key]['new'] = $value;
256
257             // Relationships changed
258             $this->changed_related[$key] = $key;
259         }
260         else
261         {
262             switch ($key)
263             {
264                 case 'auto_save':
265                     $this->auto_save = (bool) $value;
266                 break;
267             }
268         }
269     }
270
271     /**
272      * Magic method for calling ORM methods. This handles:
273      *  - as_array
274      *  - find_by_*
275      *  - find_all_by_*
276      *  - find_related_*
277      *  - has_*
278      *  - add_*
279      *  - remove_*
280      *
281      * @throws  Kohana_Exception
282      * @param   string  method name
283      * @param   array   method arguments
284      * @return  mixed
285      */
286     public function __call($method, $args)
287     {
288         if ($method === 'as_array')
289         {
290             // Return all of the object data as an array
291             return (array) $this->object;
292         }
293
294         if (substr($method, 0, 8) === 'find_by_' OR substr($method, 0, 12) === 'find_all_by_')
295         {
296             // Make a find_by call
297             return $this->call_find_by($method, $args);
298         }
299
300         if (substr($method, 0, 13) === 'find_related_')
301         {
302             // Make a find_related call
303             return $this->call_find_related(substr($method, 13), $args);
304         }
305
306         if (preg_match('/^(has|add|remove)_(.+)$/', $method, $matches))
307         {
308             if (empty($this->object->id))
309             {
310                 // many<>many relationships only work when the object has been saved
311                 return FALSE;
312             }
313
314             // Make a has/add/remove call
315             return $this->call_has_add_remove($method, $args, $matches);
316         }
317
318         if (method_exists(self::$db, $method))
319         {
320             // Do not allow query methods
321             if (preg_match('/^(?:query|get|insert|update|list_fields|field_data)$/', $method))
322                 return $this;
323
324             if ($method === 'select')
325             {
326                 $this->select = TRUE;
327             }
328             elseif (preg_match('/where|like|in|regex/', $method))
329             {
330                 $this->where = TRUE;
331             }
332             elseif ($method === 'from')
333             {
334                 $this->from = TRUE;
335             }
336             elseif ($method === 'orderby')
337             {
338                 $this->order = TRUE;
339             }
340
341             // Pass through to Database, manually calling up to 2 args, for speed.
342             switch (count($args))
343             {
344                 case 0:
345                     return self::$db->$method();
346                 break;
347                 case 1:
348                     self::$db->$method(current($args));
349                 break;
350                 case 2:
351                     self::$db->$method(current($args), next($args));
352                 break;
353                 default:
354                     call_user_func_array(array(self::$db, $method), $args);
355                 break;
356             }
357
358             return $this;
359         }
360
361         // Throw an exception to warn the user
362         throw new Kohana_Exception('orm.method_not_implemented', $method, get_class($this));
363     }
364
365     /**
366      * __call: find_by_*, find_all_by_*
367      *
368      * @param   string  method
369      * @param   array   arguments
370      * @return  object
371      */
372     protected function call_find_by($method, $args)
373     {
374         // Use ALL
375         $ALL = (substr($method, 0, 12) === 'find_all_by_');
376
377         // Method args
378         $method = $ALL ? substr($method, 12) : substr($method, 8);
379
380         // WHERE is manually set
381         $this->where = TRUE;
382
383         // split method name into $keys array by "_and_" or "_or_"
384         if (is_array($keys = $this->find_keys($method)))
385         {
386             if (strpos($method, '_or_') === FALSE)
387             {
388                 // Use AND WHERE
389                 self::$db->where(array_combine($keys, $args));
390             }
391             else
392             {
393                 if (count($args) === 1)
394                 {
395                     $val = current($args);
396                     foreach ($keys as $key)
397                     {
398                         // Use OR WHERE, with a single value
399                         self::$db->orwhere(array($key => $val));
400                     }
401                 }
402                 else
403                 {
404                     // Use OR WHERE, with multiple values
405                     self::$db->orwhere(array_combine($keys, $args));
406                 }
407             }
408         }
409         else
410         {
411             // Set WHERE
412             self::$db->where(array($keys => current($args)));
413         }
414
415         if ($ALL)
416         {
417             // Array of results
418             return $this->load_result(TRUE);
419         }
420         else
421         {
422             // Allow chains
423             return $this->find();
424         }
425     }
426
427     /**
428      * __call: find_related_*
429      *
430      * @param   string   method name
431      * @param   array    arguments
432      * @return  object
433      */
434     protected function call_find_related($table, $args)
435     {
436         // Construct a new model
437         $model = $this->load_model($table);
438
439         // Remote reference to this object
440         $remote = array($this->class.'_id' => $this->object->id);
441
442         if (in_array($table, $this->has_one))
443         {
444             // Find one<>one relationships
445             return $model->where($remote)->find();
446         }
447         elseif (in_array($table, $this->has_many))
448         {
449             // Find one<>many relationships
450             $model->where($remote);
451         }
452         elseif (in_array($table, $this->has_and_belongs_to_many))
453         {
454             // Find many<>many relationships, via a JOIN
455             $this->related_join($table);
456         }
457         elseif (in_array($table, $this->belongs_to_many))
458         {
459             // Use the foreign column name to check the relationship
460             $id = $this->class.'_id';
461
462             if ($model->$id === NULL)
463             {
464                 // Find many<>many relationships, via a JOIN
465                 $this->related_join($table);
466             }
467             else
468             {
469                 // Find one<>many relationships
470                 $model->where($remote);
471             }
472         }
473         else
474         {
475             // This table does not have ownership
476             return FALSE;
477         }
478
479         return $model->load_result(TRUE);
480     }
481
482     /**
483      * __call: has_*, add_*, remove_*
484      *
485      * @param   string   method
486      * @param   array    arguments
487      * @param   array    action matches
488      * @return  boolean
489      */
490     protected function call_has_add_remove($method, $args, $matches)
491     {
492         $action = $matches[1];
493         $model  = (count($args) > 0 AND is_object($args[0])) ? $args[0] : $this->load_model($matches[2]);
494
495         // Real foreign table name
496         $table = $model->table_name;
497
498         // Sanity check, make sure that this object has ownership
499         if (in_array($matches[2], $this->has_one))
500         {
501             $ownership = 1;
502         }
503         elseif (in_array($table, $this->has_many))
504         {
505             $ownership = 2;
506         }
507         elseif (in_array($table, $this->has_and_belongs_to_many))
508         {
509             $ownership = 3;
510         }
511         else
512         {
513             // Model does not have ownership, abort now
514             return FALSE;
515         }
516
517         // Primary key related to this object
518         $primary = $this->class.'_id';
519
520         // Related foreign key
521         $foreign = $model->class_name.'_id';
522
523         if ( ! is_object(current($args)))
524         {
525             if ($action === 'add' AND is_array(current($args)))
526             {
527                 $arg = current($args);
528
529                 foreach ($arg as $key => $val)
530                 {
531                     // Fill object with data from array
532                     $model->$key = $val;
533                 }
534             }
535             else
536             {
537                 if ($ownership === 1 OR $ownership === 2)
538                 {
539                     // Make sure the related key matches this object id
540                     self::$db->where($primary, $this->object->id);
541                 }
542
543                 // Load the related object
544                 $model->find(current($args));
545             }
546         }
547
548         if ($ownership === 3)
549         {
550             // Save the model before finishing the action
551             $model->save();
552
553             // The many<>many relationship, via a joining table
554             $relationship = array
555             (
556                 $primary => $this->object->id,
557                 $foreign => $model