root/fORMMoney.php

Revision 849, 20.7 kB (checked in by wbond, 2 months ago)

Added the $remove_zero_fraction parameter to fMoney::format() and fORMMoney::prepareMoneyColumn()

LineHide Line Numbers
1 <?php
2 /**
3  * Provides money functionality for fActiveRecord classes
4  *
5  * @copyright  Copyright (c) 2008-2010 Will Bond, others
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @author     Dan Collins, iMarc LLC [dc-imarc] <dan@imarc.net>
8  * @license    http://flourishlib.com/license
9  *
10  * @package    Flourish
11  * @link       http://flourishlib.com/fORMMoney
12  *
13  * @version    1.0.0b9
14  * @changes    1.0.0b9  Added the `$remove_zero_fraction` parameter to prepare methods [wb, 2010-06-09]
15  * @changes    1.0.0b8  Changed validation messages array to use column name keys [wb, 2010-05-26]
16  * @changes    1.0.0b7  Fixed the `set` methods to return the record object in order to be consistent with all other `set` methods [wb, 2010-03-15]
17  * @changes    1.0.0b6  Fixed duplicate validation messages and fProgrammerException object being thrown when NULL is set [dc-imarc+wb, 2010-03-03]
18  * @changes    1.0.0b5  Updated code for the new fORMDatabase and fORMSchema APIs [wb, 2009-10-28]
19  * @changes    1.0.0b4  Updated to use new fORM::registerInspectCallback() method [wb, 2009-07-13]
20  * @changes    1.0.0b3  Updated code to use new fValidationException::formatField() method [wb, 2009-06-04] 
21  * @changes    1.0.0b2  Fixed bugs with objectifying money columns [wb, 2008-11-24]
22  * @changes    1.0.0b   The initial implementation [wb, 2008-09-05]
23  */
24 class fORMMoney
25 {
26     // The following constants allow for nice looking callbacks to static methods
27     const configureMoneyColumn       = 'fORMMoney::configureMoneyColumn';
28     const encodeMoneyColumn          = 'fORMMoney::encodeMoneyColumn';
29     const inspect                    = 'fORMMoney::inspect';
30     const makeMoneyObjects           = 'fORMMoney::makeMoneyObjects';
31     const objectifyMoney             = 'fORMMoney::objectifyMoney';
32     const objectifyMoneyWithCurrency = 'fORMMoney::objectifyMoneyWithCurrency';
33     const prepareMoneyColumn         = 'fORMMoney::prepareMoneyColumn';
34     const reflect                    = 'fORMMoney::reflect';
35     const reset                      = 'fORMMoney::reset';
36     const setCurrencyColumn          = 'fORMMoney::setCurrencyColumn';
37     const setMoneyColumn             = 'fORMMoney::setMoneyColumn';
38     const validateMoneyColumns       = 'fORMMoney::validateMoneyColumns';
39    
40    
41     /**
42     * Columns that store currency information for a money column
43     *
44     * @var array
45     */
46     static private $currency_columns = array();
47    
48     /**
49     * Columns that should be formatted as money
50     *
51     * @var array
52     */
53     static private $money_columns = array();
54    
55    
56     /**
57     * Composes text using fText if loaded
58     *
59     * @param  string  $message    The message to compose
60     * @param  mixed   $component  A string or number to insert into the message
61     * @param  mixed   ...
62     * @return string  The composed and possible translated message
63     */
64     static private function compose($message)
65     {
66         $args = array_slice(func_get_args(), 1);
67        
68         if (class_exists('fText', FALSE)) {
69             return call_user_func_array(
70                 array('fText', 'compose'),
71                 array($message, $args)
72             );
73         } else {
74             return vsprintf($message, $args);
75         }
76     }
77    
78    
79     /**
80     * Sets a column to be formatted as an fMoney object
81     *
82     * @param  mixed  $class            The class name or instance of the class to set the column format
83     * @param  string $column           The column to format as an fMoney object
84     * @param  string $currency_column  If specified, this column will store the currency of the fMoney object
85     * @return void
86     */
87     static public function configureMoneyColumn($class, $column, $currency_column=NULL)
88     {
89         $class     = fORM::getClass($class);
90         $table     = fORM::tablize($class);
91         $schema    = fORMSchema::retrieve($class);
92         $data_type = $schema->getColumnInfo($table, $column, 'type');
93        
94         $valid_data_types = array('float');
95         if (!in_array($data_type, $valid_data_types)) {
96             throw new fProgrammerException(
97                 'The column specified, %1$s, is a %2$s column. Must be %3$s to be set as a money column.',
98                 $column,
99                 $data_type,
100                 join(', ', $valid_data_types)
101             );
102         }
103        
104         if ($currency_column !== NULL) {
105             $currency_column_data_type = $schema->getColumnInfo($table, $currency_column, 'type');
106             $valid_currency_column_data_types = array('varchar', 'char', 'text');
107             if (!in_array($currency_column_data_type, $valid_currency_column_data_types)) {
108                 throw new fProgrammerException(
109                     'The currency column specified, %1$s, is a %2$s column. Must be %3$s to be set as a currency column.',
110                     $currency_column,
111                     $currency_column_data_type,
112                     join(', ', $valid_currency_column_data_types)
113                 );
114             }
115         }
116        
117         $camelized_column = fGrammar::camelize($column, TRUE);
118        
119         fORM::registerActiveRecordMethod(
120             $class,
121             'encode' . $camelized_column,
122             self::encodeMoneyColumn
123         );
124        
125         fORM::registerActiveRecordMethod(
126             $class,
127             'prepare' . $camelized_column,
128             self::prepareMoneyColumn
129         );
130        
131         if (!fORM::checkHookCallback($class, 'post::validate()', self::validateMoneyColumns)) {
132             fORM::registerHookCallback($class, 'post::validate()', self::validateMoneyColumns);
133         }
134        
135         fORM::registerReflectCallback($class, self::reflect);
136         fORM::registerInspectCallback($class, $column, self::inspect);
137        
138         $value = FALSE;
139        
140         if ($currency_column) {
141             $value = $currency_column;   
142            
143             if (empty(self::$currency_columns[$class])) {
144                 self::$currency_columns[$class] = array();
145             }
146             self::$currency_columns[$class][$currency_column] = $column;
147            
148             if (!fORM::checkHookCallback($class, 'post::loadFromResult()', self::makeMoneyObjects)) {
149                 fORM::registerHookCallback($class, 'post::loadFromResult()', self::makeMoneyObjects);
150             }
151            
152             if (!fORM::checkHookCallback($class, 'pre::validate()', self::makeMoneyObjects)) {
153                 fORM::registerHookCallback($class, 'pre::validate()', self::makeMoneyObjects);
154             }
155            
156             fORM::registerActiveRecordMethod(
157                 $class,
158                 'set' . $camelized_column,
159                 self::setMoneyColumn
160             );
161            
162             fORM::registerActiveRecordMethod(
163                 $class,
164                 'set' . fGrammar::camelize($currency_column, TRUE),
165                 self::setCurrencyColumn
166             );
167        
168         } else {
169             fORM::registerObjectifyCallback($class, $column, self::objectifyMoney);
170         }
171        
172         if (empty(self::$money_columns[$class])) {
173             self::$money_columns[$class] = array();
174         }
175        
176         self::$money_columns[$class][$column] = $value;
177     }
178    
179    
180     /**
181     * Encodes a money column by calling fMoney::__toString()
182     *
183     * @internal
184      *
185     * @param  fActiveRecord $object            The fActiveRecord instance
186     * @param  array         &$values           The current values
187     * @param  array         &$old_values       The old values
188     * @param  array         &$related_records  Any records related to this record
189     * @param  array         &$cache            The cache array for the record
190     * @param  string        $method_name       The method that was called
191     * @param  array         $parameters        The parameters passed to the method
192     * @return string  The encoded monetary value
193     */
194     static public function encodeMoneyColumn($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
195     {
196         list ($action, $column) = fORM::parseMethod($method_name);
197        
198         $value = $values[$column];
199        
200         if ($value instanceof fMoney) {
201             $value = $value->__toString();
202         }
203        
204         return fHTML::prepare($value);
205     }
206    
207    
208     /**
209     * Adds metadata about features added by this class
210     *
211     * @internal
212      *
213     * @param  string $class      The class being inspected
214     * @param  string $column     The column being inspected
215     * @param  array  &$metadata  The array of metadata about a column
216     * @return void
217     */
218     static public function inspect($class, $column, &$metadata)
219     {
220         unset($metadata['auto_increment']);
221         $metadata['feature'] = 'money';
222     }
223    
224    
225     /**
226     * Makes fMoney objects for all money columns in the object that also have a currency column
227     *
228     * @internal
229      *
230     * @param  fActiveRecord $object            The fActiveRecord instance
231     * @param  array         &$values           The current values
232     * @param  array         &$old_values       The old values
233     * @param  array         &$related_records  Any records related to this record
234     * @param  array         &$cache            The cache array for the record
235     * @return void
236     */
237     static public function makeMoneyObjects($object, &$values, &$old_values, &$related_records, &$cache)
238     {
239         $class = get_class($object);
240        
241         if (!isset(self::$currency_columns[$class])) {
242             return;   
243         }
244        
245         foreach(self::$currency_columns[$class] as $currency_column => $value_column) {
246             self::objectifyMoneyWithCurrency($values, $old_values, $value_column, $currency_column);
247         }   
248     }
249    
250    
251     /**
252     * Turns a monetary value into an fMoney object
253     *
254     * @internal
255      *
256     * @param  string $class   The class this value is for
257     * @param  string $column  The column the value is in
258     * @param  mixed  $value   The value
259     * @return mixed  The fMoney object or raw value
260     */
261     static public function objectifyMoney($class, $column, $value)
262     {
263         if ((!is_string($value) && !is_numeric($value) && !is_object($value)) || !strlen(trim($value))) {
264             return $value;
265         }
266        
267         try {
268             return new fMoney($value);
269              
270         // If there was some error creating the money object, just return the raw value
271         } catch (fExpectedException $e) {
272             return $value;
273         }
274     }
275    
276    
277     /**
278     * Turns a monetary value into an fMoney object with a currency specified by another column
279     *
280     * @internal
281      *
282     * @param  array  &$values          The current values
283     * @param  array  &$old_values      The old values
284     * @param  string $value_column     The column holding the value
285     * @param  string $currency_column  The column holding the currency code
286     * @return void
287     */
288     static public function objectifyMoneyWithCurrency(&$values, &$old_values, $value_column, $currency_column)
289     {
290         if ((!is_string($values[$value_column]) && !is_numeric($values[$value_column]) && !is_object($values[$value_column])) || !strlen(trim($values[$value_column]))) {
291             return;
292         }
293            
294         try {
295             $value = $values[$value_column];
296             if ($value instanceof fMoney) {
297                 $value = $value->__toString();   
298             }
299            
300             $currency = $values[$currency_column];
301             if (!$currency && $currency !== '0' && $currency !== 0) {
302                 $currency = NULL;   
303             }
304            
305             $value = new fMoney($value, $currency);
306              
307             if (fActiveRecord::hasOld($old_values, $currency_column) && !fActiveRecord::hasOld($old_values, $value_column)) {
308                 fActiveRecord::assign($values, $old_values, $value_column, $value);       
309             } else {
310                 $values[$value_column] = $value;
311             }
312            
313             if ($values[$currency_column] === NULL) {
314                 fActiveRecord::assign($values, $old_values, $currency_column, $value->getCurrency());
315             }
316              
317         // If there was some error creating the money object, we just leave all values alone
318         } catch (fExpectedException $e) { }   
319     }
320    
321    
322     /**
323     * Prepares a money column by calling fMoney::format()
324     *
325     * @internal
326      *
327     * @param  fActiveRecord $object            The fActiveRecord instance
328     * @param  array         &$values           The current values
329     * @param  array         &$old_values       The old values
330     * @param  array         &$related_records  Any records related to this record
331     * @param  array         &$cache            The cache array for the record
332     * @param  string        $method_name       The method that was called
333     * @param  array         $parameters        The parameters passed to the method
334     * @return string  The formatted monetary value
335     */
336     static public function prepareMoneyColumn($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
337     {
338         list ($action, $column) = fORM::parseMethod($method_name);
339        
340         if (empty($values[$column])) {
341             return $values[$column];
342         }
343         $value = $values[$column];
344        
345         $remove_zero_fraction = FALSE;
346         if (count($parameters)) {
347             $remove_zero_fraction = $parameters[0];
348         }
349        
350         if ($value instanceof fMoney) {
351             $value = $value->format($remove_zero_fraction);
352         }
353        
354         return fHTML::prepare($value);
355     }
356    
357    
358     /**
359     * Adjusts the fActiveRecord::reflect() signatures of columns that have been configured in this class
360     *
361     * @internal
362      *
363     * @param  string  $class                 The class to reflect
364     * @param  array   &$signatures           The associative array of `{method name} => {signature}`
365     * @param  boolean $include_doc_comments  If doc comments should be included with the signature
366     * @return void
367     */
368     static public function reflect($class, &$signatures, $include_doc_comments)
369     {
370         if (!isset(self::$money_columns[$class])) {
371             return;   
372         }
373        
374         foreach(self::$money_columns[$class] as $column => $enabled) {
375             $camelized_column = fGrammar::camelize($column, TRUE);
376            
377             // Get and set methods
378             $signature = '';
379             if ($include_doc_comments) {
380                 $signature .= "/**\n";
381                 $signature .= " * Gets the current value of " . $column . "\n";
382                 $signature .= " * \n";
383                 $signature .= " * @return fMoney  The current value\n";
384                 $signature .= " */\n";
385             }
386             $get_method = 'get' . $camelized_column;
387             $signature .= 'public function ' . $get_method . '()';
388            
389             $signatures[$get_method] = $signature;
390            
391            
392             $signature = '';
393             if ($include_doc_comments) {
394                 $signature .= "/**\n";
395                 $signature .= " * Sets the value for " . $column . "\n";
396                 $signature .= " * \n";
397                 $signature .= " * @param  fMoney|string|integer \$" . $column . "  The new value - a string or integer will be converted to the default currency (if defined)\n";
398                 $signature .= " * @return fActiveRecord  The record object, to allow for method chaining\n";
399                 $signature .= " */\n";
400             }
401             $set_method = 'set' . $camelized_column;
402             $signature .= 'public function ' . $set_method . '($' . $column . ')';
403            
404             $signatures[$set_method] = $signature;
405            
406             $signature = '';
407             if ($include_doc_comments) {
408                 $signature .= "/**\n";
409                 $signature .= " * Encodes the value of " . $column . " for output into an HTML form\n";
410                 $signature .= " * \n";
411                 $signature .= " * If the value is an fMoney object, the ->__toString() method will be called\n";
412                 $signature .= " * resulting in the value minus the currency symbol and thousands separators\n";
413                 $signature .= " * \n";
414                 $signature .= " * @return string  The HTML form-ready value\n";
415                 $signature .= " */\n";
416             }
417             $encode_method = 'encode' . $camelized_column;
418             $signature .= 'public function ' . $encode_method . '()';
419            
420             $signatures[$encode_method] = $signature;
421            
422             $signature = '';
423             if ($include_doc_comments) {
424                 $signature .= "/**\n";
425                 $signature .= " * Prepares the value of " . $column . " for output into HTML\n";
426                 $signature .= " * \n";
427                 $signature .= " * If the value is an fMoney object, the ->format() method will be called\n";
428                 $signature .= " * resulting in the value including the currency symbol and thousands separators\n";
429                 $signature .= " * \n";
430                 $signature .= " * @param  boolean \$remove_zero_fraction  If a fraction of all zeros should be removed\n";
431                 $signature .= " * @return string  The HTML-ready value\n";
432                 $signature .= " */\n";
433             }
434             $prepare_method = 'prepare' . $camelized_column;
435             $signature .= 'public function ' . $prepare_method . '($remove_zero_fraction=FALSE)';
436            
437             $signatures[$prepare_method] = $signature;
438         }
439     }
440    
441    
442     /**
443     * Resets the configuration of the class
444     *
445     * @internal
446      *
447     * @return void
448     */
449     static public function reset()
450     {
451         self::$currency_columns = array();
452         self::$money_columns    = array();
453     }
454    
455    
456     /**
457     * Sets the currency column and then tries to objectify the related money column
458     *
459     * @internal
460      *
461     * @param  fActiveRecord $object            The fActiveRecord instance
462     * @param  array         &$values           The current values
463     * @param  array         &$old_values       The old values
464     * @param  array         &$related_records  Any records related to this record
465     * @param  array         &$cache            The cache array for the record
466     * @param  string        $method_name       The method that was called
467     * @param  array         $parameters        The parameters passed to the method
468     * @return fActiveRecord  The record object, to allow for method chaining
469     */
470     static public function setCurrencyColumn($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
471     {
472         list ($action, $column) = fORM::parseMethod($method_name);
473        
474         $class = get_class($object);
475        
476         if (count($parameters) < 1) {
477             throw new fProgrammerException(
478                 'The method, %s(), requires at least one parameter',
479                 $method_name
480             );   
481         }
482        
483         fActiveRecord::assign($values, $old_values, $column, $parameters[0]);
484        
485         // See if we can make an fMoney object out of the values
486         self::objectifyMoneyWithCurrency(
487             $values,
488             $old_values,
489             self::$currency_columns[$class][$column],
490             $column
491         );
492        
493         return $object;
494     }
495    
496    
497     /**
498     * Sets the money column and then tries to objectify it with an related currency column
499     *
500     * @internal
501      *
502     * @param  fActiveRecord $object            The fActiveRecord instance
503     * @param  array         &$values           The current values
504     * @param  array         &$old_values       The old values
505     * @param  array         &$related_records  Any records related to this record
506     * @param  array         &$cache            The cache array for the record
507     * @param  string        $method_name       The method that was called
508     * @param  array         $parameters        The parameters passed to the method
509     * @return fActiveRecord  The record object, to allow for method chaining
510     */
511     static public function setMoneyColumn($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
512     {
513         list ($action, $column) = fORM::parseMethod($method_name);
514        
515         $class = get_class($object);
516        
517         if (count($parameters) < 1) {
518             throw new fProgrammerException(
519                 'The method, %s(), requires at least one parameter',
520                 $method_name
521             );   
522         }
523        
524         $value = $parameters[0];
525        
526         fActiveRecord::assign($values, $old_values, $column, $value);
527        
528         $currency_column = self::$money_columns[$class][$column];
529        
530         // See if we can make an fMoney object out of the values
531         self::objectifyMoneyWithCurrency($values, $old_values, $column, $currency_column);
532        
533         if ($currency_column) {
534             if ($value instanceof fMoney) {
535                 fActiveRecord::assign($values, $old_values, $currency_column, $value->getCurrency());
536             }   
537         }
538        
539         return $object;
540     }
541    
542    
543     /**
544     * Validates all money columns
545     *
546     * @internal
547      *
548     * @param  fActiveRecord $object                The fActiveRecord instance
549     * @param  array         &$values               The current values
550     * @param  array         &$old_values           The old values
551     * @param  array         &$related_records      Any records related to this record
552     * @param  array         &$cache                The cache array for the record
553     * @param  array         &$validation_messages  An array of ordered validation messages
554     * @return void
555     */
556     static public function validateMoneyColumns($object, &$values, &$old_values, &$related_records, &$cache, &$validation_messages)
557     {
558         $class = get_class($object);
559        
560         if (empty(self::$money_columns[$class])) {
561             return;
562         }
563        
564         foreach (self::$money_columns[$class] as $column => $currency_column) {
565             if ($values[$column] instanceof fMoney || $values[$column] === NULL) {
566                 continue;
567             }
568            
569             // Remove any previous validation warnings
570             unset($validation_messages[$column]);
571            
572             $column_name = fValidationException::formatField(fORM::getColumnName($class, $currency_column));
573            
574             if ($currency_column && !in_array($values[$currency_column], fMoney::getCurrencies())) {
575                 $validation_messages[$column] = self::compose(
576                     '%sThe currency specified is invalid',
577                     $column_name
578                 );   
579                
580             } else {
581                 $validation_messages[$column] = self::compose(
582                     '%sPlease enter a monetary value',
583                     $column_name
584                 );
585             }
586         }
587     }
588    
589    
590     /**
591     * Forces use as a static class
592     *
593     * @return fORMMoney
594     */
595     private function __construct() { }
596 }
597  
598  
599  
600 /**
601  * Copyright (c) 2008-2010 Will Bond <will@flourishlib.com>, others
602  *
603  * Permission is hereby granted, free of charge, to any person obtaining a copy
604  * of this software and associated documentation files (the "Software"), to deal
605  * in the Software without restriction, including without limitation the rights
606  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
607  * copies of the Software, and to permit persons to whom the Software is
608  * furnished to do so, subject to the following conditions:
609  *
610  * The above copyright notice and this permission notice shall be included in
611  * all copies or substantial portions of the Software.
612  *
613  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
614  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
615  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
616  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
617  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
618  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
619  * THE SOFTWARE.
620  */