root/fORM.php

Revision 866, 37.6 kB (checked in by wbond, 2 weeks ago)

Completed ticket #466 - fixed documentation for fORM::tablize()

LineHide Line Numbers
1 <?php
2 /**
3  * Dynamically handles many centralized object-relational mapping tasks
4  *
5  * @copyright  Copyright (c) 2007-2010 Will Bond
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fORM
11  *
12  * @version    1.0.0b21
13  * @changes    1.0.0b21  Fixed some documentation to reflect the API changes from v1.0.0b9 [wb, 2010-07-14]
14  * @changes    1.0.0b20  Added the ability to register a wildcard active record method for all classes [wb, 2010-04-22]
15  * @changes    1.0.0b19  Added the method ::isClassMappedToTable() [wb, 2010-03-30]
16  * @changes    1.0.0b18  Added the `post::loadFromIdentityMap()` hook [wb, 2010-03-14]
17  * @changes    1.0.0b17  Changed ::enableSchemaCaching() to rely on fDatabase::clearCache() instead of explicitly calling fSQLTranslation::clearCache() [wb, 2010-03-09]
18  * @changes    1.0.0b16  Backwards Compatibility Break - renamed ::addCustomClassToTableMapping() to ::mapClassToTable(). Added ::getDatabaseName() and ::mapClassToDatabase(). Updated code for new fORMDatabase and fORMSchema APIs [wb, 2009-10-28]
19  * @changes    1.0.0b15  Added support for fActiveRecord to ::getRecordName() [wb, 2009-10-06]
20  * @changes    1.0.0b14  Updated documentation for ::registerActiveRecordMethod() to include info about prefix method matches [wb, 2009-08-07]
21  * @changes    1.0.0b13  Updated documentation for ::registerRecordSetMethod() [wb, 2009-07-14]
22  * @changes    1.0.0b12  Updated ::callReflectCallbacks() to accept a class name instead of an object [wb, 2009-07-13]
23  * @changes    1.0.0b11  Added ::registerInspectCallback() and ::callInspectCallbacks() [wb, 2009-07-13]
24  * @changes    1.0.0b10  Fixed a bug with ::objectify() caching during NULL date/time/timestamp values and breaking further objectification [wb, 2009-06-18]
25  * @changes    1.0.0b9   Added caching for performance and changed some method APIs to only allow class names instead of instances [wb, 2009-06-15]
26  * @changes    1.0.0b8   Updated documentation to reflect removal of `$associate` parameter for callbacks passed to ::registerRecordSetMethod() [wb, 2009-06-02]
27  * @changes    1.0.0b7   Added ::enableSchemaCaching() to replace fORMSchema::enableSmartCaching() [wb, 2009-05-04]
28  * @changes    1.0.0b6   Added the ability to pass a class instance to ::addCustomClassTableMapping() [wb, 2009-02-23]
29  * @changes    1.0.0b5   Backwards compatibility break - renamed ::addCustomTableClassMapping() to ::addCustomClassTableMapping() and swapped the parameters [wb, 2009-01-26]
30  * @changes    1.0.0b4   Fixed a bug with retrieving fActiveRecord methods registered for all classes [wb, 2009-01-14]
31  * @changes    1.0.0b3   Fixed a static method callback constant [wb, 2008-12-17]
32  * @changes    1.0.0b2   Added ::replicate() and ::registerReplicateCallback() for fActiveRecord::replicate() [wb, 2008-12-12]
33  * @changes    1.0.0b    The initial implementation [wb, 2007-08-04]
34  */
35 class fORM
36 {
37     // The following constants allow for nice looking callbacks to static methods
38     const callHookCallbacks          = 'fORM::callHookCallbacks';
39     const callInspectCallbacks       = 'fORM::callInspectCallbacks';
40     const callReflectCallbacks       = 'fORM::callReflectCallbacks';
41     const checkHookCallback          = 'fORM::checkHookCallback';
42     const classize                   = 'fORM::classize';
43     const defineActiveRecordClass    = 'fORM::defineActiveRecordClass';
44     const enableSchemaCaching        = 'fORM::enableSchemaCaching';
45     const getActiveRecordMethod      = 'fORM::getActiveRecordMethod';
46     const getClass                   = 'fORM::getClass';
47     const getColumnName              = 'fORM::getColumnName';
48     const getDatabaseName            = 'fORM::getDatabaseName';
49     const getRecordName              = 'fORM::getRecordName';
50     const getRecordSetMethod         = 'fORM::getRecordSetMethod';
51     const isClassMappedToTable       = 'fORM::isClassMappedToTable';
52     const mapClassToDatabase         = 'fORM::mapClassToDatabase';
53     const mapClassToTable            = 'fORM::mapClassToTable';
54     const objectify                  = 'fORM::objectify';
55     const overrideColumnName         = 'fORM::overrideColumnName';
56     const overrideRecordName         = 'fORM::overrideRecordName';
57     const parseMethod                = 'fORM::parseMethod';
58     const registerActiveRecordMethod = 'fORM::registerActiveRecordMethod';
59     const registerHookCallback       = 'fORM::registerHookCallback';
60     const registerInspectCallback    = 'fORM::registerInspectCallback';
61     const registerObjectifyCallback  = 'fORM::registerObjectifyCallback';
62     const registerRecordSetMethod    = 'fORM::registerRecordSetMethod';
63     const registerReflectCallback    = 'fORM::registerReflectCallback';
64     const registerReplicateCallback  = 'fORM::registerReplicateCallback';
65     const registerScalarizeCallback  = 'fORM::registerScalarizeCallback';
66     const replicate                  = 'fORM::replicate';
67     const reset                      = 'fORM::reset';
68     const scalarize                  = 'fORM::scalarize';
69     const tablize                    = 'fORM::tablize';
70    
71    
72     /**
73     * An array of `{method} => {callback}` mappings for fActiveRecord
74     *
75     * @var array
76     */
77     static private $active_record_method_callbacks = array();
78    
79     /**
80     * Cache for repetitive computation
81     *
82     * @var array
83     */
84     static private $cache = array(
85         'parseMethod'           => array(),
86         'getActiveRecordMethod' => array(),
87         'objectify'             => array()
88     );
89    
90     /**
91     * Custom mappings for class -> database
92     *
93     * @var array
94     */
95     static private $class_database_map = array(
96         'fActiveRecord' => 'default'
97     );
98    
99     /**
100     * Custom mappings for class <-> table
101     *
102     * @var array
103     */
104     static private $class_table_map = array();
105    
106     /**
107     * Custom column names for columns in fActiveRecord classes
108     *
109     * @var array
110     */
111     static private $column_names = array();
112    
113     /**
114     * Tracks callbacks registered for various fActiveRecord hooks
115     *
116     * @var array
117     */
118     static private $hook_callbacks = array();
119    
120     /**
121     * Callbacks for ::callInspectCallbacks()
122     *
123     * @var array
124     */
125     static private $inspect_callbacks = array();
126    
127     /**
128     * Callbacks for ::objectify()
129     *
130     * @var array
131     */
132     static private $objectify_callbacks = array();
133    
134     /**
135     * Custom record names for fActiveRecord classes
136     *
137     * @var array
138     */
139     static private $record_names = array(
140         'fActiveRecord' => 'Active Record'
141     );
142    
143     /**
144     * An array of `{method} => {callback}` mappings for fRecordSet
145     *
146     * @var array
147     */
148     static private $record_set_method_callbacks = array();
149    
150     /**
151     * Callbacks for ::callReflectCallbacks()
152     *
153     * @var array
154     */
155     static private $reflect_callbacks = array();
156    
157     /**
158     * Callbacks for ::replicate()
159     *
160     * @var array
161     */
162     static private $replicate_callbacks = array();
163    
164     /**
165     * Callbacks for ::scalarize()
166     *
167     * @var array
168     */
169     static private $scalarize_callbacks = array();
170    
171    
172     /**
173     * Calls the hook callbacks for the class and hook specified
174     *
175     * @internal
176      *
177     * @param  fActiveRecord $object            The instance of the class to call the hook for
178     * @param  string        $hook              The hook to call
179     * @param  array         &$values           The current values of the record
180     * @param  array         &$old_values       The old values of the record
181     * @param  array         &$related_records  Records related to the current record
182     * @param  array         &$cache            The cache array of the record
183     * @param  mixed         &$parameter        The parameter to send the callback
184     * @return void
185     */
186     static public function callHookCallbacks($object, $hook, &$values, &$old_values, &$related_records, &$cache, &$parameter=NULL)
187     {
188         $class = get_class($object);
189        
190         if (empty(self::$hook_callbacks[$class][$hook]) && empty(self::$hook_callbacks['*'][$hook])) {
191             return;
192         }
193        
194         // Get all of the callbacks for this hook, both for this class or all classes
195         $callbacks = array();
196        
197         if (isset(self::$hook_callbacks[$class][$hook])) {
198             $callbacks = array_merge($callbacks, self::$hook_callbacks[$class][$hook]);
199         }
200        
201         if (isset(self::$hook_callbacks['*'][$hook])) {
202             $callbacks = array_merge($callbacks, self::$hook_callbacks['*'][$hook]);
203         }
204        
205         foreach ($callbacks as $callback) {
206             call_user_func_array(
207                 $callback,
208                 // This is the only way to pass by reference
209                 array(
210                     $object,
211                     &$values,
212                     &$old_values,
213                     &$related_records,
214                     &$cache,
215                     &$parameter
216                 )
217             );
218         }
219     }
220    
221    
222     /**
223     * Calls all inspect callbacks for the class and column specified
224     *
225     * @internal
226      *
227     * @param  string $class      The class to inspect the column of
228     * @param  string $column     The column to inspect
229     * @param  array  &$metadata  The associative array of data about the column
230     * @return void
231     */
232     static public function callInspectCallbacks($class, $column, &$metadata)
233     {
234         if (!isset(self::$inspect_callbacks[$class][$column])) {
235             return;
236         }
237        
238         foreach (self::$inspect_callbacks[$class][$column] as $callback) {
239             // This is the only way to pass by reference
240             $parameters = array(
241                 $class,
242                 $column,
243                 &$metadata
244             );
245             call_user_func_array($callback, $parameters);
246         }
247     }
248    
249    
250     /**
251     * Calls all reflect callbacks for the class passed
252     *
253     * @internal
254      *
255     * @param  string  $class                 The class to call the callbacks for
256     * @param  array   &$signatures           The associative array of `{method_name} => {signature}`
257     * @param  boolean $include_doc_comments  If the doc comments should be included in the signature
258     * @return void
259     */
260     static public function callReflectCallbacks($class, &$signatures, $include_doc_comments)
261     {
262         if (!isset(self::$reflect_callbacks[$class]) && !isset(self::$reflect_callbacks['*'])) {
263             return;
264         }
265        
266         if (!empty(self::$reflect_callbacks['*'])) {
267             foreach (self::$reflect_callbacks['*'] as $callback) {
268                 // This is the only way to pass by reference
269                 $parameters = array(
270                     $class,
271                     &$signatures,
272                     $include_doc_comments
273                 );
274                 call_user_func_array($callback, $parameters);
275             }   
276         }
277        
278         if (!empty(self::$reflect_callbacks[$class])) {
279             foreach (self::$reflect_callbacks[$class] as $callback) {
280                 // This is the only way to pass by reference
281                 $parameters = array(
282                     $class,
283                     &$signatures,
284                     $include_doc_comments
285                 );
286                 call_user_func_array($callback, $parameters);
287             }
288         }
289     }
290    
291    
292     /**
293     * Checks to see if any (or a specific) callback has been registered for a specific hook
294     *
295     * @internal
296      *
297     * @param  string $class     The name of the class
298     * @param  string $hook      The hook to check
299     * @param  array  $callback  The specific callback to check for
300     * @return boolean  If the specified callback exists
301     */
302     static public function checkHookCallback($class, $hook, $callback=NULL)
303     {
304         if (empty(self::$hook_callbacks[$class][$hook]) && empty(self::$hook_callbacks['*'][$hook])) {
305             return FALSE;
306         }
307        
308         if (!$callback) {
309             return TRUE;
310         }
311        
312         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
313             $callback = explode('::', $callback);   
314         }
315        
316         if (!empty(self::$hook_callbacks[$class][$hook]) && in_array($callback, self::$hook_callbacks[$class][$hook])) {
317             return TRUE;   
318         }
319        
320         if (!empty(self::$hook_callbacks['*'][$hook]) && in_array($callback, self::$hook_callbacks['*'][$hook])) {
321             return TRUE;   
322         }
323        
324         return FALSE;
325     }
326    
327    
328     /**
329     * Takes a table and turns it into a class name - uses custom mapping if set
330     *
331     * @param  string $table  The table name
332     * @return string  The class name
333     */
334     static public function classize($table)
335     {
336         if (!$class = array_search($table, self::$class_table_map)) {
337             $class = fGrammar::camelize(fGrammar::singularize($table), TRUE);
338             self::$class_table_map[$class] = $table;
339         }
340        
341         return $class;
342     }
343    
344    
345     /**
346     * Will dynamically create an fActiveRecord-based class for a database table
347     *
348     * Normally this would be called from an `__autoload()` function.
349     *
350     * This method will only create classes for tables in the default ORM
351     * database.
352     *
353     * @param  string $class  The name of the class to create
354     * @return void
355     */
356     static public function defineActiveRecordClass($class)
357     {
358         if (class_exists($class, FALSE)) {
359             return;
360         }
361         $schema = fORMSchema::retrieve();
362         $tables = $schema->getTables();
363         $table  = self::tablize($class);
364         if (in_array($table, $tables)) {
365             eval('class ' . $class . ' extends fActiveRecord { };');
366             return;
367         }
368        
369         throw new fProgrammerException(
370             'The class specified, %s, does not correspond to a database table',
371             $class
372         );
373     }
374    
375    
376     /**
377     * Enables caching on the fDatabase, fSQLTranslation and fSchema objects used for the ORM
378     *
379     * This method will cache database schema information to the three objects
380     * that use it during normal ORM operation: fDatabase, fSQLTranslation and
381     * fSchema. To allow for schema changes without having to manually clear
382     * the cache, all cached information will be cleared if any
383     * fUnexpectedException objects are thrown.
384     *
385     * This method should be called right after fORMDatabase::attach().
386     *         
387     * @param  fCache $cache          The object to cache schema information to
388     * @param  string $database_name  The database to enable caching for
389     * @param  string $key_token      This is a token that is used in cache keys to prevent conflicts for server-wide caches - when non-NULL the document root is used
390     * @return void
391     */
392     static public function enableSchemaCaching($cache, $database_name='default', $key_token=NULL)
393     {
394         if ($key_token === NULL) {
395             $key_token = $_SERVER['DOCUMENT_ROOT'];   
396         }
397         $token = 'fORM::' . $database_name . '::' . $key_token . '::';
398        
399         $db = fORMDatabase::retrieve('name:' . $database_name);
400         $db->enableCaching($cache, $token);
401         fException::registerCallback($db->clearCache, 'fUnexpectedException');
402        
403         $sql_translation = $db->getSQLTranslation();
404         $sql_translation->enableCaching($cache, $token);
405        
406         $schema = fORMSchema::retrieve('name:' . $database_name);
407         $schema->enableCaching($cache, $token);
408         fException::registerCallback($schema->clearCache, 'fUnexpectedException');   
409     }
410    
411    
412     /**
413     * Returns a matching callback for the class and method specified
414     *
415     * The callback returned will be determined by the following logic:
416     *
417     *  1. If an exact callback has been defined for the method, it will be returned
418     *  2. If a callback in the form `{prefix}*` has been defined that matches the method, it will be returned
419     *  3. `NULL` will be returned
420     *
421     * @internal
422      *
423     * @param  string $class   The name of the class
424     * @param  string $method  The method to get the callback for
425     * @return string|null  The callback for the method or `NULL` if none exists - see method description for details
426     */
427     static public function getActiveRecordMethod($class, $method)
428     {
429         // This caches method lookups, providing a significant performance
430         // boost to pages with lots of method calls that get passed to
431         // fActiveRecord::__call()
432         if (isset(self::$cache['getActiveRecordMethod'][$class . '::' . $method])) {
433             return (!$method = self::$cache['getActiveRecordMethod'][$class . '::' . $method]) ? NULL : $method;     
434         }
435        
436         $callback = NULL;
437        
438         if (isset(self::$active_record_method_callbacks[$class][$method])) {
439             $callback = self::$active_record_method_callbacks[$class][$method];   
440        
441         } elseif (isset(self::$active_record_method_callbacks['*'][$method])) {
442             $callback = self::$active_record_method_callbacks['*'][$method];   
443        
444         } elseif (preg_match('#[A-Z0-9]#', $method)) {
445             list($action, $subject) = self::parseMethod($method);
446             if (isset(self::$active_record_method_callbacks[$class][$action . '*'])) {
447                 $callback = self::$active_record_method_callbacks[$class][$action . '*'];   
448             } elseif (isset(self::$active_record_method_callbacks['*'][$action . '*'])) {
449                 $callback = self::$active_record_method_callbacks['*'][$action . '*'];   
450             }   
451         }
452        
453         self::$cache['getActiveRecordMethod'][$class . '::' . $method] = ($callback === NULL) ? FALSE : $callback;
454         return $callback;
455     }
456    
457    
458     /**
459     * Takes a class name or class and returns the class name
460     *
461     * @internal
462      *
463     * @param  mixed $class  The object to get the name of, or possibly a string already containing the class
464     * @return string  The class name
465     */
466     static public function getClass($class)
467     {
468         if (is_object($class)) { return get_class($class); }
469         return $class;
470     }
471    
472    
473     /**
474     * Returns the column name
475     *
476     * The default column name is the result of calling fGrammar::humanize()
477     * on the column.
478     *
479     * @internal
480      *
481     * @param  string $class   The class name the column is part of
482     * @param  string $column  The database column
483     * @return string  The column name for the column specified
484     */
485     static public function getColumnName($class, $column)
486     {
487         if (!isset(self::$column_names[$class])) {
488             self::$column_names[$class] = array();
489         }
490        
491         if (!isset(self::$column_names[$class][$column])) {
492             self::$column_names[$class][$column] = fGrammar::humanize($column);
493         }
494        
495         return self::$column_names[$class][$column];
496     }
497    
498    
499     /**
500     * Returns the name for the database used by the class specified
501     *
502     * @internal
503      *
504     * @param  string $class   The class name to get the database name for
505     * @return string  The name of the database to use
506     */
507     static public function getDatabaseName($class)
508     {
509         if (!isset(self::$class_database_map[$class])) {
510             $class = 'fActiveRecord';   
511         }
512        
513         return self::$class_database_map[$class];
514     }
515    
516    
517     /**
518     * Returns the record name for a class
519     *
520     * The default record name is the result of calling fGrammar::humanize()
521     * on the class.
522     *
523     * @internal
524      *
525     * @param  string $class  The class name to get the record name of
526     * @return string  The record name for the class specified
527     */
528     static public function getRecordName($class)
529     {
530         if (!isset(self::$record_names[$class])) {
531             self::$record_names[$class] = fGrammar::humanize($class);
532         }
533        
534         return self::$record_names[$class];
535     }
536    
537    
538     /**
539     * Returns a matching callback for the method specified
540     *
541     * The callback returned will be determined by the following logic:
542     *
543     *  1. If an exact callback has been defined for the method, it will be returned
544     *  2. If a callback in the form `{action}*` has been defined that matches the method, it will be returned
545     *  3. `NULL` will be returned
546     *
547     * @internal
548      *
549     * @param  string $method  The method to get the callback for
550     * @return string|null  The callback for the method or `NULL` if none exists - see method description for details
551     */
552     static public function getRecordSetMethod($method)
553     {
554         if (isset(self::$record_set_method_callbacks[$method])) {
555             return self::$record_set_method_callbacks[$method];   
556         }
557        
558         if (preg_match('#[A-Z0-9]#', $method)) {
559             list($action, $subject) = self::parseMethod($method);
560             if (isset(self::$record_set_method_callbacks[$action . '*'])) {
561                 return self::$record_set_method_callbacks[$action . '*'];   
562             }   
563         }
564        
565         return NULL;   
566     }
567    
568    
569     /**
570     * Checks if a class has been mapped to a table
571     *
572     * @internal
573      *
574     * @param  mixed  $class  The name of the class
575     * @return boolean  If the class has been mapped to a table
576     */
577     static public function isClassMappedToTable($class)
578     {
579         $class = self::getClass($class);
580        
581         return isset(self::$class_table_map[$class]);
582     }
583    
584    
585     /**
586     * Sets a class to use a database other than the "default"
587     *
588     * Multiple database objects can be attached for the ORM by passing a
589     * unique `$name` to the ::attach() method.
590     *
591     * @param  mixed  $class          The name of the class, or an instance of it
592     * @param  string $database_name  The name given to the database when passed to ::attach()
593     * @return void
594     */
595     static public function mapClassToDatabase($class, $database_name)
596     {
597         $class = fORM::getClass($class);
598        
599         self::$class_database_map[$class] = $database_name;
600     }
601    
602    
603     /**
604     * Allows non-standard class to table mapping
605     *
606     * By default, all database tables are assumed to be plural nouns in
607     * `underscore_notation` and all class names are assumed to be singular
608     * nouns in `UpperCamelCase`. This method allows arbitrary class to
609     * table mapping.
610     *
611     * @param  mixed  $class  The name of the class, or an instance of it
612     * @param  string $table  The name of the database table
613     * @return void
614     */
615     static public function mapClassToTable($class, $table)
616     {
617         $class = self::getClass($class);
618        
619         self::$class_table_map[$class] = $table;
620     }
621    
622    
623     /**
624     * Takes a scalar value and turns it into an object if applicable
625     *
626     * @internal
627      *
628     * @param  string $class   The class name of the class the column is part of
629     * @param  string $column  The database column
630     * @param  mixed  $value   The value to possibly objectify
631     * @return mixed  The scalar or object version of the value, depending on the column type and column options
632     */
633     static public function objectify($class, $column, $value)
634     {
635         // This short-circuits computation for already checked columns, providing
636         // a nice little performance boost to pages with lots of records
637         if (isset(self::$cache['objectify'][$class . '::' . $column])) {
638             return $value;   
639         }
640        
641         if (!empty(self::$objectify_callbacks[$class][$column])) {
642             return call_user_func(self::$objectify_callbacks[$class][$column], $class, $column, $value);
643         }
644        
645         $table  = self::tablize($class);
646         $schema = fORMSchema::retrieve($class);
647        
648         // Turn date/time values into objects
649         $column_type = $schema->getColumnInfo($table, $column, 'type');
650        
651         if (in_array($column_type, array('date', 'time', 'timestamp'))) {
652            
653             if ($value === NULL) {
654                 return $value;   
655             }
656            
657             try {
658                
659                 // Explicit calls to the constructors are used for dependency detection
660                 switch ($column_type) {
661                     case 'date':      $value = new fDate($value);      break;
662                     case 'time':      $value = new fTime($value);      break;
663                     case 'timestamp': $value = new fTimestamp($value); break;
664                 }
665                
666             } catch (fValidationException $e) {
667                 // Validation exception results in the raw value being saved
668             }
669        
670         } else {
671             self::$cache['objectify'][$class . '::' . $column] = TRUE;   
672         }
673        
674         return $value;
675     }
676    
677    
678     /**
679     * Allows overriding of default column names
680     *
681     * By default a column name is the result of fGrammar::humanize() called
682     * on the column.
683     *
684     * @param  mixed  $class        The class name or instance of the class the column is located in
685     * @param  string $column       The database column
686     * @param  string $column_name  The name for the column
687     * @return void
688     */
689     static public function overrideColumnName($class, $column, $column_name)
690     {
691         $class = self::getClass($class);
692        
693         if (!isset(self::$column_names[$class])) {
694             self::$column_names[$class] = array();
695         }
696        
697         self::$column_names[$class][$column] = $column_name;
698     }
699    
700    
701     /**
702     * Allows overriding of default record names
703     *
704     * By default a record name is the result of fGrammar::humanize() called
705     * on the class.
706     *
707     * @param  mixed  $class        The class name or instance of the class to override the name of
708     * @param  string $record_name  The human version of the record
709     * @return void
710     */
711     static public function overrideRecordName($class, $record_name)
712     {
713         $class = self::getClass($class);
714         self::$record_names[$class] = $record_name;
715     }
716    
717    
718     /**
719     * Parses a `camelCase` method name for an action and subject in the form `actionSubject()`
720     *
721     * @internal
722      *
723     * @param  string $method  The method name to parse
724     * @return array  An array of `0 => {action}, 1 => {subject}`
725     */
726     static public function parseMethod($method)
727     {
728         if (isset(self::$cache['parseMethod'][$method])) {
729             return self::$cache['parseMethod'][$method];   
730         }
731        
732         if (!preg_match('#^([a-z]+)(.*)$#D', $method, $matches)) {
733             throw new fProgrammerException(
734                 'Invalid method, %s(), called',
735                 $method
736             );   
737         }
738         self::$cache['parseMethod'][$method] = array($matches[1], fGrammar::underscorize($matches[2]));
739         return self::$cache['parseMethod'][$method];
740     }
741    
742    
743     /**
744     * Registers a callback for an fActiveRecord method that falls through to fActiveRecord::__call() or hits a predefined method hook
745    
746     * The callback should accept the following parameters:
747     *
748     *  - **`$object`**:           The fActiveRecord instance
749     *  - **`&$values`**:          The values array for the record
750     *  - **`&$old_values`**:      The old values array for the record
751     *  - **`&$related_records`**: The related records array for the record
752     *  - **`&$cache`**:           The cache array for the record
753     *  - **`$method_name`**:      The method that was called
754     *  - **`&$parameters`**:      The parameters passed to the method
755     *
756     * @param  mixed    $class     The class name or instance of the class to register for, `'*'` will register for all classes
757     * @param  string   $method    The method to hook for - this can be a complete method name or `{prefix}*` where `*` will match any column name
758     * @param  callback $callback  The callback to execute - see method description for parameter list
759     * @return void
760     */
761     static public function registerActiveRecordMethod($class, $method, $callback)
762     {
763         $class = self::getClass($class);
764        
765         if (!isset(self::$active_record_method_callbacks[$class])) {
766             self::$active_record_method_callbacks[$class] = array();   
767         }
768        
769         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
770             $callback = explode('::', $callback);   
771         }
772        
773         self::$active_record_method_callbacks[$class][$method] = $callback;
774        
775         self::$cache['getActiveRecordMethod'] = array();
776     }
777    
778    
779     /**
780     * Registers a callback for one of the various fActiveRecord hooks - multiple callbacks can be registered for each hook
781     *
782     * The method signature should include the follow parameters:
783     *
784     *  - **`$object`**:           The fActiveRecord instance
785     *  - **`&$values`**:          The values array for the record
786     *  - **`&$old_values`**:      The old values array for the record
787     *  - **`&$related_records`**: The related records array for the record
788     *  - **`&$cache`**:           The cache array for the record
789     *
790     * The `'pre::validate()'` and `'post::validate()'` hooks have an extra parameter:
791     *
792     *  - **`&$validation_messages`**: An ordered array of validation errors that will be returned or tossed as an fValidationException
793    
794     * Below is a list of all valid hooks:
795     *
796     *  - `'post::__construct()'`
797     *  - `'pre::delete()'`
798     *  - `'post-begin::delete()'`
799     *  - `'pre-commit::delete()'`
800     *  - `'post-commit::delete()'`
801     *  - `'post-rollback::delete()'`
802     *  - `'post::delete()'`
803     *  - `'post::loadFromIdentityMap()'`
804     *  - `'post::loadFromResult()'`
805     *  - `'pre::populate()'`
806     *  - `'post::populate()'`
807     *  - `'pre::store()'`
808     *  - `'post-begin::store()'`
809     *  - `'post-validate::store()'`
810     *  - `'pre-commit::store()'`
811     *  - `'post-commit::store()'`
812     *  - `'post-rollback::store()'`
813     *  - `'post::store()'`
814     *  - `'pre::validate()'`
815     *  - `'post::validate()'`
816     *
817     * @param  mixed    $class     The class name or instance of the class to hook, `'*'` will hook all classes
818     * @param  string   $hook      The hook to register for
819     * @param  callback $callback  The callback to register - see the method description for details about the method signature
820     * @return void
821     */
822     static public function registerHookCallback($class, $hook, $callback)
823     {
824         $class = self::getClass($class);
825        
826         static $valid_hooks = array(
827             'post::__construct()',
828             'pre::delete()',
829             'post-begin::delete()',
830             'pre-commit::delete()',
831             'post-commit::delete()',
832             'post-rollback::delete()',
833             'post::delete()',
834             'post::loadFromIdentityMap()',
835             'post::loadFromResult()',
836             'pre::populate()',
837             'post::populate()',
838             'pre::store()',
839             'post-begin::store()',
840             'post-validate::store()',
841             'pre-commit::store()',
842             'post-commit::store()',
843             'post-rollback::store()',
844             'post::store()',
845             'pre::validate()',
846             'post::validate()'
847         );
848        
849         if (!in_array($hook, $valid_hooks)) {
850             throw new fProgrammerException(
851                 'The hook specified, %1$s, should be one of: %2$s.',
852                 $hook,
853                 join(', ', $valid_hooks)
854             );
855         }
856        
857         if (!isset(self::$hook_callbacks[$class])) {
858             self::$hook_callbacks[$class] = array();
859         }
860        
861         if (!isset(self::$hook_callbacks[$class][$hook])) {
862             self::$hook_callbacks[$class][$hook] = array();
863         }
864        
865         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
866             $callback = explode('::', $callback);   
867         }
868        
869         self::$hook_callbacks[$class][$hook][] = $callback;
870     }
871    
872    
873     /**
874     * Registers a callback to modify the results of fActiveRecord::inspect() methods
875     *
876     * @param  mixed    $class     The class name or instance of the class to register for
877     * @param  string   $column    The column to register for
878     * @param  callback $callback  The callback to register. Callback should accept a single parameter by reference, an associative array of the various metadata about a column.
879     * @return void
880     */
881     static public function registerInspectCallback($class, $column, $callback)
882     {
883         $class = self::getClass($class);
884        
885         if (!isset(self::$inspect_callbacks[$class])) {
886             self::$inspect_callbacks[$class] = array();
887         }
888         if (!isset(self::$inspect_callbacks[$class][$column])) {
889             self::$inspect_callbacks[$class][$column] = array();
890         }
891        
892         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
893             $callback = explode('::', $callback);   
894         }
895        
896         self::$inspect_callbacks[$class][$column][] = $callback;
897     }
898    
899    
900     /**
901     * Registers a callback for when ::objectify() is called on a specific column
902     *
903     * @param  mixed    $class     The class name or instance of the class to register for
904     * @param  string   $column    The column to register for
905     * @param  callback $callback  The callback to register. Callback should accept a single parameter, the value to objectify and should return the objectified value.
906     * @return void
907     */
908     static public function registerObjectifyCallback($class, $column, $callback)
909     {
910         $class = self::getClass($class);
911        
912         if (!isset(self::$objectify_callbacks[$class])) {
913             self::$objectify_callbacks[$class] = array();
914         }
915        
916         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
917             $callback = explode('::', $callback);   
918         }
919        
920         self::$objectify_callbacks[$class][$column] = $callback;
921        
922         self::$cache['objectify'] = array();
923     }
924    
925    
926     /**
927     * Registers a callback for an fRecordSet method that fall through to fRecordSet::__call()
928    
929     * The callback should accept the following parameters:
930     *
931     *  - **`$object`**:      The actual record set
932     *  - **`$class`**:       The class of each record
933     *  - **`&$records`**:    The ordered array of fActiveRecord objects
934     *  - **`&$pointer`**:    The current array pointer for the records array
935     *  - **`$parameters`**:  Any parameters passed to the method
936     *
937     * @param  string   $method    The method to hook for
938     * @param  callback $callback  The callback to execute - see method description for parameter list
939     * @return void
940     */
941     static public function registerRecordSetMethod($method, $callback)
942     {
943         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
944             $callback = explode('::', $callback);   
945         }
946         self::$record_set_method_callbacks[$method] = $callback;
947     }
948    
949    
950     /**
951     * Registers a callback to modify the results of fActiveRecord::reflect()
952     *
953     * Callbacks registered here can override default method signatures and add
954     * method signatures, however any methods that are defined in the actual class
955     * will override these signatures.
956     *
957     * The callback should accept three parameters:
958     *
959     *  - **`$class`**: the class name
960     *  - **`&$signatures`**: an associative array of `{method_name} => {signature}`
961     *  - **`$include_doc_comments`**: a boolean indicating if the signature should include the doc comment for the method, or just the signature
962     *
963     * @param  mixed    $class     The class name or instance of the class to register for, `'*'` will register for all classes
964     * @param  callback $callback  The callback to register. Callback should accept a three parameters - see method description for details.
965     * @return void
966     */
967     static public function registerReflectCallback($class, $callback)
968     {
969         $class = self::getClass($class);
970        
971         if (!isset(self::$reflect_callbacks[$class])) {
972             self::$reflect_callbacks[$class] = array();
973         } elseif (in_array($callback, self::$reflect_callbacks[$class])) {
974             return;
975         }
976        
977         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
978             $callback = explode('::', $callback);   
979         }
980        
981         self::$reflect_callbacks[$class][] = $callback;
982     }
983    
984    
985     /**
986     * Registers a callback for when a value is replicated for a specific column
987     *
988     * @param  mixed    $class     The class name or instance of the class to register for
989     * @param  string   $column    The column to register for
990     * @param  callback $callback  The callback to register. Callback should accept a single parameter, the value to replicate and should return the replicated value.
991     * @return void
992     */
993     static public function registerReplicateCallback($class, $column, $callback)
994     {
995         $class = self::getClass($class);
996        
997         if (!isset(self::$replicate_callbacks[$class])) {
998             self::$replicate_callbacks[$class] = array();
999         }
1000        
1001         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
1002             $callback = explode('::', $callback);   
1003         }
1004        
1005         self::$replicate_callbacks[$class][$column] = $callback;
1006     }
1007    
1008    
1009     /**
1010     * Registers a callback for when ::scalarize() is called on a specific column
1011     *
1012     * @param  mixed    $class     The class name or instance of the class to register for
1013     * @param  string   $column    The column to register for
1014     * @param  callback $callback  The callback to register. Callback should accept a single parameter, the value to scalarize and should return the scalarized value.
1015     * @return void
1016     */
1017     static public function registerScalarizeCallback($class, $column, $callback)
1018     {
1019         $class = self::getClass($class);
1020        
1021         if (!isset(self::$scalarize_callbacks[$class])) {
1022             self::$scalarize_callbacks[$class] = array();
1023         }
1024        
1025         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
1026             $callback = explode('::', $callback);   
1027         }
1028        
1029         self::$scalarize_callbacks[$class][$column] = $callback;
1030     }
1031    
1032    
1033     /**
1034     * Takes and value and returns a copy is scalar or a clone if an object
1035     *
1036     * The ::registerReplicateCallback() allows for custom replication code
1037     *
1038     * @internal
1039      *
1040     * @param  string $class   The class the column is part of
1041     * @param  string $column  The database column
1042     * @param  mixed  $value   The value to copy/clone
1043     * @return mixed  The copied/cloned value
1044     */
1045     static public function replicate($class, $column, $value)
1046     {
1047         if (!empty(self::$replicate_callbacks[$class][$column])) {
1048             return call_user_func(self::$replicate_callbacks[$class][$column], $class, $column, $value);
1049         }
1050        
1051         if (!is_object($value)) {
1052             return $value;   
1053         }
1054        
1055         return clone $value;
1056     }
1057    
1058    
1059     /**
1060     * Resets the configuration of the class
1061     *
1062     * @internal
1063      *
1064     * @return void
1065     */
1066     static public function reset()
1067     {
1068         self::$active_record_method_callbacks = array();
1069         self::$cache                          = array(
1070             'parseMethod'           => array(),
1071             'getActiveRecordMethod' => array(),
1072             'objectify'             => array()
1073         );
1074         self::$class_database_map             = array(
1075             'fActiveRecord' => 'default'
1076         );
1077         self::$class_table_map                = array();
1078         self::$column_names                   = array();
1079         self::$hook_callbacks                 = array();
1080         self::$inspect_callbacks              = array();
1081         self::$objectify_callbacks            = array();
1082         self::$record_names                   = array(
1083             'fActiveRecord' => 'Active Record'
1084         );
1085         self::$record_set_method_callbacks    = array();
1086         self::$reflect_callbacks              = array();
1087         self::$replicate_callbacks            = array();
1088         self::$scalarize_callbacks            = array();
1089     }
1090    
1091    
1092     /**
1093     * If the value passed is an object, calls `__toString()` on it
1094     *
1095     * @internal
1096      *
1097     * @param  mixed  $class   The class name or instance of the class the column is part of
1098     * @param  string $column  The database column
1099     * @param  mixed  $value   The value to get the scalar value of
1100     * @return mixed  The scalar value of the value
1101     */
1102     static public function scalarize($class, $column, $value)
1103     {
1104         $class = self::getClass($class);
1105        
1106         if (!empty(self::$scalarize_callbacks[$class][$column])) {
1107             return call_user_func(self::$scalarize_callbacks[$class][$column], $class, $column, $value);
1108         }
1109        
1110         if (is_object($value) && is_callable(array($value, '__toString'))) {
1111             return $value->__toString();
1112         } elseif (is_object($value)) {
1113             return (string) $value;
1114         }
1115        
1116         return $value;
1117     }
1118    
1119    
1120     /**
1121     * Takes a class name (or class) and turns it into a table name - Uses custom mapping if set
1122     *
1123     * @param  string $class  The class name
1124     * @return string  The table name
1125     */
1126     static public function tablize($class)
1127     {
1128         if (!isset(self::$class_table_map[$class])) {
1129             self::$class_table_map[$class] = fGrammar::underscorize(fGrammar::pluralize($class));
1130         }
1131         return self::$class_table_map[$class];
1132     }
1133    
1134    
1135     /**
1136     * Forces use as a static class
1137     *
1138     * @return fORM
1139     */
1140     private function __construct() { }
1141 }
1142  
1143  
1144  
1145 /**
1146  * Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>
1147  *
1148  * Permission is hereby granted, free of charge, to any person obtaining a copy
1149  * of this software and associated documentation files (the "Software"), to deal
1150  * in the Software without restriction, including without limitation the rights
1151  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1152  * copies of the Software, and to permit persons to whom the Software is
1153  * furnished to do so, subject to the following conditions:
1154  *
1155  * The above copyright notice and this permission notice shall be included in
1156  * all copies or substantial portions of the Software.
1157  *
1158  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1159  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1160  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1161  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1162  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1163  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1164  * THE SOFTWARE.
1165  */