root/fORM.php

Revision 603, 31.6 kB (checked in by wbond, 1 year ago)

Performance improvements, especially related to the ORM

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