root/trunk/lib/AkActiveRecord.php

Revision 1401, 214.0 kB (checked in by arnoschn, 11 months ago)

fixing IN ? bug in findBy queries, fixing bind warning in associated finder

Line 
1 <?php
2
3 // +----------------------------------------------------------------------+
4 // | Akelos Framework - http://www.akelos.org                             |
5 // +----------------------------------------------------------------------+
6 // | Released under the GNU Lesser General Public License, see LICENSE.txt|
7 // +----------------------------------------------------------------------+
8
9 /**
10  * @package ActiveRecord
11  * @component Active Record
12  * @author Bermi Ferrer <bermi a.t bermilabs c.om>
13  * @author Kaste
14  * @author Arno Schneider <arno a.t bermilabs c.om>
15  * @copyright Copyright (c) 2002-2009, The Akelos Team http://www.akelos.org
16  * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
17  */
18
19 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkAssociatedActiveRecord.php');
20 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkActiveRecordMock.php');
21 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkDbAdapter.php');
22 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkDbSchemaCache.php');
23 /**#@+
24 * Constants
25 */
26 // Akelos args is a short way to call functions that is only intended for fast prototyping
27 defined('AK_ENABLE_AKELOS_ARGS') ? null : define('AK_ENABLE_AKELOS_ARGS', false);
28 // Use setColumnName if available when using set('column_name', $value);
29 defined('AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT') ? null : define('AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT', true);
30 defined('AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS') ? null : define('AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS', false);
31 defined('AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS') ? null : define('AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS', AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS);
32 defined('AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS') ? null : define('AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS', AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS);
33
34 defined('AK_ACTIVE_RECORD_ENABLE_PERSISTENCE') ? null : define('AK_ACTIVE_RECORD_ENABLE_PERSISTENCE', AK_ENVIRONMENT != 'testing');
35 defined('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA') ? null : define('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA', AK_ACTIVE_RECORD_ENABLE_PERSISTENCE && AK_ENVIRONMENT != 'development');
36 defined('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE') ? null : define('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE', 300);
37 defined('AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES') ? null : define('AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES', true);
38 defined('AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS') ? null : define('AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS', false);
39 defined('AK_NOT_EMPTY_REGULAR_EXPRESSION') ? null : define('AK_NOT_EMPTY_REGULAR_EXPRESSION','/.+/');
40 defined('AK_EMAIL_REGULAR_EXPRESSION') ? null : define('AK_EMAIL_REGULAR_EXPRESSION',"/^([a-z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-z0-9\-]+\.)+))([a-z]{2,4}|[0-9]{1,3})(\]?)$/i");
41 defined('AK_NUMBER_REGULAR_EXPRESSION') ? null : define('AK_NUMBER_REGULAR_EXPRESSION',"/^[0-9]+$/");
42 defined('AK_PHONE_REGULAR_EXPRESSION') ? null : define('AK_PHONE_REGULAR_EXPRESSION',"/^([\+]?[(]?[\+]?[ ]?[0-9]{2,3}[)]?[ ]?)?[0-9 ()\-]{4,25}$/");
43 defined('AK_DATE_REGULAR_EXPRESSION') ? null : define('AK_DATE_REGULAR_EXPRESSION',"/^(([0-9]{1,2}(\-|\/|\.| )[0-9]{1,2}(\-|\/|\.| )[0-9]{2,4})|([0-9]{2,4}(\-|\/|\.| )[0-9]{1,2}(\-|\/|\.| )[0-9]{1,2})){1}$/");
44 defined('AK_IP4_REGULAR_EXPRESSION') ? null : define('AK_IP4_REGULAR_EXPRESSION',"/^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/");
45 defined('AK_POST_CODE_REGULAR_EXPRESSION') ? null : define('AK_POST_CODE_REGULAR_EXPRESSION',"/^[0-9A-Za-z  -]{2,9}$/");
46 /**#@-*/
47
48
49
50 ak_compat('array_combine');
51
52 /**
53 * Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with
54 * which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
55 * is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain
56 * database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
57 *
58 * See the mapping rules in table_name and the full example in README.txt for more insight.
59 *
60 * == Creation ==
61 *
62 * Active Records accepts constructor parameters either in an array or as a list of parameters in a specific format. The array method is especially useful when
63 * you're receiving the data from somewhere else, like a HTTP request. It works like this:
64 *
65 * <code>
66 *   $user = new User(array('name' => 'David', 'occupation' => 'Code Artist'));
67 *   echo $user->name; // Will print "David"
68 * </code>
69 *
70 * You can also use a parameter list initialization.:
71 *
72 *   $user = new User('name->', 'David', 'occupation->', 'Code Artist');
73 *
74 * And of course you can just create a bare object and specify the attributes after the fact:
75 *
76 * <code>
77 *   $user = new User();
78 *   $user->name = 'David';
79 *   $user->occupation = 'Code Artist';
80 * </code>
81 *
82 * == Conditions ==
83 *
84 * Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement.
85 * The array form is to be used when the condition input is tainted and requires sanitization. The string form can
86 * be used for statements that doesn't involve tainted data. Examples:
87 *
88 * <code>
89 *   class User extends ActiveRecord
90 *   {
91 *     public function authenticateUnsafely($user_name, $password)
92 *     {
93 *          return findFirst("user_name = '$user_name' AND password = '$password'");
94 *     }
95 *
96 *     public function authenticateSafely($user_name, $password)
97 *     {
98 *          return findFirst("user_name = ? AND password = ?", $user_name, $password);
99 *     }
100 *    }
101 * </code>
102 *
103 * The <tt>authenticateUnsafely</tt> method inserts the parameters directly into the query and is thus susceptible to SQL-injection
104 * attacks if the <tt>$user_name</tt> and <tt>$password</tt> parameters come directly from a HTTP request. The <tt>authenticateSafely</tt> method,
105 * on the other hand, will sanitize the <tt>$user_name</tt> and <tt>$password</tt> before inserting them in the query, which will ensure that
106 * an attacker can't escape the query and fake the login (or worse).
107 *
108 * When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth
109 * question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing
110 * the question marks with symbols and supplying a hash with values for the matching symbol keys:
111 *
112 * <code>
113 *   $Company->findFirst(
114 *              "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
115 *               array(':id' => 3, ':name' => "37signals", ':division' => "First", ':accounting_date' => '2005-01-01')
116 *             );
117 * </code>
118 *
119 * == Accessing attributes before they have been type casted ==
120 *
121 * Some times you want to be able to read the raw attribute data without having the column-determined type cast run its course first.
122 * That can be done by using the <attribute>_before_type_cast accessors that all attributes have. For example, if your Account model
123 * has a balance attribute, you can call $Account->balance_before_type_cast or $Account->id_before_type_cast.
124 *
125 * This is especially useful in validation situations where the user might supply a string for an integer field and you want to display
126 * the original string back in an error message. Accessing the attribute normally would type cast the string to 0, which isn't what you
127 * want.
128 *
129 * == Saving arrays, hashes, and other non-mappable objects in text columns ==
130 *
131 * Active Record can serialize any object in text columns. To do so, you must specify this with by setting the attribute serialize with
132 * an array where the Keys is the column name and the value should be either true or the class name of the object being serialized.
133 *
134 * This makes it possible to store arrays, hashes, and other non-mappeable objects without doing any additional work. Example:
135 *
136 * <code>
137 *   class User extends ActiveRecord
138 *   {
139 *      public $serialize = array('preferences');
140 *   }
141 *
142 *   $User = new User(array('preferences'=>array("background" => "black", "display" => 'large')));
143 *   $User->find($user_id);
144 *   $User->preferences // array("background" => "black", "display" => 'large')
145 * </code>
146 *
147 * == Single table inheritance ==
148 *
149 * Active Record allows inheritance by storing the name of the class in a column that by default is called "type" (can be changed
150 * by overwriting <tt>AkActiveRecord->_inheritanceColumn</tt>). This means that an inheritance looking like this:
151 *
152 * <code>
153 *   class Company extends ActiveRecord{}
154 *   class Firm extends Company{}
155 *   class Client extends Company{}
156 *   class PriorityClient extends Client{}
157 * </code>
158 *
159 * When you do $Firm->create('name =>', "akelos"), this record will be saved in the companies table with type = "Firm". You can then
160 * fetch this row again using $Company->find('first', "name = '37signals'") and it will return a Firm object.
161 *
162 * If you don't have a type column defined in your table, single-table inheritance won't be triggered. In that case, it'll work just
163 * like normal subclasses with no special magic for differentiating between them or reloading the right type with find.
164 *
165 * Note, all the attributes for all the cases are kept in the same table. Read more:
166 * http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
167 *
168 * == Connection to multiple databases in different models ==
169 *
170 * Connections are usually created through AkActiveRecord->establishConnection and retrieved by AkActiveRecord->connection.
171 * All classes inheriting from AkActiveRecord will use this connection. But you can also set a class-specific connection.
172 * For example, if $Course is a AkActiveRecord, but resides in a different database you can just say $Course->establishConnection
173 * and $Course and all its subclasses will use this connection instead.
174 *
175 * Active Records will automatically record creation and/or update timestamps of database objects
176 * if fields of the names created_at/created_on or updated_at/updated_on are present.
177 * Date only: created_on, updated_on
178 * Date and time: created_at, updated_at
179 *
180 * This behavior can be turned off by setting <tt>$this->_recordTimestamps = false</tt>.
181 */
182 class AkActiveRecord extends AkAssociatedActiveRecord
183 {
184     /**#@+
185     * @access private
186     */
187     //var $disableAutomatedAssociationLoading = true;
188     public $_tableName;
189     public $_db;
190     public $_newRecord;
191     public $_freeze;
192     public $_dataDictionary;
193     public $_primaryKey;
194     public $_inheritanceColumn;
195
196     public $_associations;
197
198     public $_internationalize;
199
200     public $_errors = array();
201
202     public $_attributes = array();
203
204     public $_protectedAttributes = array();
205     public $_accessibleAttributes = array();
206
207     public $_recordTimestamps = true;
208
209     // Column description
210     public $_columnNames = array();
211     // Array of column objects for the table associated with this class.
212     public $_columns = array();
213     // Columns that can be edited/viewed
214     public $_contentColumns = array();
215
216     public $_combinedAttributes = array();
217
218     public $_BlobQueryStack = null;
219
220     public $_automated_max_length_validator = false;
221     public $_automated_validators_enabled = false;
222     public $_automated_not_null_validator = false;
223     public $_set_default_attribute_values_automatically = true;
224
225     // This is needed for enabling support for static active record instantation under php
226     public $_activeRecordHasBeenInstantiated = true;
227
228     public $__ActsLikeAttributes = array();
229
230     /**
231     * Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations.
232     */
233     public $_defaultErrorMessages = array(
234     'inclusion' =>  "is not included in the list",
235     'exclusion' => "is reserved",
236     'invalid' => "is invalid",
237     'confirmation' => "doesn't match confirmation",
238     'accepted' => "must be accepted",
239     'empty' => "can't be empty",
240     'blank' => "can't be blank",
241     'too_long' => "is too long (max is %d characters)",
242     'too_short' => "is too short (min is %d characters)",
243     'wrong_length' => "is the wrong length (should be %d characters)",
244     'taken' => "has already been taken",
245     'not_a_number' => "is not a number"
246     );
247
248     public $__activeRecordObject = true;
249
250     /**#@-*/
251
252     public function __construct()
253     {
254         $attributes = (array)func_get_args();
255         return $this->init($attributes);
256     }
257
258     public function init($attributes = array())
259     {
260         AK_LOG_EVENTS ? ($this->Logger =& Ak::getLogger()) : null;
261         $this->_internationalize = is_null($this->_internationalize) && AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT ? count($this->getAvailableLocales()) > 1 : $this->_internationalize;
262
263         @$this->_instantiateDefaultObserver();
264
265         $this->establishConnection();
266
267         if(!empty($this->table_name)){
268             $this->setTableName($this->table_name);
269         }
270         $load_acts = isset($attributes[1]['load_acts']) ? $attributes[1]['load_acts'] : (isset($attributes[0]['load_acts']) ? $attributes[0]['load_acts'] : true);
271         $this->act_as = !empty($this->acts_as) ? $this->acts_as : (empty($this->act_as) ? false : $this->act_as);
272         if (!empty($this->act_as) && $load_acts) {
273             $this->_loadActAsBehaviours();
274         }
275
276         if(!empty($this->combined_attributes)){
277             foreach ($this->combined_attributes as $combined_attribute){
278                 $this->addCombinedAttributeConfiguration($combined_attribute);
279             }
280         }
281
282         if(isset($attributes[0]) && is_array($attributes[0]) && count($attributes) === 1){
283             $attributes = $attributes[0];
284             $this->_newRecord = true;
285         }
286
287         // new AkActiveRecord(23); //Returns object with primary key 23
288         if(isset($attributes[0]) && count($attributes) === 1 && $attributes[0] > 0){
289             $record = $this->find($attributes[0]);
290             if(!$record){
291                 return false;
292             }else {
293                 $this->setAttributes($record->getAttributes(), true);
294             }
295             // This option is only used internally for loading found objects
296         }elseif(isset($attributes[0]) && isset($attributes[1]) && $attributes[0] == 'attributes' && is_array($attributes[1])){
297             foreach(array_keys($attributes[1]) as $k){
298                 $attributes[1][$k] = $this->castAttributeFromDatabase($k, $attributes[1][$k]);
299             }
300
301             $avoid_loading_associations = isset($attributes[1]['load_associations']) ? false : !empty($this->disableAutomatedAssociationLoading);
302             $this->setAttributes($attributes[1], true);
303         }else{
304             $this->newRecord($attributes);
305         }
306
307         empty($avoid_loading_associations) ? $this->loadAssociations() : null;
308
309     }
310
311     public function __destruct()
312     {
313
314     }
315
316     /**
317     * New objects can be instantiated as either empty (pass no construction parameter) or pre-set with attributes but not yet saved
318     * (pass an array with key names matching the associated table column names).
319     * In both instances, valid attribute keys are determined by the column names of the associated table; hence you can't
320     * have attributes that aren't part of the table columns.
321     */
322     public function newRecord($attributes)
323     {
324         $this->_newRecord = true;
325
326         if(AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS && empty($attributes)){
327             return;
328         }
329
330         if(isset($attributes) && !is_array($attributes)){
331             $attributes = func_get_args();
332         }
333         $this->setAttributes($this->attributesFromColumnDefinition(),true);
334         $this->setAttributes($attributes);
335     }
336
337
338     /**
339     * Returns a clone of the record that hasn't been assigned an id yet and is treated as a new record.
340     */
341     public function cloneRecord()
342     {
343         $model_name = $this->getModelName();
344         $attributes = $this->getAttributesBeforeTypeCast();
345         if(isset($attributes[$this->getPrimaryKey()])){
346             unset($attributes[$this->getPrimaryKey()]);
347         }
348         return new $model_name($attributes);
349     }
350
351
352     /**
353     * Returns true if this object hasn't been saved yet that is, a record for the object doesn't exist yet.
354     */
355     public function isNewRecord()
356     {
357         if(!isset($this->_newRecord) && !isset($this->{$this->getPrimaryKey()})){
358             $this->_newRecord = true;
359         }
360         return $this->_newRecord;
361     }
362
363
364
365     /**
366     * Reloads the attributes of this object from the database.
367     */
368     public function reload()
369     {
370         /**
371         * @todo clear cache
372         */
373         if($object = $this->find($this->getId())){
374             $this->setAttributes($object->getAttributes(), true);
375             return true;
376         }else {
377             return false;
378         }
379     }
380
381
382
383     /**
384                          Creating records
385     ====================================================================
386     */
387     /**
388     * Creates an object, instantly saves it as a record (if the validation permits it), and returns it.
389     * If the save fail under validations, the unsaved object is still returned.
390     */
391     public function &create($attributes = null)
392     {
393         if(func_num_args() > 1){
394             $attributes = func_get_args();
395         }
396         $model = $this->getModelName();
397
398         $object = new $model();
399         $object->setAttributes($attributes);
400         $object->save();
401         return $object;
402     }
403
404     public function createOrUpdate($validate = true)
405     {
406         if($validate && !$this->isValid()){
407             $this->transactionFail();
408             return false;
409         }
410         return $this->isNewRecord() ? $this->_create() : $this->_update();
411     }
412
413     public function &findOrCreateBy()
414     {
415         $args = func_get_args();
416         $Item =& call_user_func_array(array(&$this,'findFirstBy'), $args);
417         if(!$Item){
418             $attributes = array();
419
420             list($sql, $columns) = $this->_getFindBySqlAndColumns(array_shift($args), $args);
421
422             if(!empty($columns)){
423                 foreach ($columns as $column){
424                     $attributes[$column] = array_shift($args);
425                 }
426             }
427             $Item =& $this->create($attributes);
428             $Item->has_been_created = true;
429         }else{
430             $Item->has_been_created = false;
431         }
432         $Item->has_been_found = !$Item->has_been_created;
433         return $Item;
434     }
435
436     /**
437     * Creates a new record with values matching those of the instance attributes.
438     * Must be called as a result of a call to createOrUpdate.
439     *
440     * @access private
441     */
442     public function _create()
443     {
444         if (!$this->beforeCreate() || !$this->notifyObservers('beforeCreate')){
445             return $this->transactionFail();
446         }
447
448         $this->_setRecordTimestamps();
449
450         // deprecated section
451         if($this->isLockingEnabled() && is_null($this->get('lock_version'))){
452             Ak::deprecateWarning(array("Column %lock_version_column should have a default setting. Assumed '1'.",'%lock_version_column'=>'lock_version'));
453             $this->setAttribute('lock_version',1);
454         } // end
455
456         $attributes = $this->getColumnsForAttributes($this->getAttributes());
457         foreach ($attributes as $column=>$value){
458             $attributes[$column] = $this->castAttributeForDatabase($column,$value);
459         }
460
461         $pk = $this->getPrimaryKey();
462         $table = $this->getTableName();
463
464         $id = $this->_db->incrementsPrimaryKeyAutomatically() ? null : $this->_db->getNextSequenceValueFor($table);
465         $attributes[$pk] = $id;
466
467         $attributes = array_diff($attributes, array(''));
468
469
470         $sql = 'INSERT INTO '.$table.' '.
471         '('.join(', ',array_keys($attributes)).') '.
472         'VALUES ('.join(',',array_values($attributes)).')';
473
474         $inserted_id = $this->_db->insert($sql, $id, $pk, $table, 'Create '.$this->getModelName());
475         if ($this->transactionHasFailed()){
476             return false;
477         }
478         $this->setId($inserted_id);
479
480         $this->_newRecord = false;
481
482         if (!$this->afterCreate() || !$this->notifyObservers('afterCreate')){
483             return $this->transactionFail();
484         }
485
486         return true;
487     }
488
489     public function _setRecordTimestamps()
490     {
491         if (!$this->_recordTimestamps){
492             return;
493         }
494         if ($this->_newRecord){
495             if ($this->hasColumn('created_at')){
496                 $this->setAttribute('created_at', Ak::getDate());
497             }
498             if ($this->hasColumn('created_on')){
499                 $this->setAttribute('created_on', Ak::getDate(null, 'Y-m-d'));
500             }
501         }else{
502             if ($this->hasColumn('updated_at')){
503                 $this->setAttribute('updated_at', Ak::getDate());
504             }
505             if ($this->hasColumn('updated_on')){
506                 $this->setAttribute('updated_on', Ak::getDate(null, 'Y-m-d'));
507             }
508         }
509
510         if($this->_newRecord && isset($this->expires_on)){
511             if(isset($this->expires_at) && $this->hasColumn('expires_at')){
512                 $this->setAttribute('expires_at',Ak::getDate(strtotime($this->expires_at) + (defined('AK_TIME_DIFFERENCE') ? AK_TIME_DIFFERENCE*60 : 0)));
513             }elseif(isset($this->expires_on) && $this->hasColumn('expires_on')){
514                 $this->setAttribute('expires_on',Ak::getDate(strtotime($this->expires_on) + (defined('AK_TIME_DIFFERENCE') ? AK_TIME_DIFFERENCE*60 : 0), 'Y-m-d'));
515             }
516         }
517
518     }
519
520     /*/Creating records*/
521
522
523     /**
524                          Saving records
525     ====================================================================
526     */
527     /**
528     * - No record exists: Creates a new record with values matching those of the object attributes.
529     * - A record does exist: Updates the record with values matching those of the object attributes.
530     */
531     public function save($validate = true)
532     {
533         if($this->isFrozen()){
534             return false;
535         }
536         $result = false;
537         $this->transactionStart();
538         if($this->beforeSave() && $this->notifyObservers('beforeSave')){
539             $result = $this->createOrUpdate($validate);
540             if(!$this->transactionHasFailed()){
541                 if(!$this->afterSave()){
542                     $this->transactionFail();
543                 }else{
544                     if(!$this->notifyObservers('afterSave')){
545                         $this->transactionFail();
546                     }
547                 }
548             }
549         }else{
550             $this->transactionFail();
551         }
552
553         $result = $this->transactionHasFailed() ? false : $result;
554         $this->transactionComplete();
555
556         return $result;
557     }
558
559     /*/Saving records*/
560
561     /**
562                             Counting Records
563     ====================================================================
564     See also: Counting Attributes.
565     */
566
567     /**
568       * Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
569       *
570       *   $Product->countBySql("SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id");
571       */
572     public function countBySql($sql)
573     {
574         if(!stristr($sql, 'COUNT') && stristr($sql, ' FROM ')){
575             $sql = 'SELECT COUNT(*) '.substr($sql,strpos(str_replace(' from ',' FROM ', $sql),' FROM '));
576         }
577         if(!$this->isConnected()){
578             $this->establishConnection();
579         }
580
581         return (integer)$this->_db->selectValue($sql);
582     }
583     /*/Counting Records*/
584
585     /**
586                           Updating records
587     ====================================================================
588     See also: Callbacks.
589     */
590
591     /**
592     * Finds the record from the passed id, instantly saves it with the passed attributes (if the validation permits it),
593     * and returns it. If the save fail under validations, the unsaved object is still returned.
594     */
595     public function update($id, $attributes)
596     {
597         if(is_array($id)){
598             $results = array();
599             foreach ($id as $idx=>$single_id){
600                 $results[] = $this->update($single_id, isset($attributes[$idx]) ? $attributes[$idx] : $attributes);
601             }
602             return $results;
603         }else{
604             $object =& $this->find($id);
605             $object->updateAttributes($attributes);
606             return $object;
607         }
608     }
609
610     /**
611     * Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
612     */
613     public function updateAttribute($name, $value, $should_validate=true)
614     {
615         $this->setAttribute($name, $value);
616         return $this->save($should_validate);
617     }
618
619
620     /**
621     * Updates all the attributes in from the passed array and saves the record. If the object is
622     * invalid, the saving will fail and false will be returned.
623     */
624     public function updateAttributes($attributes, $object = null)
625     {
626         isset($object) ? $object->setAttributes($attributes) : $this->setAttributes($attributes);
627
628         return isset($object) ? $object->save() : $this->save();
629     }
630
631     /**
632     * Updates all records with the SET-part of an SQL update statement in updates and returns an
633     * integer with the number of rows updates. A subset of the records can be selected by specifying conditions. Example:
634     * <code>$Billing->updateAll("category = 'authorized', approved = 1", "author = 'David'");</code>
635     *
636     * Or using binds, the safer way:
637     * <code>$Billing->updateAll("category = 'authorized', approved = 1", array("author = ?","David"));</code>
638     *
639     * Important note: Conditions are not sanitized yet so beware of accepting
640     * variable conditions when using this function
641     */
642     public function updateAll($updates, $conditions = null)
643     {
644         /**
645         * @todo sanitize sql conditions
646         */
647         $sql = 'UPDATE '.$this->getTableName().' SET '.$updates;
648         $binds = false;
649         if(is_array($conditions)) {
650             /*
651             * take the first item as the conditions, the following are binds
652             *
653             */
654             $binds = $conditions;
655             $conditions=array_shift($binds);
656
657         }
658         $this->addConditions($sql, $conditions);
659         if($binds) {
660             $sql = array_merge(array($sql),$binds);
661         }
662         return $this->_db->update($sql, $this->getModelName().' Update All');
663     }
664
665
666     /**
667     * Updates the associated record with values matching those of the instance attributes.
668     * Must be called as a result of a call to createOrUpdate.
669     *
670     * @access private
671     */
672     public function _update()
673     {
674         if(!$this->beforeUpdate() || !$this->notifyObservers('beforeUpdate')){
675             return $this->transactionFail();
676         }
677
678         $this->_setRecordTimestamps();
679
680         $lock_check_sql = '';
681         if ($this->isLockingEnabled()){
682             $previous_value = $this->lock_version;
683             $this->setAttribute('lock_version', $previous_value + 1);
684             $lock_check_sql = ' AND lock_version = '.$previous_value;
685         }
686
687         $quoted_attributes = $this->getAvailableAttributesQuoted();
688         $sql = 'UPDATE '.$this->getTableName().' '.
689         'SET '.join(', ', $quoted_attributes) .' '.
690         'WHERE '.$this->getPrimaryKey().'='.$this->quotedId().$lock_check_sql;
691
692         $affected_rows = $this->_db->update($sql,'Updating '.$this->getModelName());
693         if($this->transactionHasFailed()){
694             return false;
695         }
696
697         if ($this->isLockingEnabled() && $affected_rows != 1){
698             $this->setAttribute('lock_version', $previous_value);
699             trigger_error(Ak::t('Attempted to update a stale object'), E_USER_NOTICE);
700             return $this->transactionFail();
701         }
702
703         if(!$this->afterUpdate() || !$this->notifyObservers('afterUpdate')){
704             return $this->transactionFail();
705         }
706
707         return true;
708     }
709
710     /*/Updating records*/
711
712
713
714     /**
715                           Deleting records
716     ====================================================================
717     See also: Callbacks.
718     */
719
720     /**
721     * Deletes the record with the given id without instantiating an object first. If an array of
722     * ids is provided, all of them are deleted.
723     */
724     public function delete($id)
725     {
726         $id = func_num_args() > 1 ? func_get_args() : $id;
727         return $this->deleteAll($this->getPrimaryKey().' IN ('.join(', ', $this->castAttributesForDatabase($this->getPrimaryKey(), Ak::toArray($id))).')');
728     }
729
730
731     /**
732     * Deletes all the records that matches the condition without instantiating the objects first
733     * (and hence not calling the destroy method). Example:
734     *
735     * <code>$Post->destroyAll("person_id = 5 AND (category = 'Something' OR category = 'Else')");</code>
736     *
737     * Or using binds, the safer way:
738     *
739     * <code>$Post->destroyAll(array("person_id = ? AND (category = ? OR category = ?)",5,"Something","Else"));</code>
740     *
741     * Important note: Conditions are not sanitized yet so beware of accepting
742     * variable conditions when using this function
743     */
744     public function deleteAll($conditions = null)
745     {
746         /**
747         * @todo sanitize sql conditions
748         */
749         $sql = 'DELETE FROM '.$this->getTableName();
750         $binds = false;
751         if(is_array($conditions)) {
752             /*
753             * take the first item as the conditions, the following are binds
754             *
755             */
756             $binds = $conditions;
757             $conditions=array_shift($binds);
758
759         }
760         $this->addConditions($sql, $conditions);
761         if($binds) {
762             $sql = array_merge(array($sql),$binds);
763         }
764         return $this->_db->delete($sql,$this->getModelName().' Delete All');
765     }
766
767
768     /**
769     * Destroys the record with the given id by instantiating the object and calling destroy
770     * (all the callbacks are the triggered). If an array of ids is provided, all of them are destroyed.
771     * Deletes the record in the database and freezes this instance to reflect that no changes should be
772     * made (since they can't be persisted).
773     */
774     public function destroy($id = null)
775     {
776         $id = func_num_args() > 1 ? func_get_args() : $id;
777
778         if(isset($id)){
779             $this->transactionStart();
780             $id_arr = is_array($id) ? $id : array($id);
781             if($objects = $this->find($id_arr)){
782                 $results = count($objects);
783                 $no_problems = true;
784                 for ($i=0; $results > $i; $i++){
785                     if(!$objects[$i]->destroy()){
786                         $no_problems = false;
787                     }
788                 }
789                 $this->transactionComplete();
790                 return $no_problems;
791             }else {
792                 $this->transactionComplete();
793                 return false;
794             }
795         }else{
796             if(!$this->isNewRecord()){
797                 $this->transactionStart();
798                 $return = $this->_destroy() && $this->freeze();
799                 $this->transactionComplete();
800                 return $return;
801             }
802         }
803     }
804
805     public function _destroy()
806     {
807         if(!$this->beforeDestroy() || !$this->notifyObservers('beforeDestroy')){
808             return $this->transactionFail();
809         }
810         $sql = 'DELETE FROM '.$this->getTableName().' WHERE '.$this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $this->getId());
811         if ($this->_db->delete($sql,$this->getModelName().' Destroy') !== 1){
812              return $this->transactionFail();
813         }
814
815         if (!$this->afterDestroy() || !$this->notifyObservers('afterDestroy')){
816             return $this->transactionFail();
817         }
818         return true;
819     }
820
821     /**
822     * Destroys the objects for all the records that matches the condition by instantiating
823     * each object and calling the destroy method.
824     *
825     * Example:
826     *
827     *   $Person->destroyAll("last_login < '2004-04-04'");
828     */
829     public function destroyAll($conditions)
830     {
831         if($objects = $this->find('all',array('conditions'=>$conditions))){
832             $results = count($objects);
833             $no_problems = true;
834             for ($i=0; $results > $i; $i++){
835                 if(!$objects[$i]->destroy()){
836                     $no_problems = false;
837                 }
838             }
839             return $no_problems;
840         }else {
841             return false;
842         }
843     }
844
845     /*/Deleting records*/
846
847
848
849
850     /**
851                           Finding records
852     ====================================================================
853     */
854
855     /**
856     * Returns true if the given id represents the primary key of a record in the database, false otherwise. Example:
857     *
858     * $Person->exists(5);
859     */
860     public function exists($id)
861     {
862         return $this->find('first',array('conditions' => array($this->getPrimaryKey().' = '.$id))) !== false;
863     }
864
865     /**
866      * Find operates with three different retrieval approaches:
867     * * Find by id: This can either be a specific id find(1), a list of ids find(1, 5, 6),
868     *   or an array of ids find(array(5, 6, 10)). If no record can be found for all of the listed ids,
869     *   then RecordNotFound will be raised.
870     * * Find first: This will return the first record matched by the options used. These options
871     *   can either be specific conditions or merely an order.
872     *   If no record can matched, false is returned.
873     * * Find all: This will return all the records matched by the options used. If no records are found, an empty array is returned.
874     *
875     * All approaches accepts an $option array as their last parameter. The options are:
876     *
877     * 'conditions' => An SQL fragment like "administrator = 1" or array("user_name = ?" => $username). See conditions in the intro.
878     * 'order' => An SQL fragment like "created_at DESC, name".
879     * 'limit' => An integer determining the limit on the number of rows that should be returned.
880     * 'offset' => An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
881     * 'joins' => An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = $id". (Rarely needed).
882     * 'include' => Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols
883     * named refer to already defined associations. See eager loading under Associations.
884     *
885     * Examples for find by id:
886     * <code>
887     *   $Person->find(1);       // returns the object for ID = 1
888     *   $Person->find(1, 2, 6); // returns an array for objects with IDs in (1, 2, 6), Returns false if any of those IDs is not available
889     *   $Person->find(array(7, 17)); // returns an array for objects with IDs in (7, 17)
890     *   $Person->find(array(1));     // returns an array for objects the object with ID = 1
891     *   $Person->find(1, array('conditions' => "administrator = 1", 'order' => "created_on DESC"));
892     * </code>
893     *
894     * Examples for find first:
895     * <code>
896     *   $Person->find('first'); // returns the first object fetched by SELECT * FROM people
897     *   $Person->find('first', array('conditions' => array("user_name = ':user_name'", ':user_name' => $user_name)));
898     *   $Person->find('first', array('order' => "created_on DESC", 'offset' => 5));
899     * </code>
900     *
901     * Examples for find all:
902     * <code>
903     *   $Person->find('all'); // returns an array of objects for all the rows fetched by SELECT * FROM people
904     *   $Person->find(); // Same as $Person->find('all');
905     *   $Person->find('all', array('conditions' => array("category IN (categories)", 'categories' => join(','$categories)), 'limit' => 50));
906     *   $Person->find('all', array('offset' => 10, 'limit' => 10));
907     *   $Person->find('all', array('include' => array('account', 'friends'));
908     * </code>
909     */
910     public function &find()
911     {
912         $args = func_get_args();
913         $options = $this->_extractOptionsFromArgs($args);
914         list($fetch,$options) = $this->_extractConditionsFromArgs($args,$options);
915         $this->_sanitizeConditionsVariables($options);
916         switch ($fetch) {
917             case 'first':
918                 return $this->_findInitial($options);
919
920             case 'all':
921                 return $this->_findEvery($options);
922
923             default:
924                 return $this->_findFromIds($args, $options);
925         }
926         return false;
927     }
928
929     public function &_findInitial($options)
930     {
931         // TODO: virtual_limit is a hack
932         // actually we fetch_all and return only the first row
933         $options = array_merge($options, array((!empty($options['include']) ?'virtual_limit':'limit')=>1));
934
935         $result =& $this->_findEvery($options);
936
937         if(!empty($result) && is_array($result)){
938             $_result =& $result[0];
939         }else{
940             $_result = false;
941             // if we return an empty array instead of false we need to change this->exists()!
942             //$_result = array();
943         }
944         return  $_result;
945
946     }
947
948     public function &_findEvery($options)
949     {
950         if((!empty($options['include']) && $this->hasAssociations())){
951             $result =& $this->findWithAssociations($options);
952         }else{
953             $sql = $this->constructFinderSql($options);
954             if (isset($options['wrap'])) {
955                 $sql = str_replace('{query}',$sql,$options['wrap']);
956             }
957             if(!empty($options['bind']) && is_array($options['bind']) && strstr($sql,'?')){
958                 $sql = array_merge(array($sql),$options['bind']);
959             }
960             if (!empty($options['returns']) && $options['returns']!='default') {
961                 $options['returns'] = in_array($options['returns'],array('simulated','default','array'))?$options['returns']:'default';
962                 $simulation_class = !empty($options['simulation_class']) && class_exists($options['simulation_class'])?$options['simulation_class']:'AkActiveRecordMock';
963                 $result =& $this->findBySql($sql,null,null,null,$options['returns'],$simulation_class);
964             } else {
965                 $result =& $this->findBySql($sql);
966             }
967         }
968
969         if(!empty($result) && is_array($result)){
970             $_result =& $result;
971         }else{
972             $_result = false;
973         }
974         return  $_result;
975
976     }
977
978     public function &_findFromIds($ids, $options)
979     {
980         $expects_array = is_array($ids[0]);
981
982         $ids = array_map(array($this, 'quotedId'),array_unique($expects_array ? (isset($ids[1]) ? array_merge($ids[0],$ids) : $ids[0]) : $ids));
983         $num_ids = count($ids);
984
985         //at this point $options['conditions'] can't be an array
986         //$conditions = !empty($options['conditions']) ? ' AND '.$options['conditions'] : '';
987         $conditions=!empty($options['conditions'])?$options['conditions']:'';
988         switch ($num_ids){
989             case 0 :
990                 trigger_error($this->t('Couldn\'t find %object_name without an ID%conditions',array('%object_name'=>$this->getModelName(),'%conditions'=>$conditions)), E_USER_ERROR);
991                 break;
992
993             case 1 :
994                 $table_name = !empty($options['include']) && $this->hasAssociations() ? '__owner' : $this->getTableName();
995
996                 if (!preg_match('/SELECT .* FROM/is', $conditions)) {
997                     $options['conditions'] = $table_name.'.'.$this->getPrimaryKey().' = '.$ids[0].(empty($conditions)?'':' AND '.$conditions);
998                 } else {
999                     if (false!==($pos=stripos($conditions,' WHERE '))) {
1000                         $before_where = substr($conditions,0, $pos);
1001                         $after_where = substr($conditions, $pos+7);
1002                         $options['conditions'] = $before_where.' WHERE ('.$table_name.'.'.$this->getPrimaryKey().' = '.$ids[0].') AND ('.$after_where.')';
1003                     } else {
1004                         $options['conditions'].=' WHERE '.$table_name.'.'.$this->getPrimaryKey().' = '.$ids[0];
1005                     }
1006                 }
1007
1008                 $result =& $this->_findEvery($options);
1009                 if (!$expects_array && $result !== false){
1010                     return $result[0];
1011                 }
1012                 return  $result;
1013                 break;
1014
1015             default:
1016                 $without_conditions = empty($options['conditions']) ? true : false;
1017                 $ids_condition = $this->getPrimaryKey().' IN ('.join(', ', $this->castAttributesForDatabase($this->getPrimaryKey(), $ids)).')';
1018                 if (!preg_match('/SELECT .* FROM/is', $conditions)) {
1019                     $options['conditions'] = $ids_condition.(empty($conditions)?'':' AND '.$conditions);
1020                 } else {
1021                     if (false!==($pos=stripos($conditions,' WHERE '))) {
1022                         $before_where = substr($conditions,0, $pos);
1023                         $after_where = substr($conditions, $pos+7);
1024                         $options['conditions'] = $before_where.' WHERE ('.$ids_condition.') AND ('.$after_where.')';
1025                     } else {
1026                         $options['conditions'].=' WHERE '.$ids_condition;
1027                     }
1028                 }
1029
1030                 $result =& $this->_findEvery($options);
1031                 if(is_array($result) && ($num_ids==1 && count($result) != $num_ids && $without_conditions)){
1032                     $result = false;
1033                 }
1034                 return $result;
1035                 break;
1036         }
1037
1038     }
1039     public function quotedId($id = false)
1040     {
1041         return $this->castAttributeForDatabase($this->getPrimaryKey(), $id ? $id : $this->getId());
1042     }
1043     public function _extractOptionsFromArgs(&$args)
1044     {
1045         $last_arg = count($args)-1;
1046         return isset($args[$last_arg]) && is_array($args[$last_arg]) && $this->_isOptionsHash($args[$last_arg]) ? array_pop($args) : array();
1047     }
1048
1049     public function _isOptionsHash($options)
1050     {
1051         if (isset($options[0])){
1052             return false;
1053         }
1054         $valid_keys = array('simulation_class','returns','load_acts','wrap','conditions', 'include', 'joins', 'limit', 'offset', 'group', 'order', 'sort', 'bind', 'select','select_prefix', 'readonly', 'load_associations', 'load_acts');
1055         foreach (array_keys($options) as $key){
1056             if (in_array($key,$valid_keys)){
1057                 return true;
1058             }
1059         }
1060         return false;
1061     }
1062
1063     public function _extractConditionsFromArgs($args, $options)
1064     {
1065         if(empty($args)){
1066             $fetch = 'all';
1067         } else {
1068             $fetch = $args[0];
1069         }
1070         $num_args = count($args);
1071
1072         // deprecated: acts like findFirstBySQL
1073         if ($num_args === 1 && !is_numeric($args[0]) && is_string($args[0]) && $args[0] != 'all' && $args[0] != 'first'){
1074             //  $Users->find("last_name = 'Williams'");    => find('first',"last_name = 'Williams'");
1075             Ak::deprecateWarning(array("AR::find('%sql') is ambiguous and therefore deprecated, use AR::find('first',%sql) instead", '%sql'=>$args[0]));
1076             $options = array('conditions'=> $args[0]);
1077             return array('first',$options);
1078         } //end
1079
1080         // set fetch_mode to 'all' if none is given
1081         if (!is_numeric($fetch) && !is_array($fetch) && $fetch != 'all' && $fetch != 'first') {
1082             array_unshift($args, 'all');
1083             $num_args = count($args);
1084         }
1085         if ($num_args > 1) {
1086             if (is_string($args[1])){
1087                 //  $Users->find(:fetch_mode,"first_name = ?",'Tim');
1088                 $fetch = array_shift($args);
1089                 $options = array_merge($options, array('conditions'=>$args));   //TODO: merge_conditions
1090             }elseif (is_array($args[1])) {
1091                 //  $Users->find(:fetch_mode,array('first_name = ?,'Tim'));
1092                 $fetch = array_shift($args);
1093                 $options = array_merge($options, array('conditions'=>$args[0]));   //TODO: merge_conditions
1094             }
1095         }
1096
1097         return array($fetch,$options);
1098     }
1099
1100     public function _sanitizeConditionsVariables(&$options)
1101     {
1102         if(!empty($options['conditions']) && is_array($options['conditions'])){
1103             if (isset($options['conditions'][0]) && strstr($options['conditions'][0], '?') && count($options['conditions']) > 1){
1104                 //array('conditions' => array("name=?",$name))
1105                 $pattern = array_shift($options['conditions']);
1106                 $options['bind'] = array_values($options['conditions']);
1107                 $options['conditions'] = $pattern;
1108             }elseif (isset($options['conditions'][0])){
1109                 //array('conditions' => array("user_name = :user_name", ':user_name' => 'hilario')
1110                 $pattern = array_shift($options['conditions']);
1111                 $options['conditions'] = str_replace(array_keys($options['conditions']), array_values($this->getSanitizedConditionsArray($options['conditions'])),$pattern);
1112             }else{
1113                 //array('conditions' => array('user_name'=>'Hilario'))
1114                 $options['conditions'] = join(' AND ',(array)$this->getAttributesQuoted($options['conditions']));
1115             }
1116         }
1117         $this->_sanitizeConditionsCollections($options);
1118     }
1119
1120     public function _sanitizeConditionsCollections(&$options)
1121     {
1122         if(!empty($options['bind']) && is_array($options['bind']) && preg_match_all('/([a-zA-Z_]+)\s+IN\s+\(?\?\)?/i', $options['conditions'], $matches)){
1123             $i = 0;
1124             foreach($options['bind'] as $k => $v){
1125                 if(isset($matches[1][$i]) && is_array($v)){
1126                     $value = join(', ', $this->castAttributesForDatabase($matches[1][$i], $v));
1127                     $startpos=strpos($options['conditions'],$matches[0][$i]);
1128                     $endpos=$startpos+strlen($matches[0][$i]);
1129                     $options['conditions'] = substr($options['conditions'],0,$startpos).str_replace('?', $value, $matches[0][$i]).substr( $options['conditions'],$endpos);
1130                     unset($options['bind'][$k]);
1131                     $i++;
1132                 }
1133             }
1134         }
1135     }
1136
1137
1138     public function &findFirst()
1139     {
1140         $args = func_get_args();
1141         $result =& call_user_func_array(array(&$this,'find'), array_merge(array('first'),$args));
1142         return $result;
1143     }
1144
1145     public function &findAll()
1146     {
1147         $args = func_get_args();
1148         $result =& call_user_func_array(array(&$this,'find'), array_merge(array('all'),$args));
1149         return $result;
1150     }
1151
1152
1153     /**
1154     * Works like find_all, but requires a complete SQL string. Examples:
1155     * $Post->findBySql("SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id");
1156     * $Post->findBySql(array("SELECT * FROM posts WHERE author = ? AND created_on > ?", $author_id, $start_date));
1157     */
1158     public function &findBySql($sql, $limit = null, $offset = null, $bindings = null, $returns = 'default', $simulation_class = 'AkActiveRecordMock')
1159     {
1160         if ($limit || $offset){
1161             Ak::deprecateWarning("You're calling AR::findBySql with \$limit or \$offset parameters. This has been deprecated.");
1162             $this->_db->addLimitAndOffset($sql, array('limit'=>$limit,'offset'=>$offset));
1163         }
1164         $objects = array();
1165         $records = $this->_db->select ($sql,'selecting');
1166         foreach ($records as $record){
1167             if ($returns == 'default') {
1168                 $objects[] =& $this->instantiate($this->getOnlyAvailableAttributes($record), false);
1169             } else if ($returns == 'simulated') {
1170                 $objects[] = $this->_castAttributesFromDatabase($this->getOnlyAvailableAttributes($record),$this);
1171             } else if ($returns == 'array') {
1172
1173                 $objects[] = $this->_castAttributesFromDatabase($this->getOnlyAvailableAttributes($record),$this);
1174             }
1175         }
1176         if ($returns == 'simulated') {
1177             $false = false;
1178             $objects = $this->_generateStdClasses($simulation_class,$objects,$this->getType(),$false,$false,array('__owner'=>array('pk'=>$this->getPrimaryKey(),'class'=>$this->getType())));
1179         }
1180
1181         return $objects;
1182     }
1183
1184     /**
1185     * This function pretends to emulate RoR finders until AkActiveRecord::addMethod becomes stable on future PHP versions.
1186     * @todo use PHP5 __call method for handling the magic finder methods like findFirstByUnsenameAndPassword('bermi','pass')
1187     */
1188     public function &findFirstBy()
1189     {
1190         $args = func_get_args();
1191         array_unshift($args,'first');
1192         $result =& call_user_func_array(array(&$this,'findBy'), $args);
1193         return $result;
1194     }
1195
1196     public function &findLastBy()
1197     {
1198         $args = func_get_args();
1199         $options = $this->_extractOptionsFromArgs($args);
1200         $options['order'] = $this->getPrimaryKey().' DESC';
1201         array_push($args, $options);
1202         $result =& call_user_func_array(array(&$this,'findFirstBy'), $args);
1203         return $result;
1204     }
1205
1206     public function &findAllBy()
1207     {
1208         $args = func_get_args();
1209         array_unshift($args,'all');
1210         $result =& call_user_func_array(array(&$this,'findBy'), $args);
1211         return $result;
1212     }
1213
1214     /**
1215     * This method allows you to use finders in a more flexible way like:
1216     *
1217     *   findBy('username AND password', $username, $password);
1218     *   findBy('age > ? AND name:contains', 18, 'Joe');
1219     *   findBy('is_active = true AND session_id', session_id());
1220     *
1221     */
1222     public function &findBy()
1223     {
1224         $args = func_get_args();
1225         $find_by_sql = array_shift($args);
1226         if($find_by_sql == 'all' || $find_by_sql == 'first'){
1227             $fetch = $find_by_sql;
1228             $find_by_sql = array_shift($args);
1229         }else{
1230             $fetch = 'all';
1231         }
1232
1233         $options = $this->_extractOptionsFromArgs($args);
1234
1235         $query_values = $args;
1236         $query_arguments_count = count($query_values);
1237
1238         list($sql, $requested_args) = $this->_getFindBySqlAndColumns($find_by_sql, $query_values);
1239
1240         if($query_arguments_count != count($requested_args)){
1241             trigger_error(Ak::t('Argument list did not match expected set. Requested arguments are:').join(', ',$requested_args),E_USER_ERROR);
1242             $false = false;
1243             return $false;
1244         }
1245
1246         $true_bool_values = array(true,1,'true','True','TRUE','1','y','Y','yes','Yes','YES','s','Si','SI','V','v','T','t');
1247
1248         foreach ($requested_args as $k=>$v){
1249             switch ($this->getColumnType($v)) {
1250                 case 'boolean':
1251                     $query_values[$k] = in_array($query_values[$k],$true_bool_values) ? true : false;
1252                     break;
1253
1254                 case 'date':
1255                 case 'datetime':
1256                     $query_values[$k] = str_replace('/','-', $this->castAttributeForDatabase($k,$query_values[$k],false));
1257                     break;
1258
1259                 default:
1260                     break;
1261             }
1262         }
1263
1264         $conditions = array($sql);
1265         foreach ($query_values as $bind_value){
1266             $conditions[] = $bind_value;
1267         }
1268         /**
1269         * @todo merge_conditions
1270         */
1271         $options['conditions'] = $conditions;
1272
1273         $result =& call_user_func_array(array(&$this,'find'), array($fetch,$options));
1274         return $result;
1275     }
1276
1277
1278     public function _getFindBySqlAndColumns($find_by_sql, &$query_values)
1279     {
1280         $sql = str_replace(array('(',')','||','|','&&','&','  '),array(' ( ',' ) ',' OR ',' OR ',' AND ',' AND ',' '), $find_by_sql);
1281         $operators = array('AND','and','(',')','&','&&','NOT','<>','OR','|','||');
1282         $pieces = explode(' ',$sql);
1283         $pieces = array_diff($pieces,array(' ',''));
1284         $params = array_diff($pieces,$operators);
1285         $operators = array_diff($pieces,$params);
1286
1287         $new_sql = '';
1288         $parameter_count = 0;
1289         $requested_args = array();
1290         foreach ($pieces as $piece){
1291             if(in_array($piece,$params) && $this->hasColumn($piece)){
1292                 $new_sql .= $piece.' = ? ';
1293                 $requested_args[$parameter_count] = $piece;
1294                 $parameter_count++;
1295             }elseif (!in_array($piece,$operators)){
1296
1297                 if(strstr($piece,':')){
1298                     $_tmp_parts = explode(':',$piece);
1299                     if($this->hasColumn($_tmp_parts[0])){
1300                         $query_values[$parameter_count] = isset($query_values[$parameter_count]) ? $query_values[$parameter_count] : $this->get($_tmp_parts[0]);
1301                         switch (strtolower($_tmp_parts[1])) {
1302                             case 'like':
1303                             case '%like%':
1304                             case 'is':
1305                             case 'has':
1306                             case 'contains':
1307                                 $query_values[$parameter_count] = '%'.$query_values[$parameter_count].'%';
1308                                 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1309                                 break;
1310                             case 'like_left':
1311                             case 'like%':
1312                             case 'begins':
1313                             case 'begins_with':
1314                             case 'starts':
1315                             case 'starts_with':
1316                                 $query_values[$parameter_count] = $query_values[$parameter_count].'%';
1317                                 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1318                                 break;
1319                             case 'like_right':
1320                             case '%like':
1321                             case 'ends':
1322                             case 'ends_with':
1323                             case 'finishes':
1324                             case 'finishes_with':
1325                                 $query_values[$parameter_count] = '%'.$query_values[$parameter_count];
1326                                 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1327                                 break;
1328                             case 'in':
1329                                 $values = join(', ', $this->castAttributesForDatabase($_tmp_parts[0], $query_values[$parameter_count]));
1330                                 if(!empty($values)){
1331                                     $new_sql .= $_tmp_parts[0].' IN ('.$values.') ';
1332                                 }else{
1333                                     $new_sql = preg_replace('/(AND|OR) $/','', $new_sql);
1334                                 }
1335                                 unset($query_values[$parameter_count]);
1336                                 break;
1337                             default:
1338                                 $query_values[$parameter_count] = $query_values[$parameter_count];
1339                                 $new_sql .= $_tmp_parts[0].' '.$_tmp_parts[1].' ? ';
1340                                 break;
1341                         }
1342                         $requested_args[$parameter_count] = $_tmp_parts[0];
1343                         $parameter_count++;
1344                     }else {
1345                         $new_sql .= $_tmp_parts[0];
1346                     }
1347                 }else{
1348                     $new_sql .= $piece.' ';
1349                 }
1350             }else{
1351                 $new_sql .= $piece.' ';
1352             }
1353         }
1354
1355         return array($new_sql, $requested_args);
1356     }
1357
1358
1359     /**
1360      *  Given a condition that uses bindings like "user = ?  AND created_at > ?" will return a
1361      * string replacing the "?" bindings with the column values for current Active Record
1362      *
1363      * @return string
1364      */
1365     public function _getVariableSqlCondition($variable_condition)
1366     {
1367         $query_values = array();
1368         list($sql, $requested_columns) = $this->_getFindBySqlAndColumns($variable_condition, $query_values);
1369         $replacements = array();
1370         $sql = preg_replace('/((('.join($requested_columns,'|').') = \?) = \?)/','$2', $sql);
1371         foreach ($requested_columns as $attribute){
1372             $replacements[$attribute] = $this->castAttributeForDatabase($attribute, $this->get($attribute));
1373         }
1374
1375         return trim(preg_replace('/('.join('|',array_keys($replacements)).')\s+([^\?]+)\s+\?/e', "isset(\$replacements['\\1']) ? '\\1 \\2 '.\$replacements['\\1']:'\\1 \\2 null'", $sql));
1376     }
1377
1378
1379     public function constructFinderSql($options, $select_from_prefix = 'default')
1380     {
1381         $sql = isset($options['select_prefix']) ? $options['select_prefix'] : ($select_from_prefix == 'default' ? 'SELECT '.(!empty($options['joins'])?$this->getTableName().'.':'') .'* FROM '.$this->getTableName() : $select_from_prefix);
1382         $sql .= !empty($options['joins']) ? ' '.$options['joins'] : '';
1383
1384         $this->addConditions($sql, isset($options['conditions']) ? $options['conditions'] : array());
1385
1386         // Create an alias for order
1387         if(empty($options['order']) && !empty($options['sort'])){
1388             $options['order'] = $options['sort'];
1389         }
1390
1391         $sql .= !empty($options['group']) ? ' GROUP BY '.$options['group'] : '';
1392         $sql .= !empty($options['order']) ? ' ORDER BY '.$options['order'] : '';
1393
1394         $this->_db->addLimitAndOffset($sql,$options);
1395
1396         return $sql;
1397     }
1398
1399
1400     /**
1401     * Adds a sanitized version of $conditions to the $sql string. Note that the passed $sql string is changed.
1402     */
1403     public function addConditions(&$sql, $conditions = null, $table_alias = null)
1404     {
1405         if (empty($sql)) {
1406             $concat = '';
1407         }
1408         //if (is_string($conditions) && (stristr($conditions,' WHERE ') || stristr($conditions,'SELECT'))) {
1409         if (is_string($conditions) && (preg_match('/^SELECT.*?WHERE/is',trim($conditions)))) {// || stristr($conditions,'SELECT'))) {
1410             $concat = '';
1411             $sql = $conditions;
1412             $conditions = '';
1413         } else {
1414
1415             $concat = 'WHERE';
1416         }
1417         $concat = empty($sql) ? '' : ' WHERE ';
1418         if (stristr($sql,' WHERE ')) $concat = ' AND ';
1419         if (empty($conditions) && $this->_getDatabaseType() == 'sqlite') $conditions = '1'// sqlite HACK
1420
1421         if($this->getInheritanceColumn() !== false && $this->descendsFromActiveRecord($this)){
1422             $type_condition = $this->typeCondition($table_alias);
1423             if (empty($sql)) {
1424                 $sql .= !empty($type_condition) ? $concat.$type_condition : '';
1425                 $concat = ' AND ';
1426                 if (!empty($conditions)) {
1427                     $conditions = '('.$conditions.')';
1428                 }
1429             } else {
1430                 if (($wherePos=stripos($sql,'WHERE'))!==false) {
1431                     if (!empty($type_condition)) {
1432                         $oldConditions = trim(substr($sql,$wherePos+5));
1433                         $sql = substr($sql,0,$wherePos).' WHERE '.$type_condition.' AND ('.$oldConditions.')';
1434                         $concat = ' AND ';
1435                     }
1436                     if (!empty($conditions)) {
1437                         $conditions = '('.$conditions.')';
1438                     }
1439                 } else {
1440                     if (!empty($type_condition)) {
1441                         //$oldConditions = trim(substr($sql,$wherePos+5));
1442                         $sql = $sql.' WHERE '.$type_condition.'';
1443                         $concat = ' AND ';
1444                     }
1445                     if (!empty($conditions)) {
1446                         $conditions = '('.$conditions.')';
1447                     }
1448                 }
1449
1450             }
1451         }
1452
1453         if(!empty($conditions)){
1454
1455             $sql  .= $concat.$conditions;
1456             $concat = ' AND ';
1457
1458         }
1459
1460
1461         return $sql;
1462     }
1463
1464     /**
1465     * Gets a sanitized version of the input array. Each element will be escaped
1466     */
1467     public function getSanitizedConditionsArray($conditions_array)
1468     {
1469         $result = array();
1470         foreach ($conditions_array as $k=>$v){
1471             $k = str_replace(':','',$k); // Used for Oracle type bindings
1472             if($this->hasColumn($k)){
1473                 $v = $this->castAttributeForDatabase($k, $v);
1474                 $result[$k] = $v;
1475             }
1476         }
1477         return $result;
1478     }
1479
1480
1481     /**
1482     * This functions is used to get the conditions from an AkRequest object
1483     */
1484     public function getConditions($conditions, $prefix = '', $model_name = null)
1485     {
1486         $model_name = isset($model_name) ? $model_name : $this->getModelName();
1487         $model_conditions = !empty($conditions[$model_name]) ? $conditions[$model_name] : $conditions;
1488         if(is_a($this->$model_name)){
1489             $model_instance =& $this->$model_name;
1490         }else{
1491             $model_instance =& $this;
1492         }
1493         $new_conditions = array();
1494         if(is_array($model_conditions)){
1495             foreach ($model_conditions as $col=>$value){
1496                 if($model_instance->hasColumn($col)){
1497                     $new_conditions[$prefix.$col] = $value;
1498                 }
1499             }
1500         }
1501         return $new_conditions;
1502     }
1503
1504     /**
1505     *
1506     * @access private
1507     */
1508     public function _quoteColumnName($column_name)
1509     {
1510         return $this->_db->nameQuote.$column_name.$this->_db->nameQuote;
1511     }
1512
1513
1514     /**
1515     * Finder methods must instantiate through this method to work with the single-table inheritance model and
1516     * eager loading associations.
1517     * that makes it possible to create objects of different types from the same table.
1518     */
1519     public function &instantiate($record, $set_as_new = true, $call_after_instantiate = true)
1520     {
1521         $inheritance_column = $this->getInheritanceColumn();
1522         if(!empty($record[$inheritance_column])){
1523             $inheritance_column = $record[$inheritance_column];
1524             $inheritance_model_name = AkInflector::camelize($inheritance_column);
1525             @require_once(AkInflector::toModelFilename($inheritance_model_name));
1526             if(!class_exists($inheritance_model_name)){
1527                 trigger_error($this->t("The single-table inheritance mechanism failed to locate the subclass: '%class_name'. ".
1528                 "This error is raised because the column '%column' is reserved for storing the class in case of inheritance. ".
1529                 "Please rename this column if you didn't intend it to be used for storing the inheritance class ".
1530                 "or overwrite #{self.to_s}.inheritance_column to use another column for that information.",
1531                 array('%class_name'=>$inheritance_model_name, '%column'=>$this->getInheritanceColumn())),E_USER_ERROR);
1532             }
1533         }
1534
1535         $model_name = isset($inheritance_model_name) ? $inheritance_model_name : $this->getModelName();
1536         $object = new $model_name('attributes', $record);
1537
1538         $object->_newRecord = $set_as_new;
1539
1540         if ($call_after_instantiate) {
1541             $object->afterInstantiate();
1542             $object->notifyObservers('afterInstantiate');
1543         }
1544         (AK_CLI && AK_ENVIRONMENT == 'development') ? $object ->toString() : null;
1545
1546         return $object;
1547     }
1548
1549     /*/Finding records*/
1550
1551
1552
1553     /**
1554                            Table inheritance
1555      ====================================================================
1556      */
1557     public function descendsFromActiveRecord(&$object)
1558     {
1559         if(substr(strtolower(get_parent_class($object)),-12) == 'activerecord'){
1560             return true;
1561         }
1562         if(!method_exists($object, 'getInheritanceColumn')){
1563             return false;
1564         }
1565         $inheritance_column = $object->getInheritanceColumn();
1566         return !empty($inheritance_column);
1567     }
1568
1569     /**
1570      * Gets the column name for use with single table inheritance. Can be overridden in subclasses.
1571     */
1572     public function getInheritanceColumn()
1573     {
1574         return empty($this->_inheritanceColumn) ? ($this->hasColumn('type') ? 'type' : false ) : $this->_inheritanceColumn;
1575     }
1576
1577     /**
1578      * Defines the column name for use with single table inheritance. Can be overridden in subclasses.
1579      */
1580     public function setInheritanceColumn($column_name)
1581     {
1582         if(!$this->hasColumn($column_name)){
1583             trigger_error(Ak::t('Could not set "%column_name" as the inheritance column as this column is not available on the database.',array('%column_name'=>$column_name)), E_USER_NOTICE);
1584             return false;
1585         }elseif($this->getColumnType($column_name) != 'string'){
1586             trigger_error(Ak::t('Could not set %column_name as the inheritance column as this column type is "%column_type" instead of "string".',array('%column_name'=>$column_name,'%column_type'=>$this->getColumnType($column_name))), E_USER_NOTICE);
1587             return false;
1588         }else{
1589             $this->_inheritanceColumn = $column_name;
1590             return true;
1591         }
1592     }
1593
1594
1595     public function getSubclasses()
1596     {
1597         $current_class = get_class($this);
1598         $subclasses = array();
1599         $classes = get_declared_classes();
1600
1601         while ($class = array_shift($classes)) {
1602             $parent_class = get_parent_class($class);
1603             if($parent_class == $current_class || in_array($parent_class,$subclasses)){
1604                 $subclasses[] = $class;
1605             }elseif(!empty($parent_class)){
1606                 $classes[] = $parent_class;
1607             }
1608         }
1609         $subclasses = array_unique(array_map(array(&$this,'_getModelName'),$subclasses));
1610         return $subclasses;
1611     }
1612
1613
1614     public function typeCondition($table_alias = null)
1615     {
1616         $inheritance_column = $this->getInheritanceColumn();
1617         $type_condition = array();
1618         $table_name = $this->getTableName();
1619         $available_types = array_merge(array($this->getModelName()),$this->getSubclasses());
1620         foreach ($available_types as $subclass){
1621             $type_condition[] = ' '.($table_alias != null ? $table_alias : $table_name).'.'.$inheritance_column.' = \''.AkInflector::humanize(AkInflector::underscore($subclass)).'\' ';
1622         }
1623         return empty($type_condition) ? '' : '('.join('OR',$type_condition).') ';
1624     }
1625
1626     /*/Table inheritance*/
1627
1628
1629
1630     /**
1631                          Setting Attributes
1632     ====================================================================
1633     See also: Getting Attributes, Model Attributes, Toggling Attributes, Counting Attributes.
1634     */
1635     public function setAttribute($attribute, $value, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS, $compose_after_set = true)
1636     {
1637         if($attribute[0] == '_'){
1638             return false;
1639         }
1640
1641         if($this->isFrozen()){
1642             return false;
1643         }
1644         if($inspect_for_callback_child_method === true && method_exists($this,'set'.AkInflector::camelize($attribute))){
1645             static $watchdog;
1646             $watchdog[$attribute] = @$watchdog[$attribute]+1;
1647             if($watchdog[$attribute] == 5000){
1648                 if((!defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION')) || defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_SET_RECURSION){
1649                     trigger_error(Ak::t('You are calling recursively AkActiveRecord::setAttribute by placing parent::setAttribute() or  parent::set() on your model "%method" method. In order to avoid this, set the 3rd paramenter of parent::setAttribute to FALSE. If this was the behaviour you expected, please define the constant AK_ACTIVE_RECORD_PROTECT_SET_RECURSION and set it to false',array('%method'=>'set'.AkInflector::camelize($attribute))),E_USER_ERROR);
1650                     return false;
1651                 }
1652             }
1653             $this->{$attribute.'_before_type_cast'} = $value;
1654             return $this->{'set'.AkInflector::camelize($attribute)}($value);
1655         }
1656         if($this->hasAttribute($attribute)){
1657             $this->{$attribute.'_before_type_cast'} = $value;
1658             $this->$attribute = $value;
1659             if($compose_after_set && !empty($this->_combinedAttributes) && !$this->requiredForCombination($attribute)){
1660                 $combined_attributes = $this->_getCombinedAttributesWhereThisAttributeIsUsed($attribute);
1661                 foreach ($combined_attributes as $combined_attribute){
1662                     $this->composeCombinedAttribute($combined_attribute);
1663                 }
1664             }
1665             if ($compose_after_set && $this->isCombinedAttribute($attribute)){
1666                 $this->decomposeCombinedAttribute($attribute);
1667             }
1668         }elseif(substr($attribute,-12) == 'confirmation' && $this->hasAttribute(substr($attribute,0,-13))){
1669             $this->$attribute = $value;
1670         }
1671
1672         if($this->_internationalize){
1673             if(is_array($value)){
1674                 $this->setAttributeLocales($attribute, $value);
1675             }elseif(is_string($inspect_for_callback_child_method)){
1676                 $this->setAttributeByLocale($attribute, $value, $inspect_for_callback_child_method);
1677             }else{
1678                 $this->_groupInternationalizedAttribute($attribute, $value);
1679             }
1680         }
1681         return true;
1682     }
1683
1684     public function set($attribute, $value = null, $inspect_for_callback_child_method = true, $compose_after_set = true)
1685     {
1686         if(is_array($attribute)){
1687             return $this->setAttributes($attribute);
1688         }
1689         return $this->setAttribute($attribute, $value, $inspect_for_callback_child_method, $compose_after_set);
1690     }
1691
1692     /**
1693     * Allows you to set all the attributes at once by passing in an array with
1694     * keys matching the attribute names (which again matches the column names).
1695     * Sensitive attributes can be protected from this form of mass-assignment by
1696     * using the $this->setProtectedAttributes method. Or you can alternatively
1697     * specify which attributes can be accessed in with the $this->setAccessibleAttributes method.
1698     * Then all the attributes not included in that won?t be allowed to be mass-assigned.
1699     */
1700     public function setAttributes($attributes, $override_attribute_protection = false)
1701     {
1702         $this->parseAkelosArgs($attributes);
1703         if(!$override_attribute_protection){
1704             $attributes = $this->removeAttributesProtectedFromMassAssignment($attributes);
1705         }
1706         if(!empty($attributes) && is_array($attributes)){
1707             foreach ($attributes as $k=>$v){
1708                 $this->setAttribute($k, $v);
1709             }
1710         }
1711     }
1712
1713
1714     public function setId($value)
1715     {
1716         if($this->isFrozen()){
1717             return false;
1718         }
1719         $pk = $this->getPrimaryKey();
1720         $this->$pk = $value;
1721         return true;
1722     }
1723
1724
1725     /*/Setting Attributes*/
1726
1727     /**
1728                          Getting Attributes
1729     ====================================================================
1730     See also: Setting Attributes, Model Attributes, Toggling Attributes, Counting Attributes.
1731     */
1732
1733     public function getAttribute($attribute, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS)
1734     {
1735         if($attribute[0] == '_'){
1736             return false;
1737         }
1738
1739         if($inspect_for_callback_child_method === true && method_exists($this,'get'.AkInflector::camelize($attribute))){
1740             static $watchdog;
1741             $watchdog[@$attribute] = @$watchdog[$attribute]+1;
1742             if($watchdog[$attribute] == 5000){
1743                 if((!defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION')) || defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_GET_RECURSION){
1744                     trigger_error(Ak::t('You are calling recursivelly AkActiveRecord::getAttribute by placing parent::getAttribute() or  parent::get() on your model "%method" method. In order to avoid this, set the 2nd paramenter of parent::getAttribute to FALSE. If this was the behaviour you expected, please define the constant AK_ACTIVE_RECORD_PROTECT_GET_RECURSION and set it to false',array('%method'=>'get'.AkInflector::camelize($attribute))),E_USER_ERROR);
1745                     return false;
1746                 }
1747             }
1748             $value = $this->{'get'.AkInflector::camelize($attribute)}();
1749             return $this->getInheritanceColumn() === $attribute ? AkInflector::humanize(AkInflector::underscore($value)) : $value;
1750         }
1751         if(isset($this->$attribute) || (!isset($this->$attribute) && $this->isCombinedAttribute($attribute))){
1752             if($this->hasAttribute($attribute)){
1753                 if (!empty($this->_combinedAttributes) && $this->isCombinedAttribute($attribute)){
1754                     $this->composeCombinedAttribute($attribute);
1755                 }
1756                 return isset($this->$attribute) ? $this->$attribute : null;
1757             }elseif($this->_internationalize && $this->_isInternationalizeCandidate($attribute)){
1758                 if(!empty($this->$attribute) && is_string($this->$attribute)){
1759                     return $this->$attribute;
1760                 }
1761                 $current_locale = $this->getCurrentLocale();
1762                 if(!empty($this->$attribute[$current_locale]) && is_array($this->$attribute)){
1763                     return $this->$attribute[$current_locale];
1764                 }
1765                 return $this->getAttribute($current_locale.'_'.$attribute);
1766             }
1767         }
1768
1769         if($this->_internationalize){
1770             return $this->getAttributeByLocale($attribute, is_bool($inspect_for_callback_child_method) ? $this->getCurrentLocale() : $inspect_for_callback_child_method);
1771         }
1772         return null;
1773     }
1774
1775     public function get($attribute = null, $inspect_for_callback_child_method = true)
1776     {
1777         return !isset($attribute) ? $this->getAttributes($inspect_for_callback_child_method) : $this->getAttribute($attribute, $inspect_for_callback_child_method);
1778     }
1779
1780     /**
1781     * Returns an array of all the attributes with their names as keys and clones of their objects as values in case they are objects.
1782     */
1783     public function getAttributes()
1784     {
1785         $attributes = array();
1786         $available_attributes = $this->getAvailableAttributes();
1787         foreach ($available_attributes as $available_attribute){
1788             $attribute = $this->getAttribute($available_attribute['name']);
1789             $attributes[$available_attribute['name']] = AK_PHP5 && is_object($attribute) ? clone($attribute) : $attribute;
1790         }
1791
1792         if($this->_internationalize){
1793             $current_locale = $this->getCurrentLocale();
1794             foreach ($this->getInternationalizedColumns() as $column=>$languages){
1795                 if(empty($attributes[$column]) && isset($attributes[$current_locale.'_'.$column]) && in_array($current_locale,$languages)){
1796                     $attributes[$column] = $attributes[$current_locale.'_'.$column];
1797                 }
1798             }
1799         }
1800
1801         return $attributes;
1802     }
1803
1804
1805     /**
1806     * Every Active Record class must use "id" as their primary ID. This getter overwrites the native id method, which isn't being used in this context.
1807     */
1808     public function getId()
1809     {
1810         $pk=$this->getPrimaryKey();
1811         if(empty($pk)) {
1812             debug_print_backtrace();
1813         }
1814         return $this->{$pk};
1815     }
1816
1817     /*/Getting Attributes*/
1818
1819
1820
1821     /**
1822                          Toggling Attributes
1823     ====================================================================
1824     See also: Setting Attributes, Getting Attributes.
1825     */
1826     /**
1827     * Turns an attribute that's currently true into false and vice versa. Returns attribute value.
1828     */
1829     public function toggleAttribute($attribute)
1830     {
1831         $value = $this->getAttribute($attribute);
1832         $new_value = $value ? false : true;
1833         $this->setAttribute($attribute, $new_value);
1834         return $new_value;
1835     }
1836
1837
1838     /**
1839     * Toggles the attribute and saves the record.
1840     */
1841     public function toggleAttributeAndSave($attribute)
1842     {
1843         $value = $this->toggleAttribute($attribute);
1844         if($this->updateAttribute($attribute, $value)){
1845             return $value;
1846         }
1847         return null;
1848     }
1849
1850     /*/Toggling Attributes*/
1851
1852
1853     /**
1854                          Counting Attributes
1855     ====================================================================
1856     See also: Counting Records, Setting Attributes, Getting Attributes.
1857     */
1858
1859     /**
1860     * Increments the specified counter by one. So $DiscussionBoard->incrementCounter("post_count",
1861     * $discussion_board_id); would increment the "post_count" counter on the board responding to
1862     * $discussion_board_id. This is used for caching aggregate values, so that they doesn't need to
1863     * be computed every time. Especially important for looping over a collection where each element
1864     * require a number of aggregate values. Like the $DiscussionBoard that needs to list both the number of posts and comments.
1865     */
1866     public function incrementCounter($counter_name, $id, $difference = 1)
1867     {
1868         return $this->updateAll("$counter_name = $counter_name + $difference", $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 1;
1869     }
1870
1871     /**
1872     * Works like AkActiveRecord::incrementCounter, but decrements instead.
1873     */
1874     public function decrementCounter($counter_name, $id, $difference = 1)
1875     {
1876         return $this->updateAll("$counter_name = $counter_name - $difference", $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 1;
1877     }
1878
1879     /**
1880     * Initializes the attribute to zero if null and subtracts one. Only makes sense for number-based attributes. Returns attribute value.
1881     */
1882     public function decrementAttribute($attribute)
1883     {
1884         if(!isset($this->$attribute)){
1885             $this->$attribute = 0;
1886         }
1887         return $this->$attribute -= 1;
1888     }
1889
1890     /**
1891     * Decrements the attribute and saves the record.
1892     */
1893     public function decrementAndSaveAttribute($attribute)
1894     {
1895         return $this->updateAttribute($attribute,$this->decrementAttribute($attribute));
1896     }
1897
1898
1899     /**
1900     * Initializes the attribute to zero if null and adds one. Only makes sense for number-based attributes. Returns attribute value.
1901     */
1902     public function incrementAttribute($attribute)
1903     {
1904         if(!isset($this->$attribute)){
1905             $this->$attribute = 0;
1906         }
1907         return $this->$attribute += 1;
1908     }
1909
1910     /**
1911     * Increments the attribute and saves the record.
1912     */
1913     public function incrementAndSaveAttribute($attribute)
1914     {
1915         return $this->updateAttribute($attribute,$this->incrementAttribute($attribute));
1916     }
1917
1918     /*/Counting Attributes*/
1919
1920     /**
1921                          Protecting attributes
1922     ====================================================================
1923     */
1924
1925     /**
1926     * If this macro is used, only those attributed named in it will be accessible
1927     * for mass-assignment, such as new ModelName($attributes) and $this->attributes($attributes).
1928     * This is the more conservative choice for mass-assignment protection.
1929     * If you'd rather start from an all-open default and restrict attributes as needed,
1930     * have a look at AkActiveRecord::setProtectedAttributes().
1931     */
1932     public function setAccessibleAttributes()
1933     {
1934         $args = func_get_args();
1935         $this->_accessibleAttributes = array_unique(array_merge((array)$this->_accessibleAttributes, $args));
1936     }
1937
1938     /**
1939      * Attributes named in this macro are protected from mass-assignment, such as
1940      * new ModelName($attributes) and $this->attributes(attributes). Their assignment
1941      * will simply be ignored. Instead, you can use the direct writer methods to do assignment.
1942      * This is meant to protect sensitive attributes to be overwritten by URL/form hackers.
1943      *
1944      * Example:
1945      * <code>
1946      *   class Customer extends ActiveRecord
1947      *    {
1948      *      public function Customer()
1949      *      {
1950      *          $this->setProtectedAttributes('credit_rating');
1951      *      }
1952      *    }
1953      *
1954      *    $Customer = new Customer('name' => 'David', 'credit_rating' => 'Excellent');
1955      *    $Customer->credit_rating // => null
1956      *    $Customer->attributes(array('description' => 'Jolly fellow', 'credit_rating' => 'Superb'));
1957      *    $Customer->credit_rating // => null
1958      *
1959      *    $Customer->credit_rating = 'Average'
1960      *    $Customer->credit_rating // => 'Average'
1961      *  </code>
1962      */
1963     public function setProtectedAttributes()
1964     {
1965         $args = func_get_args();
1966         $this->_protectedAttributes = array_unique(array_merge((array)$this->_protectedAttributes, $args));
1967     }
1968
1969     public function removeAttributesProtectedFromMassAssignment($attributes)
1970     {
1971         if(!empty($this->_accessibleAttributes) && is_array($this->_accessibleAttributes) &&  is_array($attributes)){
1972             foreach (array_keys($attributes) as $k){
1973                 if(!in_array($k,$this->_accessibleAttributes)){
1974                     unset($attributes[$k]);
1975                 }
1976             }
1977         }elseif (!empty($this->_protectedAttributes) && is_array($this->_protectedAttributes) &&  is_array($attributes)){
1978             foreach (array_keys($attributes) as $k){
1979                 if(in_array($k,$this->_protectedAttributes)){
1980                     unset($attributes[$k]);
1981                 }
1982             }
1983         }
1984         return $attributes;
1985     }
1986
1987     /*/Protecting attributes*/
1988
1989
1990     /**
1991                           Model Attributes
1992      ====================================================================
1993      See also: Getting Attributes, Setting Attributes.
1994      */
1995
1996     public function getAvailableAttributes()
1997     {
1998         return array_merge($this->getColumns(), $this->getAvailableCombinedAttributes());
1999     }
2000
2001     public function getAttributeCaption($attribute)
2002     {
2003         return $this->t(AkInflector::humanize($attribute));
2004     }
2005
2006     /**
2007      * This function is useful in case you need to know if attributes have been assigned to an object.
2008      */
2009     public function hasAttributesDefined()
2010     {
2011         $attributes = join('',$this->getAttributes());
2012         return empty($attributes);
2013     }
2014
2015
2016     /**
2017     * Returns the primary key field.
2018     */
2019     public function getPrimaryKey()
2020     {
2021         if(!isset($this->_primaryKey)){
2022             $this->setPrimaryKey();
2023         }
2024         return $this->_primaryKey;
2025     }
2026
2027     public function getColumnNames()
2028     {
2029         if(empty($this->_columnNames)){
2030             $columns = $this->getColumns();
2031             foreach ($columns as $column_name=>$details){
2032                 $this->_columnNames[$column_name] = isset($details->columnName) ? $this->t($details->columnName) : $this->getAttributeCaption($column_name);
2033             }
2034         }
2035         return $this->_columnNames;
2036     }
2037
2038
2039     /**
2040     * Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count",
2041     * and columns used for single table inheritance has been removed.
2042     */
2043     public function getContentColumns()
2044     {
2045         $inheritance_column = $this->getInheritanceColumn();
2046         $columns = $this->getColumns();
2047         foreach ($columns as $name=>$details){
2048             if((substr($name,-3) == '_id' || substr($name,-6) == '_count') ||
2049             !empty($details['primaryKey']) || ($inheritance_column !== false && $inheritance_column == $name)){
2050                 unset($columns[$name]);
2051             }
2052         }
2053         return $columns;
2054     }
2055
2056     /**
2057     * Returns an array of names for the attributes available on this object sorted alphabetically.
2058     */
2059     public function getAttributeNames()
2060     {
2061         $attributes = array_keys($this->getAvailableAttributes());
2062         $names = array_combine($attributes,array_map(array(&$this,'getAttributeCaption'), $attributes));
2063         natsort($names);
2064         return $names;
2065     }
2066
2067
2068     /**
2069     * Returns true if the specified attribute has been set by the user or by a database load and is neither null nor empty?
2070     */
2071     public function isAttributePresent($attribute)
2072     {
2073         $value = $this->getAttribute($attribute);
2074         return !empty($value);
2075     }
2076
2077     /**
2078     * Returns true if given attribute exists for this Model.
2079     *
2080     * @param string $attribute
2081     * @return boolean
2082     */
2083     public function hasAttribute ($attribute)
2084     {
2085         empty($this->_columns) ? $this->getColumns() : $this->_columns; // HINT: only used by HasAndBelongsToMany joinObjects, if the table is not present yet!
2086         return isset($this->_columns[$attribute]) || (!empty($this->_combinedAttributes) && $this->isCombinedAttribute($attribute));
2087     }
2088
2089     /*/Model Attributes*/
2090
2091
2092     /**
2093                           Combined attributes
2094     ====================================================================
2095     *
2096     * The Akelos Framework has a handy way to represent combined fields.
2097     * You can add a new attribute to your models using a printf patter to glue
2098     * multiple parameters in a single one.
2099     *
2100     * For example, If we set...
2101     * $this->addCombinedAttributeConfiguration('name', "%s %s", 'first_name', 'last_name');
2102     * $this->addCombinedAttributeConfiguration('date', "%04d-%02d-%02d", 'year', 'month', 'day');
2103     * $this->setAttributes('first_name=>','John','last_name=>','Smith','year=>',2005,'month=>',9,'day=>',27);
2104     *
2105     * $this->name // will have "John Smith" as value and
2106     * $this->date // will be 2005-09-27
2107     *
2108     * On the other hand if you do
2109     *
2110     *   $this->setAttribute('date', '2008-11-30');
2111     *
2112     *   All the 'year', 'month' and 'day' getters will be fired (if they exist) the following attributes will be set
2113     *
2114     *    $this->year // will be 2008
2115     *    $this->month // will be 11 and
2116     *    $this->day // will be 27
2117     *
2118     * Sometimes you might need a pattern for composing and another for decomposing attributes. In this case you can specify
2119     * an array as the pattern values, where first element will be the composing pattern and second element will be used
2120     * for decomposing.
2121     *
2122     * You can also specify a callback method from this object function instead of a pattern. You can also assign a callback
2123     * for composing and another for decomposing by passing their names as an array like on the patterns.
2124     *
2125     *    <?php
2126     *    class User extends ActiveRecord
2127     *    {
2128     *        public function User()
2129     *        {
2130     *            // You can use a multiple patterns array where "%s, %s" will be used for combining fields and "%[^,], %s" will be used
2131     *            // for decomposing fields. (as you can see you can also use regular expressions on your patterns)
2132     *            $User->addCombinedAttributeConfiguration('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name');
2133     *
2134     *            //Here we set email_link so compose_email_link() will be triggered for building up the field and parse_email_link will
2135     *            // be used for getting the fields out
2136     *            $User->addCombinedAttributeConfiguration('email_link', array("compose_email_link","parse_email_link"), 'email', 'name');
2137     *
2138     *            // We need to tell the ActiveRecord to load it's magic (see the example below for a simpler solution)
2139     *            $attributes = (array)func_get_args();
2140     *            return $this->init($attributes);
2141     *
2142     *        }
2143     *        public function compose_email_link()
2144     *        {
2145     *            $args = func_get_arg(0);
2146     *            return "<a href=\'mailto:{$args[\'email\']}\'>{$args[\'name\']}</a>";
2147     *        }
2148     *        public function parse_email_link($email_link)
2149     *        {
2150     *            $results = sscanf($email_link, "<a href=\'mailto:%[^\']\'>%[^<]</a>");
2151     *            return array(\'email\'=>$results[0],\'name\'=>$results[1]);
2152     *        }
2153     *
2154     *    }
2155     *   ?>
2156     *
2157     * You can also simplify your live by declaring the combined attributes as a class variable like:
2158     *    <?php
2159     *    class User extends ActiveRecord
2160     *    {
2161     *       public $combined_attributes array(
2162     *       array('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name')
2163     *       array('email_link', array("compose_email_link","parse_email_link"), 'email', 'name')
2164     *       );
2165     *
2166     *       // ....
2167     *    }
2168     *   ?>
2169     *
2170     */
2171
2172     /**
2173     * Returns true if given attribute is a combined attribute for this Model.
2174     *
2175     * @param string $attribute
2176     * @return boolean
2177     */
2178     public function isCombinedAttribute ($attribute)
2179     {
2180         return !empty($this->_combinedAttributes) && isset($this->_combinedAttributes[$attribute]);
2181     }
2182
2183     public function addCombinedAttributeConfiguration($attribute)
2184     {
2185         $args = is_array($attribute) ? $attribute : func_get_args();
2186         $columns = array_slice($args,2);
2187         $invalid_columns = array();
2188         foreach ($columns as $colum){
2189             if(!$this->hasAttribute($colum)){
2190                 $invalid_columns[] = $colum;
2191             }
2192         }
2193         if(!empty($invalid_columns)){
2194             trigger_error(Ak::t('There was an error while setting the composed field "%field_name", the following mapping column/s "%columns" do not exist',
2195             array('%field_name'=>$args[0],'%columns'=>join(', ',$invalid_columns))), E_USER_ERROR);
2196         }else{
2197             $attribute = array_shift($args);
2198             $this->_combinedAttributes[$attribute] = $args;
2199             $this->composeCombinedAttribute($attribute);
2200         }
2201     }
2202
2203     public function composeCombinedAttributes()
2204     {
2205
2206         if(!empty($this->_combinedAttributes)){
2207             $attributes = array_keys($this->_combinedAttributes);
2208             foreach ($attributes as $attribute){
2209                 $this->composeCombinedAttribute($attribute);
2210             }
2211         }
2212     }
2213
2214     public function composeCombinedAttribute($combined_attribute)
2215     {
2216         if($this->isCombinedAttribute($combined_attribute)){
2217             $config = $this->_combinedAttributes[$combined_attribute];
2218             $pattern = array_shift($config);
2219
2220             $pattern = is_array($pattern) ? $pattern[0] : $pattern;
2221             $got = array();
2222
2223             foreach ($config as $attribute){
2224                 if(isset($this->$attribute)){
2225                     $got[$attribute] = $this->getAttribute($attribute);
2226                 }
2227             }
2228             if(count($got) === count($config)){
2229                 $this->$combined_attribute = method_exists($this, $pattern) ? $this->{$pattern}($got) : vsprintf($pattern, $got);
2230             }
2231         }
2232     }
2233
2234     /**
2235     * @access private
2236     */
2237     public function _getCombinedAttributesWhereThisAttributeIsUsed($attribute)
2238     {
2239         $result = array();
2240         foreach ($this->_combinedAttributes as $combined_attribute=>$settings){
2241             if(in_array($attribute,$settings)){
2242                 $result[] = $combined_attribute;
2243             }
2244         }
2245         return $result;
2246     }
2247
2248
2249     public function requiredForCombination($attribute)
2250     {
2251         foreach ($this->_combinedAttributes as $settings){
2252             if(in_array($attribute,$settings)){
2253                 return true;
2254             }
2255         }
2256         return false;
2257     }
2258
2259     public function hasCombinedAttributes()
2260     {
2261         return count($this->getCombinedSubattributes()) === 0 ? false :true;
2262     }
2263
2264     public function getCombinedSubattributes($attribute)
2265     {
2266         $result = array();
2267         if(is_array($this->_combinedAttributes[$attribute])){
2268             $attributes = $this->_combinedAttributes[$attribute];
2269             array_shift($attributes);
2270             foreach ($attributes as $attribute_to_check){
2271                 if(isset($this->_combinedAttributes[$attribute_to_check])){
2272                     $result[] = $attribute_to_check;
2273                 }
2274             }
2275         }
2276         return $result;
2277     }
2278
2279     public function decomposeCombinedAttributes()
2280     {
2281         if(!empty($this->_combinedAttributes)){
2282             $attributes = array_keys($this->_combinedAttributes);
2283             foreach ($attributes as $attribute){
2284                 $this->decomposeCombinedAttribute($attribute);
2285             }
2286         }
2287     }
2288
2289     public function decomposeCombinedAttribute($combined_attribute, $used_on_combined_fields = false)
2290     {
2291         if(isset($this->$combined_attribute) && $this->isCombinedAttribute($combined_attribute)){
2292             $config = $this->_combinedAttributes[$combined_attribute];
2293             $pattern = array_shift($config);
2294             $pattern = is_array($pattern) ? $pattern[1] : $pattern;
2295
2296             if(method_exists($this, $pattern)){
2297                 $pieces = $this->{$pattern}($this->$combined_attribute);
2298                 if(is_array($pieces)){
2299                     foreach ($pieces as $k=>$v){
2300                         $is_combined = $this->isCombinedAttribute($k);
2301                         if($is_combined){
2302                             $this->decomposeCombinedAttribute($k);
2303                         }
2304                         $this->setAttribute($k, $v, true, !$is_combined);
2305                     }
2306                     if($is_combined && !$used_on_combined_fields){
2307                         $combined_attributes_contained_on_this_attribute = $this->getCombinedSubattributes($combined_attribute);
2308                         if(count($combined_attributes_contained_on_this_attribute)){
2309                             $this->decomposeCombinedAttribute($combined_attribute, true);
2310                         }
2311                     }
2312                 }
2313             }else{
2314                 $got = sscanf($this->$combined_attribute, $pattern);
2315                 for ($x=0; $x<count($got); $x++){
2316                     $attribute = $config[$x];
2317                     $is_combined = $this->isCombinedAttribute($attribute);
2318                     if($is_combined){
2319                         $this->decomposeCombinedAttribute($attribute);
2320                     }
2321                     $this->setAttribute($attribute, $got[$x], true, !$is_combined);
2322                 }
2323             }
2324         }
2325     }
2326
2327     public function getAvailableCombinedAttributes()
2328     {
2329         $combined_attributes = array();
2330         foreach ($this->_combinedAttributes as $attribute=>$details){
2331             $combined_attributes[$attribute] = array('name'=>$attribute, 'type'=>'string', 'path' => array_shift($details), 'uses'=>$details);
2332         }
2333         return !empty($this->_combinedAttributes) && is_array($this->_combinedAttributes) ? $combined_attributes : array();
2334     }
2335
2336     /*/Combined attributes*/
2337
2338
2339
2340
2341     /**
2342                          Database connection
2343     ====================================================================
2344     */
2345     /**
2346     * Establishes the connection to the database. Accepts either a profile name specified in config/config.php or
2347     * an array as input where the 'type' key must be specified with the name of a database adapter (in lower-case)
2348     * example for regular databases (MySQL, Postgresql, etc):
2349     *
2350     *   $AkActiveRecord->establishConnection('development');
2351     *   $AkActiveRecord->establishConnection('super_user');
2352     *
2353     *   $AkActiveRecord->establishConnection(
2354     *       array(
2355     *       'type'  => "mysql",
2356     *       'host'     => "localhost",
2357     *       'username' => "myuser",
2358     *       'password' => "mypass",
2359     *       'database' => "somedatabase"
2360     *       ));
2361     *
2362     *    Example for SQLite database:
2363     *
2364     *     $AkActiveRecord->establishConnection(
2365     *       array(
2366     *       'type' => "sqlite",
2367     *       'dbfile'  => "path/to/dbfile"
2368     *       )
2369     *     )
2370     */
2371     public function &establishConnection($specification_or_profile = AK_DEFAULT_DATABASE_PROFILE)
2372     {
2373         $adapter =& AkDbAdapter::getInstance($specification_or_profile);
2374         return $this->setConnection(&$adapter);
2375     }
2376
2377
2378     /**
2379     * Returns true if a connection that's accessible to this class have already been opened.
2380     */
2381     public function isConnected()
2382     {
2383         return isset($this->_db);
2384     }
2385
2386     /**
2387     * Returns the connection currently associated with the class. This can also be used to
2388     * "borrow" the connection to do database work unrelated to any of the specific Active Records.
2389     */
2390     public function &getConnection()
2391     {
2392         return $this->_db;
2393     }
2394
2395     /**
2396     * Sets the connection for the class.
2397     */
2398     public function &setConnection($db_adapter = null)
2399     {
2400         if (is_null($db_adapter)){
2401             $db_adapter =& AkDbAdapter::getInstance();
2402         }
2403         return $this->_db =& $db_adapter;
2404     }
2405
2406     /**
2407     * @access private
2408     */
2409     public function _getDatabaseType()
2410     {
2411         return $this->_db->type();
2412     }
2413     /*/Database connection*/
2414
2415
2416     /**
2417                            Table Settings
2418     ====================================================================
2419     See also: Database Reflection.
2420     */
2421
2422     /**
2423     * Defines the primary key field ? can be overridden in subclasses.
2424     */
2425     public function setPrimaryKey($primary_key = 'id')
2426     {
2427         if(!$this->hasColumn($primary_key)){
2428             trigger_error($this->t('Opps! We could not find primary key column %primary_key on the table %table, for the model %model',array('%primary_key'=>$primary_key,'%table'=>$this->getTableName(), '%model'=>$this->getModelName())),E_USER_ERROR);
2429         }else {
2430             $this->_primaryKey = $primary_key;
2431         }
2432     }
2433
2434
2435     public function getTableName($modify_for_associations = true)
2436     {
2437         if(!isset($this->_tableName)){
2438             // We check if we are on a inheritance Table Model
2439             $this->getClassForDatabaseTableMapping();
2440             if(!isset($this->_tableName)){
2441                 $this->setTableName();
2442             }
2443         }
2444
2445         if($modify_for_associations && isset($this->_associationTablePrefixes[$this->_tableName])){
2446             return $this->_associationTablePrefixes[$this->_tableName];
2447         }
2448
2449         return $this->_tableName;
2450     }
2451
2452     public function setTableName($table_name = null, $check_for_existence = AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES, $check_mode = false)
2453     {
2454         if(empty($table_name)){
2455             $table_name = AkInflector::tableize($this->getModelName());
2456         }
2457         if($check_for_existence){
2458             !isset($this->_db) && $this->establishConnection();
2459             if(!$this->_db->tableExists($table_name, true)){
2460                 if(!$check_mode){
2461                     trigger_error(Ak::t('Unable to set "%table_name" table for the model "%model".'.
2462                     '  There is no "%table_name" available into current database layout.'.
2463                     ' Set AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES constant to false in order to'.
2464                     ' avoid table name validation',array('%table_name'=>$table_name,'%model'=>$this->getModelName())),E_USER_WARNING);
2465                 }
2466                 return false;
2467             }
2468         }
2469         $this->_tableName = $table_name;
2470         return true;
2471     }
2472
2473
2474     public function getOnlyAvailableAttributes($attributes)
2475     {
2476         $table_name = $this->getTableName();
2477         $ret_attributes = array();
2478         if(!empty($attributes) && is_array($attributes)){
2479             $available_attributes = $this->getAvailableAttributes();
2480
2481             $keys = array_keys($attributes);
2482             $size = sizeOf($keys);
2483             for ($i=0; $i < $size; $i++){
2484                 $k = str_replace($table_name.'.','',$keys[$i]);
2485                 if(isset($available_attributes[$k]['name'][$k])){
2486                     $ret_attributes[$k] =& $attributes[$keys[$i]];
2487                 }
2488             }
2489         }
2490         return $ret_attributes;
2491     }
2492
2493     public function getColumnsForAttributes($attributes)
2494     {
2495         $ret_attributes = array();
2496         $table_name = $this->getTableName();
2497         if(!empty($attributes) && is_array($attributes)){
2498             $columns = $this->getColumns();
2499             foreach ($attributes as $k=>$v){
2500                 $k = str_replace($table_name.'.','',$k);
2501                 if(isset($columns[$k]['name'][$k])){
2502                     $ret_attributes[$k] = $v;
2503                 }
2504             }
2505         }
2506         return $ret_attributes;
2507     }
2508
2509     /**
2510     * Returns true if given attribute exists for this Model.
2511     *
2512     * @param string $name Name of table to look in
2513     * @return boolean
2514     */
2515     public function hasColumn($column)
2516     {
2517         empty($this->_columns) ? $this->getColumns() : $this->_columns;
2518         return isset($this->_columns[$column]);
2519     }
2520
2521
2522     /*/Table Settings*/
2523
2524     /**
2525                            Database Reflection
2526     ====================================================================
2527     See also: Table Settings, Type Casting.
2528     */
2529
2530
2531     /**
2532     * Initializes the attributes array with keys matching the columns from the linked table and
2533     * the values matching the corresponding default value of that column, so
2534     * that a new instance, or one populated from a passed-in array, still has all the attributes
2535     * that instances loaded from the database would.
2536     */
2537     public function attributesFromColumnDefinition()
2538     {
2539         $attributes = array();
2540
2541         foreach ((array)$this->getColumns() as $column_name=>$column_settings){
2542             if (!isset($column_settings['primaryKey']) && isset($column_settings['hasDefault'])) {
2543                 $attributes[$column_name] = $this->_extractValueFromDefault($column_settings['defaultValue']);
2544             } else {
2545                 $attributes[$column_name] = null;
2546             }
2547         }
2548         return $attributes;
2549     }
2550
2551     /**
2552      * Gets information from the database engine about a single table
2553      *
2554      * @access private
2555      */
2556     public function _databaseTableInternals($table)
2557     {
2558         if (!$cache = AkDbSchemaCache::get('table_internals_for_'.$table)) {
2559             $cache = $this->_db->getColumnDetails($table);
2560             AkDbSchemaCache::set('table_internals_for_'.$table, $cache);
2561         }
2562         return $cache;
2563     }
2564     public function getColumnsWithRegexBoundariesAndAlias($alias)
2565     {
2566         $columns = array_keys($this->getColumns());
2567         foreach ($columns as $k=>$column){
2568             $columns[$k] = '/([^_])\b('.$alias.')\.('.$column.')\b/';
2569         }
2570         return $columns;
2571     }
2572     public function getColumnsWithRegexBoundaries()
2573     {
2574         $columns = array_keys($this->getColumns());
2575         foreach ($columns as $k=>$column){
2576             $columns[$k] = '/([^\.])\b('.$column.')\b/';
2577         }
2578         return $columns;
2579     }
2580
2581
2582     /**
2583     * If is the first time we use a model this function will run the installer for the model if it exists
2584     *
2585     * @access private
2586     */
2587     public function _runCurrentModelInstallerIfExists(&$column_objects)
2588     {
2589         static $installed_models = array();
2590         if(!defined('AK_AVOID_AUTOMATIC_ACTIVE_RECORD_INSTALLERS') && !in_array($this->getModelName(), $installed_models)){
2591             $installed_models[] = $this->getModelName();
2592             require_once(AK_LIB_DIR.DS.'AkInstaller.php');
2593             $installer_name = $this->getModelName().'Installer';
2594             $installer_file = AK_APP_DIR.DS.'installers'.DS.AkInflector::underscore($installer_name).'.php';
2595             if(file_exists($installer_file)){
2596                 require_once($installer_file);
2597                 if(class_exists($installer_name)){
2598                     $Installer = new $installer_name();
2599                     if(method_exists($Installer,'install')){
2600                         $Installer->install();
2601                         $column_objects = $this->_databaseTableInternals($this->getTableName());
2602                         return !empty($column_objects);
2603                     }
2604                 }
2605             }
2606         }
2607         return false;
2608     }
2609
2610
2611     /**
2612     * Returns an array of column objects for the table associated with this class.
2613     */
2614     public function getColumns($force_reload = false)
2615     {
2616         if(empty($this->_columns) || $force_reload){
2617             $this->_columns = $this->getColumnSettings($force_reload);
2618         }
2619
2620         return (array)$this->_columns;
2621     }
2622
2623     public function getColumnSettings($force_reload = false)
2624     {
2625         if(empty($this->_columnsSettings) || $force_reload){
2626             $this->loadColumnsSettings($force_reload);
2627             $this->initiateColumnsToNull();
2628         }
2629         return isset($this->_columnsSettings) ? $this->_columnsSettings : array();
2630     }
2631
2632     public function loadColumnsSettings($force_reload = false)
2633     {
2634         if(is_null($this->_db)){
2635             $this->establishConnection();
2636         }
2637         $this->_columnsSettings = ($force_reload ? null : $this->_getPersistedTableColumnSettings());
2638         if(empty($this->_columnsSettings)){
2639             if(empty($this->_dataDictionary)){
2640                 $this->_dataDictionary =& $this->_db->getDictionary();
2641             }
2642
2643             $column_objects = $this->_databaseTableInternals($this->getTableName());
2644
2645             if( !isset($this->_avoidTableNameValidation) &&
2646             !is_array($column_objects) &&
2647             !$this->_runCurrentModelInstallerIfExists($column_objects)){
2648                 trigger_error(Ak::t('Ooops! Could not fetch details for the table %table_name.', array('%table_name'=>$this->getTableName())), E_USER_ERROR);
2649                 return false;
2650             }elseif (empty($column_objects)){
2651                 $this->_runCurrentModelInstallerIfExists($column_objects);
2652             }
2653             if(is_array($column_objects)){
2654                 foreach (array_keys($column_objects) as $k){
2655                     $this->setColumnSettings($column_objects[$k]->name, $column_objects[$k]);
2656                 }
2657             }
2658             if(!empty($this->_columnsSettings)){
2659                 $this->_persistTableColumnSettings();
2660             }
2661         }
2662         return isset($this->_columnsSettings) ? $this->_columnsSettings : array();
2663     }
2664
2665
2666
2667     public function setColumnSettings($column_name, $column_object)
2668     {
2669         $this->_columnsSettings[$column_name] = array();
2670         $this->_columnsSettings[$column_name]['name'] = $column_object->name;
2671
2672         if($this->_internationalize && $this->_isInternationalizeCandidate($column_object->name)){
2673             $this->_addInternationalizedColumn($column_object->name);
2674         }
2675
2676         $this->_columnsSettings[$column_name]['type'] = $this->getAkelosDataType($column_object);
2677         if(!empty($column_object->primary_key)){
2678             $this->_primaryKey = empty($this->_primaryKey) ? $column_object->name : $this->_primaryKey;
2679             $this->_columnsSettings[$column_name]['primaryKey'] = true;
2680         }
2681         if(!empty($column_object->auto_increment)){
2682             $this->_columnsSettings[$column_name]['autoIncrement'] = true;
2683         }
2684         if(!empty($column_object->has_default)){
2685             $this->_columnsSettings[$column_name]['hasDefault'] = true;
2686         }
2687         if(!empty($column_object->not_null)){
2688             $this->_columnsSettings[$column_name]['notNull'] = true;
2689         }
2690         if(!empty($column_object->max_length) && $column_object->max_length > 0){
2691             $this->_columnsSettings[$column_name]['maxLength'] = $column_object->max_length;
2692         }
2693         if(!empty($column_object->scale) && $column_object->scale > 0){
2694             $this->_columnsSettings[$column_name]['scale'] = $column_object->scale;
2695         }
2696         if(isset($column_object->default_value)){
2697             $this->_columnsSettings[$column_name]['defaultValue'] = $column_object->default_value;
2698         }
2699     }
2700
2701
2702     /**
2703     * Resets all the cached information about columns, which will cause they to be reloaded on the next request.
2704     */
2705     public function resetColumnInformation()
2706     {
2707         $this->_clearPersitedColumnSettings();
2708         $this->_columnNames = $this->_columns = $this->_columnsSettings = $this->_contentColumns = array();
2709     }
2710
2711
2712     /**
2713     * @access private
2714     */
2715     public function _getModelColumnSettings()
2716     {
2717         return AkDbSchemaCache::get($this->getModelName().'_column_settings');
2718
2719     }
2720
2721     /**
2722     * @access private
2723     */
2724     public function _persistTableColumnSettings()
2725     {
2726         AkDbSchemaCache::set($this->getModelName().'_column_settings', $this->_columnsSettings);
2727     }
2728
2729     /**
2730     * @access private
2731     */
2732     public function _getPersistedTableColumnSettings()
2733     {
2734         return AkDbSchemaCache::get($this->getModelName().'_column_settings');
2735     }
2736
2737     /**
2738     * @access private
2739     */
2740     public function _clearPersitedColumnSettings()
2741     {
2742         AkDbSchemaCache::clear($this->getModelName());
2743     }
2744
2745
2746
2747     public function initiateAttributeToNull($attribute)
2748     {
2749         if(!isset($this->$attribute)){
2750             $this->$attribute = null;
2751         }
2752     }
2753
2754     public function initiateColumnsToNull()
2755     {
2756         if(isset($this->_columnsSettings) && is_array($this->_columnsSettings)){
2757             array_map(array(&$this,'initiateAttributeToNull'),array_keys($this->_columnsSettings));
2758         }
2759     }
2760
2761
2762     /**
2763     * Akelos data types are mapped to phpAdodb data types
2764     *
2765     * Returns the Akelos data type for an Adodb Column Object
2766     *
2767     * 'C'=>'string', // Varchar, capped to 255 characters.
2768     * 'X' => 'text' // Larger varchar, capped to 4000 characters (to be compatible with Oracle).
2769     * 'XL' => 'text' // For Oracle, returns CLOB, otherwise the largest varchar size.
2770     *
2771     * 'C2' => 'string', // Multibyte varchar
2772     * 'X2' => 'string', // Multibyte varchar (largest size)
2773     *
2774     * 'B' => 'binary', // BLOB (binary large object)
2775     *
2776     * 'D' => array('date', 'datetime'), //  Date (some databases do not support this, and we return a datetime type)
2777     * 'T' =>  array('datetime', 'timestamp'), //Datetime or Timestamp
2778     * 'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1)
2779     * 'I' => // Integer (mapped to I4)
2780     * 'I1' => 'integer', // 1-byte integer
2781     * 'I2' => 'integer', // 2-byte integer
2782     * 'I4' => 'integer', // 4-byte integer
2783     * 'I8' => 'integer', // 8-byte integer
2784     * 'F' => 'float', // Floating point number
2785     * 'N' => 'integer' //  Numeric or decimal number
2786     *
2787     * @return string One of this 'string','text','integer','float','datetime','timestamp',
2788     * 'time', 'name','date', 'binary', 'boolean'
2789     */
2790     public function getAkelosDataType(&$adodb_column_object)
2791     {
2792         $config_var_name = AkInflector::variablize($adodb_column_object->name.'_data_type');
2793         if(!empty($this->{$config_var_name})){
2794             return $this->{$config_var_name};
2795         }
2796         if(stristr($adodb_column_object->type, 'BLOB')){
2797             return 'binary';
2798         }
2799         if(!empty($adodb_column_object->auto_increment)) {
2800             return 'serial';
2801         }
2802         $meta_type = $this->_dataDictionary->MetaType($adodb_column_object);
2803         $adodb_data_types = array(
2804         'C'=>'string', // Varchar, capped to 255 characters.
2805         'X' => 'text', // Larger varchar, capped to 4000 characters (to be compatible with Oracle).
2806         'XL' => 'text', // For Oracle, returns CLOB, otherwise the largest varchar size.
2807
2808         'C2' => 'string', // Multibyte varchar
2809         'X2' => 'string', // Multibyte varchar (largest size)
2810
2811         'B' => 'binary', // BLOB (binary large object)
2812
2813         'D' => array('date'), //  Date
2814         'T' =>  array('datetime', 'timestamp'), //Datetime or Timestamp
2815         'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1)
2816         'R' => 'serial', // Serial Integer
2817         'I' => 'integer', // Integer (mapped to I4)
2818         'I1' => 'integer', // 1-byte integer
2819         'I2' => 'integer', // 2-byte integer
2820         'I4' => 'integer', // 4-byte integer
2821         'I8' => 'integer', // 8-byte integer
2822         'F' => 'float', // Floating point number
2823         'N' => 'decimal' //  Numeric or decimal number
2824         );
2825
2826         $result = !isset($adodb_data_types[$meta_type]) ?
2827         'string' :
2828         (is_array($adodb_data_types[$meta_type]) ? $adodb_data_types[$meta_type][0] : $adodb_data_types[$meta_type]);
2829
2830         if($result == 'text'){
2831             if(stristr($adodb_column_object->type, 'CHAR') | (isset($adodb_column_object->max_length) && $adodb_column_object->max_length > 0 &&$adodb_column_object->max_length < 256 )){
2832                 return 'string';
2833             }
2834         }
2835
2836         if($this->_getDatabaseType() == 'mysql'){
2837             if($result == 'integer' && stristr($adodb_column_object->type, 'TINYINT')){
2838                 return 'boolean';
2839             }
2840         }elseif($this->_getDatabaseType() == 'postgre'){
2841             if($adodb_column_object->type == 'timestamp' || $result == 'datetime'){
2842                 $adodb_column_object->max_length = 19;
2843             }
2844         }elseif($this->_getDatabaseType() == 'sqlite'){
2845             if($result == 'integer' && (int)$adodb_column_object->max_length === 1 && stristr($adodb_column_object->type, 'TINYINT')){
2846                 return 'boolean';
2847             }elseif($result == 'integer' && stristr($adodb_column_object->type, 'DOUBLE')){
2848                 return 'float';
2849             }
2850         }
2851
2852         if($result == 'datetime' && substr($adodb_column_object->name,-3) == '_on'){
2853             $result = 'date';
2854         }
2855
2856         return $result;
2857     }
2858
2859
2860     /**
2861      * This method retrieves current class name that will be used to map
2862      * your database to this object.
2863      */
2864     public function getClassForDatabaseTableMapping()
2865     {
2866         $class_name = get_class($this);
2867         if(is_subclass_of($this,'akactiverecord') || is_subclass_of($this,'AkActiveRecord')){
2868             $parent_class = get_parent_class($this);
2869             while (substr(strtolower($parent_class),-12) != 'activerecord'){
2870                 $class_name = $parent_class;
2871                 $parent_class = get_parent_class($parent_class);
2872             }
2873         }
2874
2875         $class_name = $this->_getModelName($class_name);
2876         // This is an Active Record Inheritance so we set current table to parent table.
2877         if(!empty($class_name) && strtolower($class_name) != 'activerecord'){
2878             $this->_inheritanceClassName = $class_name;
2879             @$this->setTableName(AkInflector::tableize($class_name), false);
2880         }
2881
2882         return $class_name;
2883     }
2884
2885     public function getDisplayField()
2886     {
2887         return  empty($this->displayField) && $this->hasAttribute('name') ? 'name' : (isset($this->displayField) && $this->hasAttribute($this->displayField) ? $this->displayField : $this->getPrimaryKey());
2888     }
2889
2890     public function setDisplayField($attribute_name)
2891     {
2892         if($this->hasAttribute($attribute_name)){
2893             $this->displayField = $attribute_name;
2894             return true;
2895         }else {
2896             return false;
2897         }
2898     }
2899
2900
2901
2902
2903     /*/Database Reflection*/
2904
2905     /**
2906                                Localization
2907     ====================================================================
2908     */
2909
2910     public function t($string, $array = null,$model=null)
2911     {
2912         return Ak::t($string, $array, empty($model)?AkInflector::underscore($this->getModelName()):$model);
2913     }
2914
2915     public function getInternationalizedColumns()
2916     {
2917         static $cache;
2918         $model = $this->getModelName();
2919         $available_locales = $this->getAvailableLocales();
2920         if(empty($cache[$model])){
2921             $cache[$model] = array();
2922             foreach ($this->getColumnSettings() as $column_name=>$details){
2923                 if(!empty($details['i18n'])){
2924                     $_tmp_pos = strpos($column_name,'_');
2925                     $column = substr($column_name,$_tmp_pos+1);
2926                     $lang = substr($column_name,0,$_tmp_pos);
2927                     if(in_array($lang, $available_locales)){
2928                         $cache[$model][$column] = empty($cache[$model][$column]) ? array($lang) :
2929                         array_merge($cache[$model][$column] ,array($lang));
2930                     }
2931                 }
2932             }
2933         }
2934
2935         return $cache[$model];
2936     }
2937
2938     public function getAvailableLocales()
2939     {
2940         static $available_locales;
2941         if(empty($available_locales)){
2942             if(defined('AK_ACTIVE_RECORD_DEFAULT_LOCALES')){
2943                 $available_locales = Ak::stringToArray(AK_ACTIVE_RECORD_DEFAULT_LOCALES);
2944             }else{
2945                 $available_locales Ak::langs();
2946             }
2947         }
2948         return $available_locales;
2949     }
2950
2951     public function getCurrentLocale()
2952     {
2953         static $current_locale;
2954         if(empty($current_locale)){
2955             $current_locale = Ak::lang();
2956             $available_locales = $this->getAvailableLocales();
2957             if(!in_array($current_locale, $available_locales)){
2958                 $current_locale = array_shift($available_locales);
2959             }
2960         }
2961         return $current_locale;
2962     }
2963
2964
2965     public function getAttributeByLocale($attribute, $locale)
2966     {
2967         $internationalizable_columns = $this->getInternationalizedColumns();
2968         if(!empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){
2969             return $this->getAttribute($locale.'_'.$attribute);
2970         }
2971     }
2972
2973     public function getAttributeLocales($attribute)
2974     {
2975         $attribute_locales = array();
2976         foreach ($this->getAvailableLocales() as $locale){
2977             if($this->hasColumn($locale.'_'.$attribute)){
2978                 $attribute_locales[$locale] = $this->getAttributeByLocale($attribute, $locale);
2979             }
2980         }
2981         return $attribute_locales;
2982     }
2983
2984     public function setAttributeByLocale($attribute, $value, $locale)
2985     {
2986         $internationalizable_columns = $this->getInternationalizedColumns();
2987
2988         if($this->_isInternationalizeCandidate($locale.'_'.$attribute) && !empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){
2989             $this->setAttribute($locale.'_'.$attribute, $value);
2990         }
2991
2992     }
2993
2994     public function setAttributeLocales($attribute, $values = array())
2995     {
2996         foreach ($values as $locale=>$value){
2997             $this->setAttributeByLocale($attribute, $value, $locale);
2998         }
2999     }
3000
3001     /**
3002     * @access private
3003     */
3004     public function _delocalizeAttribute($attribute)
3005     {
3006         return $this->_isInternationalizeCandidate($attribute) ? substr($attribute,3) : $attribute;
3007     }
3008
3009     /**
3010     * @access private
3011     */
3012     public function _isInternationalizeCandidate($column_name)
3013     {
3014         $pos = strpos($column_name,'_');
3015         return $pos === 2 && in_array(substr($column_name,0,$pos),$this->getAvailableLocales());
3016     }
3017
3018     /**
3019     * @access private
3020     */
3021     public function _addInternationalizedColumn($column_name)
3022     {
3023         $this->_columnsSettings[$column_name]['i18n'] = true;
3024     }
3025
3026
3027     /**
3028      * Adds an internationalized attribute to an array containing other locales for the same column name
3029      *
3030      * Example:
3031      *  es_title and en_title will be available user title = array('es'=>'...', 'en' => '...')
3032      *
3033      * @access private
3034      */
3035     public function _groupInternationalizedAttribute($attribute, $value)
3036     {
3037         if($this->_internationalize && $this->_isInternationalizeCandidate($attribute)){
3038             if(!empty($this->$attribute)){
3039                 $_tmp_pos = strpos($attribute,'_');
3040                 $column = substr($attribute,$_tmp_pos+1);
3041                 $lang = substr($attribute,0,$_tmp_pos);
3042                 $this->$column = empty($this->$column) ? array() : $this->$column;
3043                 if(empty($this->$column) || (!empty($this->$column) && is_array($this->$column))){
3044                     $this->$column = empty($this->$column) ? array($lang=>$value) : array_merge($this->$column,array($lang=>$value));
3045                 }
3046             }
3047         }
3048     }
3049
3050     /*/Localization*/
3051
3052
3053
3054
3055     /**
3056                              Type Casting
3057     ====================================================================
3058     See also: Database Reflection.
3059     */
3060
3061     public function getAttributesBeforeTypeCast()
3062     {
3063         $attributes_array = array();
3064         $available_attributes = $this->getAvailableAttributes();
3065         foreach ($available_attributes as $attribute){
3066             $attribute_value = $this->getAttributeBeforeTypeCast($attribute['name']);
3067             if(!empty($attribute_value)){
3068                 $attributes_array[$attribute['name']] = $attribute_value;
3069             }
3070         }
3071         return $attributes_array;
3072     }
3073
3074
3075     public function getAttributeBeforeTypeCast($attribute)
3076     {
3077         if(isset($this->{$attribute.'_before_type_cast'})){
3078             return $this->{$attribute.'_before_type_cast'};
3079         }
3080         return null;
3081     }
3082
3083     public function getAvailableAttributesQuoted()
3084     {
3085         return $this->getAttributesQuoted($this->getAttributes());
3086     }
3087
3088
3089     public function getAttributesQuoted($attributes_array)
3090     {
3091         $set = array();
3092         $attributes_array = $this->getSanitizedConditionsArray($attributes_array);
3093         foreach (array_diff($attributes_array,array('')) as $k=>$v){
3094             $set[$k] = $k.'='.$v;
3095         }
3096
3097         return $set;
3098     }
3099
3100     public function getColumnType($column_name)
3101     {
3102         empty($this->_columns) ? $this->getColumns() : null;
3103         return empty($this->_columns[$column_name]['type']) ? false : $this->_columns[$column_name]['type'];
3104     }
3105
3106     public function getColumnScale($column_name)
3107     {
3108         empty($this->_columns) ? $this->getColumns() : null;
3109         return empty($this->_columns[$column_name]['scale']) ? false : $this->_columns[$column_name]['scale'];
3110     }
3111
3112     public function castAttributeForDatabase($column_name, $value, $add_quotes = true)
3113     {
3114         $result = '';
3115         switch ($this->getColumnType($column_name)) {
3116             case 'datetime':
3117                 if(!empty($value)){
3118                     $date_time = $this->_db->quote_datetime(Ak::getTimestamp($value));
3119                     $result = $add_quotes ? $date_time : trim($date_time ,"'");
3120                 }else{
3121                     $result = 'null';
3122                 }
3123                 break;
3124
3125             case 'date':
3126                 if(!empty($value)){
3127                     $date = $this->_db->quote_date(Ak::getTimestamp($value));
3128                     $result = $add_quotes ? $date : trim($date, "'");
3129                 }else{
3130                     $result = 'null';
3131                 }
3132                 break;
3133
3134             case 'boolean':
3135                 $result = is_null($value) ? 'null' : (!empty($value) ? "'1'" : "'0'");
3136                 break;
3137
3138             case 'binary':
3139                 if($this->_getDatabaseType() == 'postgre'){
3140                     $result is_null($value) ? 'null::bytea ' : " '".$this->_db->escape_blob($value)."'::bytea ";
3141                 }else{
3142                     $result = is_null($value) ? 'null' : ($add_quotes ? $this->_db->quote_string($value) : $value);
3143                 }
3144                 break;
3145
3146             case 'decimal':
3147                 if(is_null($value)){
3148                     $result = 'null';
3149                 }else{
3150                     if($scale = $this->getColumnScale($column_name)){
3151                         $value = number_format($value, $scale, '.', '');
3152                     }
3153                     $result = $add_quotes ? $this->_db->quote_string($value) : $value;
3154                 }
3155                 break;
3156
3157             case 'serial':
3158             case 'integer':
3159                 $result = (is_null($value) || $value==='') ? 'null' : (integer)$value;
3160                 break;
3161
3162             case 'float':
3163                 $result = (empty($value) && $value !== 0) ? 'null' : (is_numeric($value) ? $value : $this->_db->quote_string($value));
3164                 $result = !empty($this->_columns[$column_name]['notNull']) && $result == 'null' && $this->_getDatabaseType() == 'sqlite' ? '0' : $result;
3165                 break;
3166
3167             default:
3168                 if($this->_shouldSerializeColumn($column_name)){
3169                     $value = serialize($value);
3170                 }
3171                 $result = is_null($value) ? 'null' : ($add_quotes ? $this->_db->quote_string($value) : $value);
3172                 break;
3173         }
3174
3175         //  !! nullable vs. not nullable !!
3176         return empty($this->_columns[$column_name]['notNull']) ? ($result === '' ? "''" : $result) : ($result === 'null' ? '' : $result);
3177     }
3178
3179     /**
3180     * You can use this method for casting multiple attributes of the same time at once.
3181     *
3182     * You can pass an array of values or an array of Active Records that might be the response of a finder.
3183     */
3184     public function castAttributesForDatabase($column_name, $values, $add_quotes = true)
3185     {
3186         $casted_values = array();
3187         $values = !empty($values[0]) && is_object($values[0]) && method_exists($values[0], 'collect') && method_exists($values[0], 'getPrimaryKey') ?
3188         $values[0]->collect($values, $values[0]->getPrimaryKey(), $column_name)
3189         : Ak::toArray($values);
3190         if(!empty($values)){
3191             $casted_values = array();
3192             foreach ($values as $value){
3193                 $casted_values[] = $this->castAttributeForDatabase($column_name, $value, $add_quotes);
3194             }
3195         }
3196         return $casted_values;
3197     }
3198
3199     public function castAttributeFromDatabase($column_name, $value)
3200     {
3201         if($this->hasColumn($column_name)){
3202             $column_type = $this->getColumnType($column_name);
3203
3204             if($column_type){
3205                 if('integer' == $column_type){
3206                     return is_null($value) ? null : (integer)$value;
3207                     //return is_null($value) ? null : $value;    // maybe for bigint we can do this
3208                 }elseif('boolean' == $column_type){
3209                     if (is_null($value)) {
3210                         return null;
3211                     }
3212                     if ($this->_getDatabaseType()=='postgre'){
3213                         return $value=='t' ? true : false;
3214                     }
3215                     return (integer)$value === 1 ? true : false;
3216                 }elseif(!empty($value) && 'date' == $column_type && strstr(trim($value),' ')){
3217                     return substr($value,0,10) == '0000-00-00' ? null : str_replace(substr($value,strpos($value,' ')), '', $value);
3218                 }elseif (!empty($value) && 'datetime' == $column_type && substr($value,0,10) == '0000-00-00'){
3219                     return null;
3220                 }elseif ('binary' == $column_type && $this->_getDatabaseType() == 'postgre'){
3221                     $value = $this->_db->unescape_blob($value);
3222                     $value = empty($value) || trim($value) == 'null' ? null : $value;
3223                 }elseif($this->_shouldSerializeColumn($column_name) && is_string($value)){
3224                     $this->_ensureClassExistsForSerializedColumnBeforeUnserializing($column_name);
3225                     $value = @unserialize($value);
3226                 }
3227             }
3228         }
3229         return $value;
3230     }
3231
3232
3233     /**
3234     * Joins date arguments into a single attribute. Like the array generated by the date_helper, so
3235     * array('published_on(1i)' => 2002, 'published_on(2i)' => 'January', 'published_on(3i)' => 24)
3236     * Will be converted to array('published_on'=>'2002-01-24')
3237     *
3238     * @access private
3239     */
3240     public function _castDateParametersFromDateHelper_(&$params)
3241     {
3242         if(empty($params)){
3243             return;
3244         }
3245         $date_attributes = array();
3246         foreach ($params as $k=>$v) {
3247             if(preg_match('/^([A-Za-z0-9_]+)\(([1-5]{1})i\)$/',$k,$match)){
3248                 $date_attributes[$match[1]][$match[2]] = $v;
3249                 $this->$k = $v;
3250                 unset($params[$k]);
3251             }
3252         }
3253         foreach ($date_attributes as $attribute=>$date){
3254             $params[$attribute] = trim(@$date[1].'-'.@$date[2].'-'.@$date[3].' '.@$date[4].':'.@$date[5].':'.@$date[6],' :-');
3255         }
3256     }
3257
3258     /**
3259     * @access private
3260     */
3261     public function _addBlobQueryStack($column_name, $blob_value)
3262     {
3263         $this->_BlobQueryStack[$column_name] = $blob_value;
3264     }
3265
3266     /**
3267     * @access private
3268     */
3269     public function _shouldSerializeColumn($column_name)
3270     {
3271         if(empty($this->serialize)){
3272             return false;
3273         }elseif(!is_array($this->serialize)){
3274             $this->serialize = Ak::toArray($this->serialize);
3275         }
3276         $return=isset($this->serialize[$column_name]) || in_array($column_name, $this->serialize);
3277         return $return;
3278     }
3279
3280     /**
3281     * @access private
3282     */
3283     public function _ensureClassExistsForSerializedColumnBeforeUnserializing($column_name)
3284     {
3285         static $imported_cache = array();
3286         if(empty($imported_cache[$column_name])){
3287             $class_name = isset($this->serialize[$column_name])  ?
3288             (is_string($this->serialize[$column_name]) ? $this->serialize[$column_name] : $column_name) : false;
3289             if($class_name) {
3290                 Ak::import($class_name);
3291             }
3292             $imported_cache[$column_name] = true;
3293         }
3294     }
3295
3296     /**
3297     * @access private
3298     */
3299     public function _updateBlobFields($condition)
3300     {
3301         if(!empty($this->_BlobQueryStack) && is_array($this->_BlobQueryStack)){
3302             foreach ($this->_BlobQueryStack as $column=>$value){
3303                 $this->_db->UpdateBlob($this->getTableName(), $column, $value, $condition);
3304             }
3305             $this->_BlobQueryStack = null;
3306         }
3307     }
3308
3309     /*/Type Casting*/
3310
3311     /**
3312                              Optimistic Locking
3313     ====================================================================
3314     *
3315     * Active Records support optimistic locking if the field <tt>lock_version</tt> is present.  Each update to the
3316     * record increments the lock_version column and the locking facilities ensure that records instantiated twice
3317     * will let the last one saved return false on save() if the first was also updated. Example:
3318     *
3319     *   $p1 = new Person(1);
3320     *   $p2 = new Person(1);
3321     *
3322     *   $p1->first_name = "Michael";
3323     *   $p1->save();
3324     *
3325     *   $p2->first_name = "should fail";
3326     *   $p2->save(); // Returns false
3327     *
3328     * You're then responsible for dealing with the conflict by checking the return value of save(); and either rolling back, merging,
3329     * or otherwise apply the business logic needed to resolve the conflict.
3330     *
3331     * You must ensure that your database schema defaults the lock_version column to 0.
3332     *
3333     * This behavior can be turned off by setting <tt>AkActiveRecord::lock_optimistically = false</tt>.
3334     */
3335     public function isLockingEnabled()
3336     {
3337         return (!isset($this->lock_optimistically) || $this->lock_optimistically !== false) && $this->hasColumn('lock_version');
3338     }
3339     /*/Optimistic Locking*/
3340
3341
3342     /**
3343                                     Callbacks
3344     ====================================================================
3345     See also: Observers.
3346     *
3347     * Callbacks are hooks into the life-cycle of an Active Record object that allows you to trigger logic
3348     * before or after an alteration of the object state. This can be used to make sure that associated and
3349     * dependent objects are deleted when destroy is called (by overwriting beforeDestroy) or to massage attributes
3350     * before they're validated (by overwriting beforeValidation). As an example of the callbacks initiated, consider
3351     * the AkActiveRecord->save() call:
3352     *
3353     * - (-) save()
3354     * - (-) needsValidation()
3355     * - (1) beforeValidation()
3356     * - (2) beforeValidationOnCreate() / beforeValidationOnUpdate()
3357     * - (-) validate()
3358     * - (-) validateOnCreate()
3359     * - (4) afterValidation()
3360     * - (5) afterValidationOnCreate() / afterValidationOnUpdate()
3361     * - (6) beforeSave()
3362     * - (7) beforeCreate() / beforeUpdate()
3363     * - (-) create()
3364     * - (8) afterCreate() / afterUpdate()
3365     * - (9) afterSave()
3366     * - (10) afterDestroy()
3367     * - (11) beforeDestroy()
3368     *
3369     *
3370     * That's a total of 15 callbacks, which gives you immense power to react and prepare for each state in the
3371     * Active Record lifecycle.
3372     *
3373     * Examples:
3374     *   class CreditCard extends ActiveRecord
3375     *   {
3376     *       // Strip everything but digits, so the user can specify "555 234 34" or
3377     *       // "5552-3434" or both will mean "55523434"
3378     *       public function beforeValidationOnCreate
3379     *       {
3380     *           if(!empty($this->number)){
3381     *               $this->number = ereg_replace('[^0-9]*','',$this->number);
3382     *           }
3383     *       }
3384     *   }
3385     *
3386     *   class Subscription extends ActiveRecord
3387     *   {
3388     *       // Note: This is not implemented yet
3389     *       public $beforeCreate  = 'recordSignup';
3390     *
3391     *       public function recordSignup()
3392     *       {
3393     *         $this->signed_up_on = date("Y-m-d");
3394     *       }
3395     *   }
3396     *
3397     *   class Firm extends ActiveRecord
3398     *   {
3399     *       //Destroys the associated clients and people when the firm is destroyed
3400     *       // Note: This is not implemented yet
3401     *       public $beforeDestroy = array('destroyAssociatedPeople', 'destroyAssociatedClients');
3402     *
3403     *       public function destroyAssociatedPeople()
3404     *       {
3405     *           $Person = new Person();
3406     *           $Person->destroyAll("firm_id=>", $this->id);
3407     *       }
3408     *
3409     *       public function destroyAssociatedClients()
3410     *       {
3411     *           $Client = new Client();
3412     *           $Client->destroyAll("client_of=>", $this->id);
3413     *       }
3414     *   }
3415     *
3416     *
3417     * == Canceling callbacks ==
3418     *
3419     * If a before* callback returns false, all the later callbacks and the associated action are cancelled. If an after* callback returns
3420     * false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
3421     * defined as methods on the model, which are called last.
3422     *
3423     * Override this methods to hook Active Records
3424     *
3425     * @access public
3426     */
3427
3428     public function beforeCreate(){return true;}
3429     public function beforeValidation(){return true;}
3430     public function beforeValidationOnCreate(){return true;}
3431     public function beforeValidationOnUpdate(){return true;}
3432     public function beforeSave(){return true;}
3433     public function beforeUpdate(){return true;}
3434     public function afterUpdate(){return true;}
3435     public function afterValidation(){return true;}
3436     public function afterValidationOnCreate(){return true;}
3437     public function afterValidationOnUpdate(){return true;}
3438     public function afterInstantiate(){return true;}
3439     public function afterCreate(){return true;}
3440     public function afterDestroy(){return true;}
3441     public function beforeDestroy(){return true;}
3442     public function afterSave(){return true;}
3443
3444     /*/Callbacks*/
3445
3446
3447     /**
3448                                     Transactions
3449     ====================================================================
3450     *
3451     * Transaction support for database operations
3452     *
3453     * Transactions are enabled automatically for Active record objects, But you can nest transactions within models.
3454     * This transactions are nested, and only the outermost will be executed
3455     *
3456     *   $User->transactionStart();
3457     *   $User->create('username'=>'Bermi');
3458     *   $Members->create('username'=>'Bermi');
3459     *
3460     *    if(!checkSomething()){
3461     *       $User->transactionFail();
3462     *    }
3463     *
3464     *   $User->transactionComplete();
3465     */
3466
3467     public function transactionStart()
3468     {
3469         return $this->_db->startTransaction();
3470     }
3471
3472     public function transactionComplete()
3473     {
3474         return $this->_db->stopTransaction();
3475     }
3476
3477     public function transactionFail()
3478     {
3479         $this->_db->failTransaction();
3480         return false;
3481     }
3482
3483     public function transactionHasFailed()
3484     {
3485         return $this->_db->hasTransactionFailed();
3486     }
3487
3488     /*/Transactions*/
3489
3490
3491
3492
3493     /**
3494                                     Validators
3495     ====================================================================
3496     See also: Error Handling.
3497     *
3498     * Active Records implement validation by overwriting AkActiveRecord::validate (or the variations, validateOnCreate and
3499     * validateOnUpdate). Each of these methods can inspect the state of the object, which usually means ensuring
3500     * that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
3501     *
3502     * Example:
3503     *
3504     *   class Person extends ActiveRecord
3505     *   {
3506     *       public function validate()
3507     *       {
3508     *           $this->addErrorOnEmpty(array('first_name', 'last_name'));
3509     *           if(!preg_match('/[0-9]{4,12}/', $this->phone_number)){
3510     *               $this->addError("phone_number", "has invalid format");
3511     *           }
3512     *       }
3513     *
3514     *       public function validateOnCreate() // is only run the first time a new object is saved
3515     *       {
3516     *           if(!isValidDiscount($this->membership_discount)){
3517     *               $this->addError("membership_discount", "has expired");
3518     *           }
3519     *       }
3520     *
3521     *       public function validateOnUpdate()
3522     *       {
3523     *           if($this->countChangedAttributes() == 0){
3524     *               $this->addErrorToBase("No changes have occurred");
3525     *           }
3526     *       }
3527     *   }
3528     *
3529     *   $Person = new Person(array("first_name" => "David", "phone_number" => "what?"));
3530     *   $Person->save();                    // => false (and doesn't do the save);
3531     *   $Person->hasErrors();         // => false
3532     *   $Person->countErrors();          // => 2
3533     *   $Person->getErrorsOn("last_name");       // => "can't be empty"
3534     *   $Person->getErrorsOn("phone_number");    // => "has invalid format"
3535     *   $Person->yieldEachFullError();        // => "Last name can't be empty \n Phone number has invalid format"
3536     *
3537     *   $Person->setAttributes(array("last_name" => "Heinemeier", "phone_number" => "555-555"));
3538     *   $Person->save(); // => true (and person is now saved in the database)
3539     *
3540     * An "_errors" array is available for every Active Record.
3541     *
3542     */
3543
3544     /**
3545       * Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
3546       *
3547       *  Model:
3548       *     class Person extends ActiveRecord
3549       *     {
3550       *         public function validate()
3551       *         {
3552       *             $this->validatesConfirmationOf('password');
3553       *             $this->validatesConfirmationOf('email_address', "should match confirmation");
3554       *         }
3555       *    }
3556       *
3557       *  View:
3558       *    <?=$form_helper->password_field("person", "password"); ?>
3559       *    <?=$form_helper->password_field("person", "password_confirmation"); ?>
3560       *
3561       * The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual.
3562       * It exists only as an in-memory variable for validating the password. This check is performed only if password_confirmation
3563       * is not null.
3564       *
3565       */
3566     public function validatesConfirmationOf($attribute_names, $message = 'confirmation')
3567     {
3568         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3569         $attribute_names = Ak::toArray($attribute_names);
3570         foreach ($attribute_names as $attribute_name){
3571             $attribute_accessor = $attribute_name.'_confirmation';
3572             if(isset($this->$attribute_accessor) && @$this->$attribute_accessor != @$this->$attribute_name){
3573                 $this->addError($attribute_name, $message);
3574             }
3575         }
3576     }
3577
3578     /**
3579       * Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
3580       *
3581       * class Person extends ActiveRecord
3582       * {
3583       *     public function validateOnCreate()
3584       *     {
3585       *         $this->validatesAcceptanceOf('terms_of_service');
3586       *         $this->validatesAcceptanceOf('eula', "must be abided");
3587       *     }
3588       * }
3589       *
3590       * The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed only if
3591       * terms_of_service is not null.
3592       *
3593       *
3594       * @param accept 1
3595       * Specifies value that is considered accepted.  The default value is a string "1", which makes it easy to relate to an HTML checkbox.
3596       */
3597     public function validatesAcceptanceOf($attribute_names, $message = 'accepted', $accept = 1)
3598     {
3599         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3600
3601         $attribute_names = Ak::toArray($attribute_names);
3602         foreach ($attribute_names as $attribute_name){
3603             if(@$this->$attribute_name != $accept){
3604                 $this->addError($attribute_name, $message);
3605             }
3606         }
3607     }
3608
3609     /**
3610     * Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
3611     *
3612     *   class Book extends ActiveRecord
3613     *   {
3614     *       public $has_many = 'pages';
3615     *       public $belongs_to = 'library';
3616     *
3617     *       public function validate(){
3618     *           $this->validatesAssociated(array('pages', 'library'));
3619     *       }
3620     *   }
3621     *
3622     *
3623     * Warning: If, after the above definition, you then wrote:
3624     *
3625     *   class Page extends ActiveRecord
3626     *   {
3627     *       public $belongs_to = 'book';
3628     *       public function validate(){
3629     *           $this->validatesAssociated('book');
3630     *       }
3631     *   }
3632     *
3633     * ...this would specify a circular dependency and cause infinite recursion.
3634     *
3635     * NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
3636     * is both present and guaranteed to be valid, you also need to use validatesPresenceOf.
3637     */
3638     public function validatesAssociated($attribute_names, $message = 'invalid')
3639     {
3640         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3641         $attribute_names = Ak::toArray($attribute_names);
3642         foreach ($attribute_names as $attribute_name){
3643             if(!empty($this->$attribute_name)){
3644                 if(is_array($this->$attribute_name)){
3645                     foreach(array_keys($this->$attribute_name) as $k){
3646                         if(method_exists($this->{$attribute_name}[$k],'isValid') && !$this->{$attribute_name}[$k]->isValid()){
3647                             $this->addError($attribute_name, $message);
3648                         }
3649                     }
3650                 }elseif (method_exists($this->$attribute_name,'isValid') && !$this->$attribute_name->isValid()){
3651                     $this->addError($attribute_name, $message);
3652                 }
3653             }
3654         }
3655     }
3656
3657     public function isBlank($value = null)
3658     {
3659         return trim((string)$value) == '';
3660     }
3661
3662     /**
3663       * Validates that the specified attributes are not blank (as defined by AkActiveRecord::isBlank()).
3664       */
3665     public function validatesPresenceOf($attribute_names, $message = 'blank')
3666     {
3667         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3668
3669         $attribute_names = Ak::toArray($attribute_names);
3670         foreach ($attribute_names as $attribute_name){
3671             $this->addErrorOnBlank($attribute_name, $message);
3672         }
3673     }
3674
3675     /**
3676       * Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
3677       *
3678       * class Person extends ActiveRecord
3679       * {
3680       *     public function validate()
3681       *     {
3682       *         $this->validatesLengthOf('first_name', array('maximum'=>30));
3683       *         $this->validatesLengthOf('last_name', array('maximum'=>30,'message'=> "less than %d if you don't mind"));
3684       *         $this->validatesLengthOf('last_name', array('within'=>array(7, 32)));
3685       *         $this->validatesLengthOf('last_name', array('in'=>array(6, 20), 'too_long' => "pick a shorter name", 'too_short' => "pick a longer name"));
3686       *         $this->validatesLengthOf('fav_bra_size', array('minimum'=>1, 'too_short'=>"please enter at least %d character"));
3687       *         $this->validatesLengthOf('smurf_leader', array('is'=>4, 'message'=>"papa is spelled with %d characters... don't play me."));
3688       *     }
3689       * }
3690       *
3691       * NOTE: Be aware that $this->validatesLengthOf('field', array('is'=>5)); Will match a string containing 5 characters (Ie. "Spain"), an integer 5, and an array with 5 elements. You must supply additional checking to check for appropriate types.
3692       *
3693       * Configuration options:
3694       * <tt>minimum</tt> - The minimum size of the attribute
3695       * <tt>maximum</tt> - The maximum size of the attribute
3696       * <tt>is</tt> - The exact size of the attribute
3697       * <tt>within</tt> - A range specifying the minimum and maximum size of the attribute
3698       * <tt>in</tt> - A synonym(or alias) for :within
3699       * <tt>allow_null</tt> - Attribute may be null; skip validation.
3700       *
3701       * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default "is" "is too long (max is %d characters)")
3702       * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default "is" "is too short (min is %d characters)")
3703       * <tt>wrong_length</tt> - The error message if using the "is" method and the attribute is the wrong size (default "is" "is the wrong length (should be %d characters)")
3704       * <tt>message</tt> - The error message to use for a "minimum", "maximum", or "is" violation.  An alias of the appropriate too_long/too_short/wrong_length message
3705       */
3706     public function validatesLengthOf($attribute_names, $options = array())
3707     {
3708         // Merge given options with defaults.
3709         $default_options = array(
3710         'too_long'     => $this->_defaultErrorMessages['too_long'],
3711         'too_short'     => $this->_defaultErrorMessages['too_short'],
3712         'wrong_length'     => $this->_defaultErrorMessages['wrong_length'],
3713         'allow_null' => false
3714         );
3715
3716         $range_options = array();
3717         foreach ($options as $k=>$v){
3718             if(in_array($k,array('minimum','maximum','is','in','within'))){
3719                 $range_options[$k] = $v;
3720                 $option = $k;
3721                 $option_value = $v;
3722             }
3723         }
3724
3725         // Ensure that one and only one range option is specified.
3726         switch (count($range_options)) {
3727             case 0:
3728                 trigger_error(Ak::t('Range unspecified.  Specify the "within", "maximum", "minimum, or "is" option.'), E_USER_ERROR);
3729                 return false;
3730                 break;
3731             case 1:
3732                 $options = array_merge($default_options, $options);
3733                 break;
3734             default:
3735                 trigger_error(Ak::t('Too many range options specified.  Choose only one.'), E_USER_ERROR);
3736                 return false;
3737                 break;
3738         }
3739
3740
3741         switch ($option) {
3742             case 'within':
3743             case 'in':
3744                 if(empty($option_value) || !is_array($option_value) || count($option_value) != 2 || !is_numeric($option_value[0]) || !is_numeric($option_value[1])){
3745                     trigger_error(Ak::t('%option must be a Range (array(min, max))',array('%option',$option)), E_USER_ERROR);
3746                     return false;
3747                 }
3748                 $attribute_names = Ak::toArray($attribute_names);
3749
3750                 foreach ($attribute_names as $attribute_name){
3751                     if((!empty($option['allow_null']) && !isset($this->$attribute_name)) || (Ak::size($this->$attribute_name)) < $option_value[0]){
3752                         $this->addError($attribute_name, sprintf($options['too_short'], $option_value[0]));
3753                     }elseif((!empty($option['allow_null']) && !isset($this->$attribute_name)) || (Ak::size($this->$attribute_name)) > $option_value[1]){
3754                         $this->addError($attribute_name, sprintf($options['too_long'], $option_value[1]));
3755                     }
3756                 }
3757                 break;
3758
3759             case 'is':
3760             case 'minimum':
3761             case 'maximum':
3762
3763                 if(empty($option_value) || !is_numeric($option_value) || $option_value <= 0){
3764                     trigger_error(Ak::t('%option must be a nonnegative Integer',array('%option',$option_value)), E_USER_ERROR);
3765                     return false;
3766                 }
3767
3768                 // Declare different validations per option.
3769                 $validity_checks = array('is' => "==", 'minimum' => ">=", 'maximum' => "<=");
3770                 $message_options = array('is' => 'wrong_length', 'minimum' => 'too_short', 'maximum' => 'too_long');
3771
3772                 $message = sprintf(!empty($options['message']) ? $options['message'] : $options[$message_options[$option]],$option_value);
3773
3774                 $attribute_names = Ak::toArray($attribute_names);
3775                 foreach ($attribute_names as $attribute_name){
3776                     if((!$options['allow_null'] && !isset($this->$attribute_name)) ||
3777                     eval("return !(".Ak::size(@$this->$attribute_name)." {$validity_checks[$option]} $option_value);")){
3778                         $this->addError($attribute_name, $message);
3779                     }
3780                 }
3781                 break;
3782             default:
3783                 break;
3784         }
3785
3786         return true;
3787     }
3788
3789     public function validatesSizeOf($attribute_names, $options = array())
3790     {
3791         return validatesLengthOf($attribute_names, $options);
3792     }
3793
3794     /**
3795     * Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
3796     * can be named "davidhh".
3797     *
3798     *  class Person extends ActiveRecord
3799     *   {
3800     *       public function validate()
3801     *       {
3802     *           $this->validatesUniquenessOf('passport_number');
3803     *           $this->validatesUniquenessOf('user_name', array('scope' => "account_id"));
3804     *       }
3805     *   }
3806     *
3807     * It can also validate whether the value of the specified attributes are unique based on multiple scope parameters.  For example,
3808     * making sure that a teacher can only be on the schedule once per semester for a particular class.
3809     *
3810     *   class TeacherSchedule extends ActiveRecord
3811     *   {
3812     *       public function validate()
3813     *       {
3814     *           $this->validatesUniquenessOf('passport_number');
3815     *           $this->validatesUniquenessOf('teacher_id', array('scope' => array("semester_id", "class_id"));
3816     *       }
3817     *   }
3818     *
3819     *
3820     * When the record is created, a check is performed to make sure that no record exist in the database with the given value for the specified
3821     * attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
3822     *
3823     * Configuration options:
3824     * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
3825     * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope"
3826     * <tt>case_sensitive</tt> - Looks for an exact match.  Ignored by non-text columns (true by default).
3827     * <tt>if</tt> - Specifies a method to call or a string to evaluate to determine if the validation should
3828     * occur (e.g. 'if' => 'allowValidation', or 'if' => '$this->signup_step > 2').  The
3829     * method, or string should return or evaluate to a true or false value.
3830     */
3831     public function validatesUniquenessOf($attribute_names, $options = array())
3832     {
3833         $default_options = array('case_sensitive'=>true, 'message'=>'taken');
3834         $options = array_merge($default_options, $options);
3835
3836         if(!empty($options['if'])){
3837             if(method_exists($this,$options['if'])){
3838                 if($this->{$options['if']}() === false){
3839                     return true;
3840                 }
3841             }else {
3842                 eval('$__eval_result = ('.rtrim($options['if'],';').');');
3843                 if(empty($__eval_result)){
3844                     return true;
3845                 }
3846             }
3847         }
3848
3849         $message = isset($this->_defaultErrorMessages[$options['message']]) ? $this->t($this->_defaultErrorMessages[$options['message']]) : $options['message'];
3850         unset($options['message']);
3851
3852         foreach ((array)$attribute_names as $attribute_name){
3853             $value = isset($this->$attribute_name) ? $this->$attribute_name : null;
3854
3855             if($value === null || ($options['case_sensitive'] || !$this->hasColumn($attribute_name))){
3856                 $condition_sql = $this->getTableName().'.'.$attribute_name.' '.$this->getAttributeCondition($value);
3857                 $condition_params = array($value);
3858             }else{
3859                 include_once(AK_VENDOR_DIR.DS.'phputf8'.DS.'utf8.php');
3860                 $condition_sql = 'LOWER('.$this->getTableName().'.'.$attribute_name.') '.$this->getAttributeCondition($value);
3861                 $condition_params = array(is_array($value) ? array_map('utf8_strtolower',$value) : utf8_strtolower($value));
3862             }
3863
3864             if(!empty($options['scope'])){
3865                 foreach ((array)$options['scope'] as $scope_item){
3866                     $scope_value = $this->get($scope_item);
3867                     $condition_sql .= ' AND '.$this->getTableName().'.'.$scope_item.' '.$this->getAttributeCondition($scope_value);
3868                     $condition_params[] = $scope_value;
3869                 }
3870             }
3871
3872             if(!$this->isNewRecord()){
3873                 $condition_sql .= ' AND '.$this->getTableName().'.'.$this->getPrimaryKey().' <> ?';
3874                 $condition_params[] = $this->getId();
3875             }
3876             array_unshift($condition_params,$condition_sql);
3877             if ($this->find('first', array('conditions' => $condition_params))){
3878                 $this->addError($attribute_name, $message);
3879             }
3880         }
3881     }
3882
3883
3884
3885     /**
3886     * Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
3887     * provided.
3888     *
3889     * <code>
3890     *   class Person extends ActiveRecord
3891     *   {
3892     *       public function validate()
3893     *       {
3894     *           $this->validatesFormatOf('email', "/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/");
3895     *       }
3896     *   }
3897     * </code>
3898     *
3899     * A regular expression must be provided or else an exception will be raised.
3900     *
3901     * There are some regular expressions bundled with the Akelos Framework.
3902     * You can override them by defining them as PHP constants (Ie. define('AK_EMAIL_REGULAR_EXPRESSION', '/^My custom email regex$/');). This must be done on your main configuration file.
3903     * This are predefined perl-like regular extensions.
3904     *
3905     * * AK_NOT_EMPTY_REGULAR_EXPRESSION ---> /.+/
3906     * * AK_EMAIL_REGULAR_EXPRESSION ---> /^([a-z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-z0-9\-]+\.)+))([a-z]{2,4}|[0-9]{1,3})(\]?)$/i
3907     * * AK_NUMBER_REGULAR_EXPRESSION ---> /^[0-9]+$/
3908     * * AK_PHONE_REGULAR_EXPRESSION ---> /^([\+]?[(]?[\+]?[ ]?[0-9]{2,3}[)]?[ ]?)?[0-9 ()\-]{4,25}$/
3909     * * AK_DATE_REGULAR_EXPRESSION ---> /^(([0-9]{1,2}(\-|\/|\.| )[0-9]{1,2}(\-|\/|\.| )[0-9]{2,4})|([0-9]{2,4}(\-|\/|\.| )[0-9]{1,2}(\-|\/|\.| )[0-9]{1,2})){1}$/
3910     * * AK_IP4_REGULAR_EXPRESSION ---> /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/
3911     * * AK_POST_CODE_REGULAR_EXPRESSION ---> /^[0-9A-Za-z  -]{2,7}$/
3912     *
3913     * IMPORTANT: Predefined regular expressions may change in newer versions of the Framework, so is highly recommended to hardcode you own on regex on your validators.
3914     *
3915     * Params:
3916     * <tt>$message</tt> - A custom error message (default is: "is invalid")
3917     * <tt>$regular_expression</tt> - The regular expression used to validate the format with (note: must be supplied!)
3918     */
3919     public function validatesFormatOf($attribute_names, $regular_expression, $message = 'invalid', $regex_function = 'preg_match')
3920     {
3921         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3922
3923         $attribute_names = Ak::toArray($attribute_names);
3924         foreach ($attribute_names as $attribute_name){
3925             if(!isset($this->$attribute_name) || !$regex_function($regular_expression, $this->$attribute_name)){
3926                 $this->addError($attribute_name, $message);
3927             }
3928         }
3929     }
3930
3931     /**
3932     * Validates whether the value of the specified attribute is available in a particular array of elements.
3933     *
3934     * class Person extends ActiveRecord
3935     * {
3936     *   function validate()
3937     *   {
3938     *       $this->validatesInclusionOf('gender', array('male', 'female'), "woah! what are you then!??!!");
3939     *       $this->validatesInclusionOf('age', range(0, 99));
3940     *   }
3941     *
3942     * Parameters:
3943     * <tt>$array_of_ possibilities</tt> - An array of available items
3944     * <tt>$message</tt> - Specifies a customer error message (default is: "is not included in the list")
3945     * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false)
3946     */
3947     public function validatesInclusionOf($attribute_names, $array_of_possibilities, $message = 'inclusion', $allow_null = false)
3948     {
3949         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3950
3951         $attribute_names = Ak::toArray($attribute_names);
3952         foreach ($attribute_names as $attribute_name){
3953             if($allow_null ? (@$this->$attribute_name != '' ? (!in_array($this->$attribute_name,$array_of_possibilities)) : @$this->$attribute_name === 0 ) : (isset($this->$attribute_name) ? !in_array(@$this->$attribute_name,$array_of_possibilities) : true )){
3954                 $this->addError($attribute_name, $message);
3955             }
3956         }
3957     }
3958
3959     /**
3960     * Validates that the value of the specified attribute is not in a particular array of elements.
3961     *
3962     *   class Person extends ActiveRecord
3963     *   {
3964     *       public function validate()
3965     *       {
3966     *           $this->validatesExclusionOf('username', array('admin', 'superuser'), "You don't belong here");
3967     *           $this->validatesExclusionOf('age', range(30,60), "This site is only for under 30 and over 60");
3968     *       }
3969     *   }
3970     *
3971     * Parameters:
3972     * <tt>$array_of_possibilities</tt> - An array of items that the value shouldn't be part of
3973     * <tt>$message</tt> - Specifies a customer error message (default is: "is reserved")
3974     * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false)
3975     */
3976     public function validatesExclusionOf($attribute_names, $array_of_possibilities, $message = 'exclusion', $allow_null = false)
3977     {
3978         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3979
3980         $attribute_names = Ak::toArray($attribute_names);
3981         foreach ($attribute_names as $attribute_name){
3982
3983             if($allow_null ? (!empty($this->$attribute_name) ? (in_array(@$this->$attribute_name,$array_of_possibilities)) : false ) : (isset($this->$attribute_name) ? in_array(@$this->$attribute_name,$array_of_possibilities) : true )){
3984                 $this->addError($attribute_name, $message);
3985             }
3986         }
3987     }
3988
3989
3990
3991
3992     /**
3993     * Validates whether the value of the specified attribute is numeric.
3994     *
3995     *   class Person extends ActiveRecord
3996     *   {
3997     *       public function validate()
3998     *       {
3999     *           $this->validatesNumericalityOf('value');
4000     *       }
4001     *   }
4002     *
4003     * Parameters:
4004     * <tt>$message</tt> - A custom error message (default is: "is not a number")
4005     * <tt>$only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
4006     * <tt>$allow_null</tt> Skip validation if attribute is null (default is false).
4007     */
4008     public function validatesNumericalityOf($attribute_names, $message = 'not_a_number', $only_integer = false, $allow_null = false)
4009     {
4010         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
4011
4012         $attribute_names = Ak::toArray($attribute_names);
4013         foreach ($attribute_names as $attribute_name){
4014             if (isset($this->$attribute_name)){
4015                 $value = $this->$attribute_name;
4016                 if ($only_integer){
4017                     $is_int = is_numeric($value) && (int)$value == $value;
4018                     $has_error = !$is_int;
4019                 }else{
4020                     $has_error = !is_numeric($value);
4021                 }
4022             }else{
4023                 $has_error = $allow_null ? false : true;
4024             }
4025
4026             if ($has_error){
4027                 $this->addError($attribute_name, $message);
4028             }
4029         }
4030     }
4031
4032
4033
4034     /**
4035     * Returns true if no errors were added otherwise false.
4036     */
4037     public function isValid()
4038     {
4039         $this->clearErrors();
4040         if($this->beforeValidation() && $this->notifyObservers('beforeValidation')){
4041
4042
4043             if($this->_set_default_attribute_values_automatically){
4044                 $this->_setDefaultAttributeValuesAutomatically();
4045             }
4046
4047             $this->validate();
4048
4049             if($this->_automated_validators_enabled){
4050                 $this->_runAutomatedValidators();
4051             }
4052
4053             $this->afterValidation();
4054             $this->notifyObservers('afterValidation');
4055
4056             if ($this->isNewRecord()){
4057                 if($this->beforeValidationOnCreate()){
4058                     $this->notifyObservers('beforeValidationOnCreate');
4059                     $this->validateOnCreate();
4060                     $this->afterValidationOnCreate();
4061                     $this->notifyObservers('afterValidationOnCreate');
4062                 }
4063             }else{
4064                 if($this->beforeValidationOnUpdate()){
4065                     $this->notifyObservers('beforeValidationOnUpdate');
4066                     $this->validateOnUpdate();
4067                     $this->afterValidationOnUpdate();
4068                     $this->notifyObservers('afterValidationOnUpdate');
4069                 }
4070             }
4071         }
4072
4073         return !$this->hasErrors();
4074     }
4075
4076     /**
4077     * By default the Active Record will validate for the maximum length for database columns. You can
4078     * disable the automated validators by setting $this->_automated_validators_enabled to false.
4079     * Specific validators are (for now):
4080     * $this->_automated_max_length_validator = false; // false by default, but you can set it to true on your model
4081     * $this->_automated_not_null_validator = false; // disabled by default
4082     *
4083     * @access private
4084     */
4085     public function _runAutomatedValidators()
4086     {
4087         foreach ($this->_columns as $column_name=>$column_settings){
4088             if($this->_automated_max_length_validator &&
4089             empty($column_settings['primaryKey']) &&
4090             !empty($this->$column_name) &&
4091             !empty($column_settings['maxLength']) && $column_settings['maxLength'] > 0 &&
4092             strlen($this->$column_name) > $column_settings['maxLength']){
4093                 $this->addError($column_name, sprintf($this->_defaultErrorMessages['too_long'], $column_settings['maxLength']));
4094             }elseif($this->_automated_not_null_validator && empty($column_settings['primaryKey']) && !empty($column_settings['notNull']) && (!isset($this->$column_name) || is_null($this->$column_name))){
4095                 $this->addError($column_name,'empty');
4096             }
4097         }
4098     }
4099
4100     /**
4101     * $this->_set_default_attribute_values_automatically = true; // This enables automated attribute setting from database definition
4102     *
4103     * @access private
4104     */
4105     public function _setDefaultAttributeValuesAutomatically()
4106     {
4107         foreach ($this->_columns as $column_name=>$column_settings){
4108             if(empty($column_settings['primaryKey']) && isset($column_settings['hasDefault']) && $column_settings['hasDefault'] && (!isset($this->$column_name) || is_null($this->$column_name))){
4109                 if(empty($column_settings['defaultValue'])){
4110                     if($column_settings['type'] == 'integer' && empty($column_settings['notNull'])){
4111                         $this->$column_name = 0;
4112                     }elseif(($column_settings['type'] == 'string' || $column_settings['type'] == 'text') && empty($column_settings['notNull'])){
4113                         $this->$column_name = '';
4114                     }
4115                 }else {
4116                     $this->$column_name = $column_settings['defaultValue'];
4117                 }
4118             }
4119         }
4120     }
4121
4122     /**
4123     * Overwrite this method for validation checks on all saves and use addError($field, $message); for invalid attributes.
4124     */
4125     public function validate()
4126     {
4127     }
4128
4129     /**
4130     * Overwrite this method for validation checks used only on creation.
4131     */
4132     public function validateOnCreate()
4133     {
4134     }
4135
4136     /**
4137     * Overwrite this method for validation checks used only on updates.
4138     */
4139     public function validateOnUpdate()
4140     {
4141     }
4142
4143     /*/Validators*/
4144
4145
4146     /**
4147                                   Observers
4148     ====================================================================
4149     See also: Callbacks.
4150     */
4151
4152     /**
4153     * $state store the state of this observable object
4154     *
4155     * @access private
4156     */
4157     public $_observable_state;
4158
4159     /**
4160     * @access private
4161     */
4162     public function _instantiateDefaultObserver()
4163     {
4164         $default_observer_name = ucfirst($this->getModelName().'Observer');
4165         if(class_exists($default_observer_name)){
4166             //$Observer = new $default_observer_name($this);
4167             Ak::singleton($default_observer_name$this);
4168         }
4169     }
4170
4171     /**
4172     * Calls the $method using the reference to each
4173     * registered observer.
4174     * @return true (this is used internally for triggering observers on default callbacks)
4175     */
4176     public function notifyObservers ($method = null)
4177     {
4178         $observers =& $this->getObservers();
4179         $observer_count = count($observers);
4180
4181         if(!empty($method)){
4182             $this->setObservableState($method);
4183         }
4184
4185         $model_name = $this->getModelName();
4186         for ($i=0; $i<$observer_count; $i++) {
4187             if(in_array($model_name, $observers[$i]->_observing)){
4188                 if(method_exists($observers[$i], $method)){
4189                     $observers[$i]->$method($this);
4190                 }else{
4191                     $observers[$i]->update($this->getObservableState(), &$this);
4192                 }
4193             }else{
4194                 $observers[$i]->update($this->getObservableState(), &$this);
4195             }
4196         }
4197         $this->setObservableState('');
4198
4199         return true;
4200     }
4201
4202
4203     public function setObservableState($state_message)
4204     {
4205         $this->_observable_state = $state_message;
4206     }
4207
4208     public function getObservableState()
4209     {
4210         return $this->_observable_state;
4211     }
4212
4213     /**
4214     * Register the reference to an object object
4215     *
4216     *
4217     * @param $observer AkObserver
4218     * @param $options array of options for the observer
4219     * @return void
4220     */
4221     public function addObserver(&$observer)
4222     {
4223         $staticVarNs='AkActiveRecord::observers::' . $this->_modelName;
4224         $observer_class_name = get_class($observer);
4225         /**
4226          * get the statically stored observers for the namespace
4227          */
4228         $observers = &Ak::getStaticVar($staticVarNs);
4229         if (!is_array($observers)) {
4230             $observers = array('classes'=>array(),'objects'=>array());
4231         }
4232         /**
4233          * if not already registered, the observerclass will
4234          * be registered now
4235          */
4236         if (!in_array($observer_class_name,$observers['classes'])) {
4237             $observers['classes'][] = $observer_class_name;
4238             $observers['objects'][] = &$observer;
4239             Ak::setStaticVar($staticVarNs, $observers);
4240
4241         }
4242     }
4243     /**
4244     * Register the reference to an object object
4245     * @return void
4246     */
4247     public function &getObservers()
4248     {
4249         $staticVarNs='AkActiveRecord::observers::' . $this->_modelName;
4250         $key = 'objects';
4251
4252         $array = array();
4253         $observers_arr =& Ak::getStaticVar($staticVarNs);
4254         if (isset($observers_arr[$key])) {
4255             $observers = &$observers_arr[$key];
4256         } else {
4257             $observers = &$array;
4258         }
4259
4260         return $observers;
4261     }
4262
4263     /*/Observers*/
4264
4265
4266
4267
4268     /**
4269                                     Error Handling
4270     ====================================================================
4271     See also: Validators.
4272     */
4273
4274
4275     /**
4276     * Returns the Errors array that holds all information about attribute error messages.
4277     */
4278     public function getErrors()
4279     {
4280         return $this->_errors;
4281     }
4282
4283     /**
4284     * Adds an error to the base object instead of any particular attribute. This is used
4285     * to report errors that doesn't tie to any specific attribute, but rather to the object
4286     * as a whole. These error messages doesn't get prepended with any field name when iterating
4287     * with yieldEachFullError, so they should be complete sentences.
4288     */
4289     public function addErrorToBase($message)
4290     {
4291         $this->addError($this->getModelName(), $message);
4292     }
4293
4294     /**
4295     * Returns errors assigned to base object through addToBase according to the normal rules of getErrorsOn($attribute).
4296     */
4297     public function getBaseErrors()
4298     {
4299         $errors = $this->getErrors();
4300         return (array)@$errors[$this->getModelName()];
4301     }
4302
4303
4304     /**
4305     * Adds an error message ($message) to the ($attribute), which will be returned on a call to <tt>getErrorsOn($attribute)</tt>
4306     * for the same attribute and ensure that this error object returns false when asked if <tt>hasErrors</tt>. More than one
4307     * error can be added to the same $attribute in which case an array will be returned on a call to <tt>getErrorsOn($attribute)</tt>.
4308     * If no $message is supplied, "invalid" is assumed.
4309     */
4310     public function addError($attribute, $message = 'invalid')
4311     {
4312         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
4313         $this->_errors[$attribute][] = $message;
4314     }
4315
4316     /**
4317     * Will add an error message to each of the attributes in $attributes that is empty.
4318     */
4319     public function addErrorOnEmpty($attribute_names, $message = 'empty')
4320     {
4321         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
4322         $attribute_names = Ak::toArray($attribute_names);
4323         foreach ($attribute_names as $attribute){
4324             if(empty($this->$attribute)){
4325                 $this->addError($attribute, $message);
4326             }
4327         }
4328     }
4329
4330     /**
4331     * Will add an error message to each of the attributes in $attributes that is blank (using $this->isBlank).
4332     */
4333     public function addErrorOnBlank($attribute_names, $message = 'blank')
4334     {
4335         $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
4336         $attribute_names = Ak::toArray($attribute_names);
4337         foreach ($attribute_names as $attribute){
4338             if($this->isBlank(@$this->$attribute)){
4339                 $this->addError($attribute, $message);
4340             }
4341         }
4342     }
4343
4344     /**
4345     * Will add an error message to each of the attributes in $attributes that has a length outside of the passed boundary $range.
4346     * If the length is above the boundary, the too_long_message message will be used. If below, the too_short_message.
4347     */
4348     public function addErrorOnBoundaryBreaking($attribute_names, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short')
4349     {
4350         $too_long_message = isset($this->_defaultErrorMessages[$too_long_message]) ? $this->_defaultErrorMessages[$too_long_message] : $too_long_message;
4351         $too_short_message = isset($this->_defaultErrorMessages[$too_short_message]) ? $this->_defaultErrorMessages[$too_short_message] : $too_short_message;
4352
4353         $attribute_names = Ak::toArray($attribute_names);
4354         foreach ($attribute_names as $attribute){
4355             if(@$this->$attribute < $range_begin){
4356                 $this->addError($attribute, $too_short_message);
4357             }
4358             if(@$this->$attribute > $range_end){
4359                 $this->addError($attribute, $too_long_message);
4360             }
4361         }
4362
4363     }
4364
4365     public function addErrorOnBoundryBreaking ($attributes, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short')
4366     {
4367         $this->addErrorOnBoundaryBreaking($attributes, $range_begin, $range_end, $too_long_message, $too_short_message);
4368     }
4369
4370     /**
4371     * Returns true if the specified $attribute has errors associated with it.
4372     */
4373     public function isInvalid($attribute)
4374     {
4375         return $this->getErrorsOn($attribute);
4376     }
4377
4378     /**
4379     * Returns false, if no errors are associated with the specified $attribute.
4380     * Returns the error message, if one error is associated with the specified $attribute.
4381     * Returns an array of error messages, if more than one error is associated with the specified $attribute.
4382     */
4383     public function getErrorsOn($attribute)
4384     {
4385         if (empty($this->_errors[$attribute])){
4386             return false;
4387         }elseif (count($this->_errors[$attribute]) == 1){
4388             $k = array_keys($this->_errors[$attribute]);
4389             return $this->_errors[$attribute][$k[0]];
4390         }else{
4391             return $this->_errors[$attribute];
4392         }
4393     }
4394
4395
4396     /**
4397     * Yields each attribute and associated message per error added.
4398     */
4399     public function yieldEachError()
4400     {
4401         foreach ($this->_errors as $errors){
4402             foreach ($errors as $error){
4403                 $this->yieldError($error);
4404             }
4405         }
4406     }
4407
4408     public function yieldError($message)
4409     {
4410         $messages = is_array($message) ? $message : array($message);
4411         foreach ($messages as $message){
4412             echo "<div class='error'><p>$message</p></div>\n";
4413         }
4414
4415     }
4416
4417     /**
4418     * Yields each full error message added. So Person->addError("first_name", "can't be empty") will be returned
4419     * through iteration as "First name can't be empty".
4420     */
4421     public function yieldEachFullError()
4422     {
4423         $full_messages = $this->getFullErrorMessages();
4424         foreach ($full_messages as $full_message){
4425             $this->yieldError($full_message);
4426         }
4427     }
4428
4429
4430     /**
4431     * Returns all the full error messages in an array.
4432     */
4433     public function getFullErrorMessages()
4434     {
4435         $full_messages = array();
4436
4437         foreach ($this->_errors as $attribute=>$errors){
4438             $full_messages[$attribute] = array();
4439             $attribute_name = AkInflector::humanize($this->_delocalizeAttribute($attribute));
4440             foreach ($errors as $error){
4441                 $full_messages[$attribute][] = $this->t('%attribute_name %error', array(
4442                 '%attribute_name' => $attribute_name,
4443                 '%error' => $error
4444                 ));
4445             }
4446         }
4447         return $full_messages;
4448     }
4449
4450     /**
4451     * Returns true if no errors have been added.
4452     */
4453     public function hasErrors()
4454     {
4455         return !empty($this->_errors);
4456     }
4457
4458     /**
4459     * Removes all the errors that have been added.
4460     */
4461     public function clearErrors()
4462     {
4463         $this->_errors = array();
4464     }
4465
4466     /**
4467     * Returns the total number of errors added. Two errors added to the same attribute will be counted as such
4468     * with this as well.
4469     */
4470     public function countErrors()
4471     {
4472         $error_count = 0;
4473         foreach ($this->_errors as $errors){
4474             $error_count = count($errors)+$error_count;
4475         }
4476
4477         return $error_count;
4478     }
4479
4480
4481     public function errorsToString($print = false)
4482     {
4483         $result = "\n<div id='errors'>\n<ul class='error'>\n";
4484         foreach ($this->getFullErrorMessages() as $error){
4485             $result .= is_array($error) ? "<li class='error'>".join('</li><li class=\'error\'>',$error)."</li>\n" : "<li class='error'>$error</li>\n";
4486         }
4487         $result .= "</ul>\n</div>\n";
4488
4489         if($print){
4490             echo $result;
4491         }
4492         return $result;
4493     }
4494
4495     /*/Error Handling*/
4496
4497
4498
4499     /**
4500                             Act as Behaviours
4501     ====================================================================
4502     See also: Acts as List, Acts as Tree, Acts as Nested Set.
4503     */
4504
4505     /**
4506      * actAs provides a method for extending Active Record models.
4507      *
4508      * Example:
4509      * $this->actsAs('list', array('scope' => 'todo_list'));
4510      */
4511     public function actsAs($behaviour, $options = array())
4512     {
4513         $class_name = $this->_getActAsClassName($behaviour);
4514         $underscored_place_holder = AkInflector::underscore($behaviour);
4515         $camelized_place_holder = AkInflector::camelize($underscored_place_holder);
4516
4517         if($this->$underscored_place_holder =& $this->_getActAsInstance($class_name, $options)){
4518             $this->$camelized_place_holder =& $this->$underscored_place_holder;
4519             if($this->$underscored_place_holder->init($options)){
4520                 $this->__ActsLikeAttributes[$underscored_place_holder] = $underscored_place_holder;
4521             }
4522         }
4523     }
4524
4525     /**
4526     * @access private
4527     */
4528     public function _getActAsClassName($behaviour)
4529     {
4530         $class_name = AkInflector::camelize($behaviour);
4531         return file_exists(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkActsAsBehaviours'.DS.'AkActsAs'.$class_name.'.php') && !class_exists('ActsAs'.$class_name) ?
4532         'AkActsAs'.$class_name : 'ActsAs'.$class_name;
4533     }
4534
4535     /**
4536     * @access private
4537     */
4538     public function &_getActAsInstance($class_name, $options)
4539     {
4540         if(!class_exists($class_name)){
4541             if(substr($class_name,0,2) == 'Ak'){
4542                 include_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkActsAsBehaviours'.DS.$class_name.'.php');
4543             }else{
4544                 include_once(AK_APP_PLUGINS_DIR.DS.AkInflector::underscore($class_name).DS.'lib'.DS.$class_name.'.php');
4545             }
4546         }
4547         if(!class_exists($class_name)){
4548             trigger_error(Ak::t('The class %class used for handling an "act_as %class" does not exist',array('%class'=>$class_name)), E_USER_ERROR);
4549             $false = false;
4550             return $false;
4551         }else{
4552             $ActAsInstance = new $class_name($this, $options);
4553             return $ActAsInstance;
4554         }
4555     }
4556
4557     /**
4558     * @access private
4559     */
4560     public function _loadActAsBehaviours()
4561     {
4562         //$this->act_as = !empty($this->acts_as) ? $this->acts_as : (empty($this->act_as) ? false : $this->act_as);
4563         if(!empty($this->act_as)){
4564             if(is_string($this->act_as)){
4565                 $this->act_as = array_unique(array_diff(array_map('trim',explode(',',$this->act_as.',')), array('')));
4566                 foreach ($this->act_as as $type){
4567                     $this->actsAs($type);
4568                 }
4569             }elseif (is_array($this->act_as)){
4570                 foreach ($this->act_as as $type=>$options){
4571                     if(is_numeric($type)){
4572                         $this->actsAs($options, array());
4573                     }else{
4574                         $this->actsAs($type, $options);
4575                     }
4576                 }
4577             }
4578         }
4579     }
4580
4581     /**
4582     * Returns a comma separated list of possible acts like (active record, nested set, list)....
4583     */
4584     public function actsLike()
4585     {
4586         $result = 'active record';
4587         foreach ($this->__ActsLikeAttributes as $type){
4588             if(!empty($this->$type) && is_object($this->$type) && method_exists($this->{$type}, 'getType')){
4589                 $result .= ','.$this->{$type}->getType();
4590             }
4591         }
4592         return $result;
4593     }
4594
4595     /*/Act as Behaviours*/
4596
4597     /**
4598                             Debugging
4599     ====================================================================
4600     */
4601
4602
4603     public function dbug()
4604     {
4605         if(!$this->isConnected()){
4606             $this->establishConnection();
4607         }
4608         $this->_db->connection->debug = $this->_db->connection->debug ? false : true;
4609         $this->db_debug =& $this->_db->connection->debug;
4610     }
4611
4612     public function toString($print = false)
4613     {
4614         $result = '';
4615         if(!AK_CLI || (AK_ENVIRONMENT == 'testing' && !AK_CLI)){
4616             $result = "<h2>Details for ".AkInflector::humanize(AkInflector::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()."</h2>\n<dl>\n";
4617             foreach ($this->getColumnNames() as $column=>$caption){
4618                 $result .= "<dt>$caption</dt>\n<dd>".$this->getAttribute($column)."</dd>\n";
4619             }
4620             $result .= "</dl>\n<hr />";
4621             if($print){
4622                 echo $result;
4623             }
4624         }elseif(AK_DEV_MODE){
4625             $result =   "\n".
4626             str_replace("\n"," ",var_export($this->getAttributes(),true));
4627             $result .= "\n";
4628             echo $result;
4629             return '';
4630         }elseif (AK_CLI){
4631             $result = "\n-------\n Details for ".AkInflector::humanize(AkInflector::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()." ==\n\n/==\n";
4632             foreach ($this->getColumnNames() as $column=>$caption){
4633                 $result .= "\t * $caption: ".$this->getAttribute($column)."\n";
4634             }
4635             $result .= "\n\n-------\n";
4636             if($print){
4637                 echo $result;
4638             }
4639         }
4640         return $result;
4641     }
4642
4643     public function dbugging($trace_this_on_debug_mode = null)
4644     {
4645         if(!empty($this->_db->debug) && !empty($trace_this_on_debug_mode)){
4646             $message = !is_scalar($trace_this_on_debug_mode) ? var_export($trace_this_on_debug_mode, true) : (string)$trace_this_on_debug_mode;
4647             Ak::trace($message);
4648         }
4649         return !empty($this->_db->debug);
4650     }
4651
4652
4653
4654     public function debug ($data = 'active_record_class', $_functions=0)
4655     {
4656         if(!AK_DEBUG && !AK_DEV_MODE){
4657             return;
4658         }
4659
4660         $data = $data == 'active_record_class' ?  (AK_PHP5 ? clone($this) : $this) : $data;
4661
4662         if($_functions!=0) {
4663             $sf=1;
4664         } else {
4665             $sf=0 ;
4666         }
4667
4668         if (isset ($data)) {
4669             if (is_array($data) || is_object($data)) {
4670
4671                 if (count ($data)) {
4672                     echo AK_CLI ? "/--\n" : "<ol>\n";
4673                     while (list ($key,$value) = each ($data)) {
4674                         if($key{0} == '_'){
4675                             continue;
4676                         }
4677                         $type=gettype($value);
4678                         if ($type=="array") {
4679                             AK_CLI ? printf ("\t* (%s) %s:\n",$type, $key) :
4680                             printf ("<li>(%s) <b>%s</b>:\n",$type, $key);
4681                             ob_start();
4682                             Ak::debug ($value,$sf);
4683                             $lines = explode("\n",ob_get_clean()."\n");
4684                             foreach ($lines as $line){
4685                                 echo "\t".$line."\n";
4686                             }
4687                         }elseif($type == "object"){
4688                             if(method_exists($value,'hasColumn') && $value->hasColumn($key)){
4689                                 $value->toString(true);
4690                                 AK_CLI ? printf ("\t* (%s) %s:\n",$type, $key) :
4691                                 printf ("<li>(%s) <b>%s</b>:\n",$type, $key);
4692                                 ob_start();
4693                                 Ak::debug ($value,$sf);
4694                                 $lines = explode("\n",ob_get_clean()."\n");
4695                                 foreach ($lines as $line){
4696                                     echo "\t".$line."\n";
4697                                 }
4698                             }
4699                         }elseif (eregi ("function", $type)) {
4700                             if ($sf) {
4701                                 AK_CLI ? printf ("\t* (%s) %s:\n",$type, $key, $value) :
4702                                 printf ("<li>(%s) <b>%s</b> </li>\n",$type, $key, $value);
4703                             }
4704                         } else {
4705                             if (!$value) {
4706                                 $value="(none)";
4707                             }
4708                             AK_CLI ? printf ("\t* (%s) %s = %s\n",$type, $key, $value) :
4709                             printf ("<li>(%s) <b>%s</b> = %s</li>\n",$type, $key, $value);
4710                         }
4711                     }
4712                     echo AK_CLI ? "\n--/\n" : "</ol>fin.\n";
4713                 } else {
4714                     echo "(empty)";
4715                 }
4716             }
4717         }
4718     }
4719
4720     /*/Debugging*/
4721
4722
4723
4724     /**
4725                         Utilities
4726     ====================================================================
4727     */
4728     /**
4729      * Selects and filters a search result to include only specified columns
4730      *
4731      *    $people_for_select = $People->select($People->find(),'name','email');
4732      *
4733      *    Now $people_for_select will hold an array with
4734      *    array (
4735      *        array ('name' => 'Jose','email' => 'jose@example.com'),
4736      *        array ('name' => 'Alicia','email' => 'alicia@example.com'),
4737      *        array ('name' => 'Hilario','email' => 'hilario@example.com'),
4738      *        array ('name' => 'Bermi','email' => 'bermi@example.com')
4739      *    );
4740      */
4741     public function select(&$source_array)
4742     {
4743         $resulting_array = array();
4744         if(!empty($source_array) && is_array($source_array) && func_num_args() > 1) {
4745         (array)$args = array_filter(array_slice(func_get_args(),1),array($this,'hasColumn'));
4746         foreach ($source_array as $source_item){
4747             $item_fields = array();
4748             foreach ($args as $arg){
4749                 $item_fields[$arg] =& $source_item->get($arg);
4750             }
4751             $resulting_array[] =& $item_fields;
4752         }
4753         }
4754         return $resulting_array;
4755     }
4756
4757
4758     /**
4759      * Collect is a function for selecting items from double depth array
4760      * like the ones returned by the AkActiveRecord. This comes useful when you just need some
4761      * fields for generating tables, select lists with only desired fields.
4762      *
4763      *    $people_for_select = Ak::select($People->find(),'id','email');
4764      *
4765      *    Returns something like:
4766      *    array (
4767      *        array ('10' => 'jose@example.com'),
4768      *        array ('15' => 'alicia@example.com'),
4769      *        array ('16' => 'hilario@example.com'),
4770      *        array ('18' => 'bermi@example.com')
4771      *    );
4772      */
4773     public function collect(&$source_array, $key_index, $value_index)
4774     {
4775         $resulting_array = array();
4776         if(!empty($source_array) && is_array($source_array)) {
4777             foreach ($source_array as $source_item){
4778                 $resulting_array[$source_item->get($key_index)] = $source_item->get($value_index);
4779             }
4780         }
4781         return $resulting_array;
4782     }
4783
4784     /**
4785      * Generate a json representation of the model record.
4786      *
4787      * parameters:
4788      *
4789      * @param array $options
4790      *
4791      *              option parameters:
4792      *             array(
4793      *              'collection' => array($Person1,$Person), // array of ActiveRecords
4794      *              'include' => array('association1','association2'), // include the associations when exporting
4795      *              'exclude' => array('id','name'), // exclude the attribtues
4796      *              'only' => array('email','last_name') // only export these attributes
4797      *              )
4798      * @return string in Json Format
4799      */
4800     public function toJson($options = array())
4801     {
4802         if (is_array($options) && isset($options[0]) && is_a($options[0], 'AkActiveRecord')) {
4803             $options = array('collection'=>$options);
4804         }
4805         if (isset($options['collection']) && is_array($options['collection']) && $options['collection'][0]->_modelName == $this->_modelName) {
4806             $json = '';
4807
4808             $collection = $options['collection'];
4809             unset($options['collection']);
4810             $jsonVals = array();
4811             foreach ($collection as $element) {
4812                 $jsonVals[]= $element->toJson($options);
4813             }
4814             $json = '['.implode(',',$jsonVals).']';
4815             return $json;
4816         }
4817         /**
4818          * see if we need to include associations
4819          */
4820         $associatedIds = array();
4821         if (isset($options['include']) && !empty($options['include'])) {
4822             $options['include'] = is_array($options['include'])?$options['include']:preg_split('/,\s*/',$options['include']);
4823             foreach ($this->_associations as $key => $obj) {
4824                 if (in_array($key,$options['include'])) {
4825                     $associatedIds[$obj->getAssociationId() . '_id'] = array('name'=>$key,'type'=>$obj->getType());
4826                 }
4827             }
4828         }
4829         if (isset($options['only'])) {
4830             $options['only'] = is_array($options['only'])?$options['only']:preg_split('/,\s*/',$options['only']);
4831         }
4832         if (isset($options['except'])) {
4833             $options['except'] = is_array($options['except'])?$options['except']:preg_split('/,\s*/',$options['except']);
4834         }
4835         foreach ($this->_columns as $key => $def) {
4836
4837             if (isset($options['except']) && in_array($key, $options['except'])) {
4838                 continue;
4839             } else if (isset($options['only']) && !in_array($key, $options['only'])) {
4840                 continue;
4841             } else {
4842                 $val = $this->$key;
4843                 $type = $this->getColumnType($key);
4844                 if (($type == 'serial' || $type=='integer') && $val!==null) $val = intval($val);
4845                 if ($type == 'float' && $val!==null) $val = floatval($val);
4846                 if ($type == 'boolean') $val = $val?1:0;
4847                 $data[$key] = $val;
4848             }
4849         }
4850         if (isset($options['include'])) {
4851             foreach($this->_associationIds as $key=>$val) {
4852                 if ((in_array($key,$options['include']) || in_array($val,$options['include']))) {
4853                     $this->$key->load();
4854                     $associationElement = $key;
4855                     $associationElement = $this->_convert_column_to_xml_element($associationElement);
4856                     if (is_array($this->$key)) {
4857                         $data[$associationElement] = array();
4858                         foreach ($this->$key as $el) {
4859                             if (is_a($el,'AkActiveRecord')) {
4860                                 $attributes = $el->getAttributes();
4861                                 foreach($attributes as $ak=>$av) {
4862                                     $type = $el->getColumnType($ak);
4863                                     if (($type == 'serial' || $type=='integer') && $av!==null) $av = intval($av);
4864                                     if ($type == 'float' && $av!==null) $av = floatval($av);
4865                                     if ($type == 'boolean') $av = $av?1:0;
4866                                     $attributes[$ak]=$av;
4867                                 }
4868                                 $data[$associationElement][] = $attributes;
4869                             }
4870                         }
4871                     } else {
4872                         $el = &$this->$key->load();
4873                         if (is_a($el,'AkActiveRecord')) {
4874                             $attributes = $el->getAttributes();
4875                             foreach($attributes as $ak=>$av) {
4876                                 $type = $el->getColumnType($ak);
4877                                 if (($type == 'serial' || $type=='integer') && $av!==null) $av = intval($av);
4878                                 if ($type == 'float' && $av!==null) $av = floatval($av);
4879                                 if ($type == 'boolean') $av = $av?1:0;
4880                                 $attributes[$ak]=$av;
4881                             }
4882                             $data[$associationElement] = $attributes;
4883                         }
4884                     }
4885                 }
4886             }
4887         }
4888         return Ak::toJson($data);
4889     }
4890     public function _convert_column_to_xml_element($col)
4891     {
4892         return str_replace('_','-',$col);
4893     }
4894     public function _convert_column_from_xml_element($col)
4895     {
4896         return str_replace('-','_',$col);
4897     }
4898
4899     public function _parseXmlAttributes($attributes)
4900     {
4901         $new = array();
4902         foreach($attributes as $key=>$value)
4903         {
4904             $new[$this->_convert_column_from_xml_element($key)] = $value;
4905         }
4906         return $new;
4907     }
4908
4909     public function &_generateModelFromArray($modelName,$attributes)
4910     {
4911         if (isset($attributes[0]) && is_array($attributes[0])) {
4912             $attributes = $attributes[0];
4913         }
4914         $record = new $modelName('attributes',$this->_parseXmlAttributes($attributes));
4915         $record->_newRecord = !empty($attributes['id']);
4916
4917         $associatedIds = array();
4918         foreach ($record->getAssociatedIds() as $key) {
4919             if (isset($attributes[$key]) && is_array($attributes[$key])) {
4920                 $class = $record->$key->_AssociationHandler->getOption($key,'class_name');
4921                 $related = $this->_generateModelFromArray($class,$attributes[$key]);
4922                 $record->$key->build($related->getAttributes(),false);
4923                 $related = &$record->$key->load();
4924                 $record->$key = &$related;
4925             }
4926         }
4927         return $record;
4928     }
4929
4930     public function _fromArray($array)
4931     {
4932         $data  = $array;
4933         $modelName = $this->getModelName();
4934         $values = array();
4935         if (!isset($data[0])) {
4936             $data = array($data);
4937         }
4938         foreach ($data as $key => $value) {
4939             if (is_array($value)){
4940                 $values[] = &$this->_generateModelFromArray($modelName,$value);
4941             }
4942         }
4943         return count($values)==1?$values[0]:$values;
4944     }
4945
4946     /**
4947      * Reads Xml in the following format:
4948      *
4949      *
4950      * <?xml version="1.0" encoding="UTF-8"?>
4951      * <person>
4952      *    <id>1</id>
4953      *    <first-name>Hansi</first-name>
4954      *    <last-name>Müller</last-name>
4955      *    <email>hans@mueller.com</email>
4956      *    <created-at type="datetime">2008-01-01 13:01:23</created-at>
4957      * </person>
4958      *
4959      * and returns an ActiveRecord Object
4960      *
4961      * @param string $xml
4962      * @return AkActiveRecord
4963      */
4964     public function fromXml($xml)
4965     {
4966         $array = Ak::xml_to_array($xml);
4967         $array = $this->_fromXmlCleanup($array);
4968         return $this->_fromArray($array);
4969     }
4970
4971     public function _fromXmlCleanup($array)
4972     {
4973         $result = array();
4974         $key = key($array);
4975         while(is_string($key) && is_array($array[$key]) && count($array[$key])==1) {
4976             $array = $array[$key][0];
4977             $key = key($array);
4978         }
4979         if (is_string($key) && is_array($array[$key])) {
4980             $array = $array[$key];
4981         }
4982         return $array;
4983     }
4984     /**
4985      * Reads Json string in the following format:
4986      *
4987      * {"id":1,"first_name":"Hansi","last_name":"M\u00fcller",
4988      *  "email":"hans@mueller.com","created_at":"2008-01-01 13:01:23"}
4989      *
4990      * and returns an ActiveRecord Object
4991      *
4992      * @param string $json
4993      * @return AkActiveRecord
4994      */
4995     public function fromJson($json)
4996     {
4997         $json = Ak::fromJson($json);
4998         $array = Ak::convert('Object','Array',$json);
4999         return $this->_fromArray($array);
5000     }
5001
5002     /**
5003      * Generate a xml representation of the model record.
5004      *
5005      * Example result:
5006      *
5007      * <?xml version="1.0" encoding="UTF-8"?>
5008      * <person>
5009      *    <id>1</id>
5010      *    <first-name>Hansi</first-name>
5011      *    <last-name>Müller</last-name>
5012      *    <email>hans@mueller.com</email>
5013      *    <created-at type="datetime">2008-01-01 13:01:23</created-at>
5014      * </person>
5015      *
5016      * parameters:
5017      *
5018      * @param array $options
5019      *
5020      *              option parameters:
5021      *             array(
5022      *              'collection' => array($Person1,$Person), // array of ActiveRecords
5023      *              'include' => array('association1','association2'), // include the associations when exporting
5024      *              'exclude' => array('id','name'), // exclude the attribtues
5025      *              'only' => array('email','last_name') // only export these attributes
5026      *              )
5027      * @return string in Xml Format
5028      */
5029     public function toXml($options = array())
5030     {
5031         if (is_array($options) && isset($options[0]) && is_a($options[0], 'AkActiveRecord')) {
5032             $options = array('collection'=>$options);
5033         }
5034         if (isset($options['collection']) && is_array($options['collection']) && $options['collection'][0]->_modelName == $this->_modelName) {
5035             $root = strtolower(AkInflector::pluralize($this->_modelName));
5036             $root = $this->_convert_column_to_xml_element($root);
5037             $xml = '';
5038             if (!(isset($options['skip_instruct']) && $options['skip_instruct'] == true)) {
5039                 $xml .= '<?xml version="1.0" encoding="UTF-8"?>';
5040             }
5041             $xml .= '<' . $root . '>';
5042             $collection = $options['collection'];
5043             unset($options['collection']);
5044             $options['skip_instruct'] = true;
5045             foreach ($collection as $element) {
5046                 $xml .= $element->toXml($options);
5047             }
5048             $xml .= '</' . $root .'>';
5049             return $xml;
5050         }
5051         /**
5052          * see if we need to include associations
5053          */
5054         $associatedIds = array();
5055         if (isset($options['include']) && !empty($options['include'])) {
5056             $options['include'] = is_array($options['include'])?$options['include']:preg_split('/,\s*/',$options['include']);
5057             foreach ($this->_associations as $key => $obj) {
5058                 if (in_array($key,$options['include'])) {
5059                     if ($obj->getType()!='hasAndBelongsToMany') {
5060                         $associatedIds[$obj->getAssociationId() . '_id'] = array('name'=>$key,'type'=>$obj->getType());
5061                     } else {
5062                         $associatedIds[$key] = array('name'=>$key,'type'=>$obj->getType());
5063                     }
5064                 }
5065             }
5066         }
5067         if (isset($options['only'])) {
5068             $options['only'] = is_array($options['only'])?$options['only']:preg_split('/,\s*/',$options['only']);
5069         }
5070         if (isset($options['except'])) {
5071             $options['except'] = is_array($options['except'])?$options['except']:preg_split('/,\s*/',$options['except']);
5072         }
5073         $xml = '';
5074         if (!(isset($options['skip_instruct']) && $options['skip_instruct'] == true)) {
5075             $xml .= '<?xml version="1.0" encoding="UTF-8"?>';
5076         }
5077         $root = $this->_convert_column_to_xml_element(strtolower($this->_modelName));
5078
5079         $xml .= '<' . $root . '>';
5080         $xml .= "\n";
5081         foreach ($this->_columns as $key => $def) {
5082
5083             if (isset($options['except']) && in_array($key, $options['except'])) {
5084                 continue;
5085             } else if (isset($options['only']) && !in_array($key, $options['only'])) {
5086                 continue;
5087             } else {
5088                 $columnType = $def['type'];
5089                 $elementName = $this->_convert_column_to_xml_element($key);
5090                 $xml .= '<' . $elementName;
5091                 $val = $this->$key;
5092                 if (!in_array($columnType,array('string','text','serial'))) {
5093                     $xml .= ' type="' . $columnType . '"';
5094                     if ($columnType=='boolean') $val = $val?1:0;
5095                 }
5096                 $xml .= '>' . Ak::utf8($val) . '</' . $elementName . '>';
5097                 $xml .= "\n";
5098             }
5099         }
5100         if (isset($options['include'])) {
5101             foreach($this->_associationIds as $key=>$val) {
5102                 if ((in_array($key,$options['include']) || in_array($val,$options['include']))) {
5103                     if (is_array($this->$key)) {
5104
5105                         $associationElement = $key;
5106                         $associationElement = AkInflector::pluralize($associationElement);
5107                         $associationElement = $this->_convert_column_to_xml_element($associationElement);
5108                         $xml .= '<'.$associationElement.'>';
5109                         foreach ($this->$key as $el) {
5110                             if (is_a($el,'AkActiveRecord')) {
5111                                 $xml .= $el->toXml(array('skip_instruct'=>true));
5112                             }
5113                         }
5114                         $xml .= '</' . $associationElement .'>';
5115                     } else {
5116                         $el = &$this->$key->load();
5117                         if (is_a($el,'AkActiveRecord')) {
5118                             $xml.=$el->toXml(array('skip_instruct'=>true));
5119                         }
5120                     }
5121                 }
5122             }
5123         }
5124         $xml .= '</' . $root . '>';
5125         return $xml;
5126     }
5127     /**
5128      * converts to yaml-strings
5129      *
5130      * examples:
5131      * User::toYaml($users->find('all'));
5132      * $Bermi->toYaml();
5133      *
5134      * @param array of ActiveRecords[optional] $data
5135      */
5136     public function toYaml($data = null)
5137     {
5138         return Ak::convert('active_record', 'yaml', empty($data) ? $this : $data);
5139     }
5140
5141
5142     /**
5143     * Parses an special formated array as a list of keys and values
5144     *
5145     * This function generates an array with values and keys from an array with numeric keys.
5146     *
5147     * This allows to parse an array to a function in the following manner.
5148     * create('first_name->', 'Bermi', 'last_name->', 'Ferrer');
5149     * //Previous code will be the same that
5150     * create(array('first_name'=>'Bermi', 'last_name'=> 'Ferrer'));
5151     *
5152     * Use this syntax only for quick testings, not for production environments. If the number of arguments varies, the result might be unpredictable.
5153     *
5154     * This function syntax is disabled by default. You need to define('AK_ENABLE_AKELOS_ARGS', true)
5155     * if you need this functionality.
5156     *
5157     * @deprecated
5158     */
5159     public function parseAkelosArgs(&$args)
5160     {
5161         if(!AK_ENABLE_AKELOS_ARGS){
5162             $this->_castDateParametersFromDateHelper_($args);
5163             return ;
5164         }
5165         $k = array_keys($args);
5166         if(isset($k[1]) && substr($args[$k[0]],-1) == '>'){
5167             $size = sizeOf($k);
5168             $params = array();
5169             for($i = 0; $i < $size; $i++ ) {
5170                 $v = $args[$k[$i]];
5171                 if(!isset($key) && is_string($args[$k[$i]]) && substr($v,-1) == '>'){
5172                     $key = rtrim($v, '=-> ');
5173                 }elseif(isset($key)) {
5174                     $params[$key] = $v;
5175                     unset($key);
5176                 }else{
5177                     $params[$k[$i]] = $v;
5178                 }
5179             }
5180             if(!empty($params)){
5181                 $args = $params;
5182             }
5183         }
5184         $this->_castDateParametersFromDateHelper_($args);
5185     }
5186     /**
5187     * Gets an array from a string.
5188     *
5189     * Acts like Php explode() function but uses any of this as valid separators ' AND ',' and ',' + ',' ',',',';'
5190     */
5191     public function getArrayFromAkString($string)
5192     {
5193         if(is_array($string)){
5194             return $string;
5195         }
5196         $string = str_replace(array(' AND ',' and ',' + ',' ',',',';'),array('|','|','|','','|','|'),trim($string));
5197         return strstr($string,'|') ? explode('|', $string) : array($string);
5198     }
5199     /*/Utilities*/
5200
5201
5202     public function getAttributeCondition($argument)
5203     {
5204         if(is_array($argument)){
5205             return 'IN (?)';
5206         }elseif (is_null($argument)){
5207             return 'IS ?';
5208         }else{
5209             return '= ?';
5210         }
5211     }
5212
5213
5214     /**
5215                      Calculations
5216  ====================================================================
5217  */
5218
5219     /**
5220     * @access private
5221     */
5222     public $_calculation_options = array('conditions', 'joins', 'order', 'select', 'group', 'having', 'distinct', 'limit', 'offset');
5223
5224     /**
5225       * Count operates using three different approaches.
5226       *
5227       * * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
5228       * * Count by conditions or joins
5229       * * Count using options will find the row count matched by the options used.
5230       *
5231       * The last approach, count using options, accepts an option hash as the only parameter. The options are:
5232       *
5233       * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array("user_name = ?", $username ). See conditions in the intro.
5234       * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
5235       * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
5236       * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
5237       * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join.
5238       * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
5239       *
5240       * Examples for counting all:
5241       *   $Person->count();         // returns the total count of all people
5242       *
5243       * Examples for count by +conditions+ and +joins+ (this has been deprecated):
5244       *   $Person->count("age > 26");  // returns the number of people older than 26
5245       *   $Person->find("age > 26 AND job.salary > 60000", "LEFT JOIN jobs on jobs.person_id = ".$Person->id); // returns the total number of rows matching the conditions and joins fetched by SELECT COUNT(*).
5246       *
5247       * Examples for count with options:
5248       *   $Person->count('conditions' => "age > 26");
5249       *   $Person->count('conditions' => "age > 26 AND job.salary > 60000", 'joins' => "LEFT JOIN jobs on jobs.person_id = $Person->id"); // finds the number of rows matching the conditions and joins.
5250       *   $Person->count('id', 'conditions' => "age > 26"); // Performs a COUNT(id)
5251       *   $Person->count('all', 'conditions' => "age > 26"); // Performs a COUNT(*) ('all' is an alias for '*')
5252       *
5253       * Note: $Person->count('all') will not work because it will use 'all' as the condition.  Use $Person->count() instead.
5254       */
5255     public function count()
5256     {
5257         $args = func_get_args();
5258         list($column_name, $options) = $this->_constructCountOptionsFromLegacyArgs($args);
5259         return $this->calculate('count', $column_name, $options);
5260     }
5261
5262     /**
5263       * Calculates average value on a given column.  The value is returned as a float.  See #calculate for examples with options.
5264       *
5265       *     $Person->average('age');
5266       */
5267     public function average($column_name, $options = array())
5268     {
5269         return $this->calculate('avg', $column_name, $options);
5270     }
5271
5272     /**
5273       * Calculates the minimum value on a given column.  The value is returned with the same data type of the column..  See #calculate for examples with options.
5274       *
5275       *   $Person->minimum('age');
5276       */
5277     public function minimum($column_name, $options = array())
5278     {
5279         return $this->calculate('min', $column_name, $options);
5280     }
5281
5282     /**
5283       * Calculates the maximum value on a given column.  The value is returned with the same data type of the column..  See #calculate for examples with options.
5284       *
5285       *   $Person->maximum('age');
5286       */
5287     public function maximum($column_name, $options = array())
5288     {
5289         return $this->calculate('max', $column_name, $options);
5290     }
5291
5292     /**
5293       * Calculates the sum value on a given column.  The value is returned with the same data type of the column..  See #calculate for examples with options.
5294       *
5295       *   $Person->sum('age');
5296       */
5297     public function sum($column_name, $options = array())
5298     {
5299         return $this->calculate('sum', $column_name, $options);
5300     }
5301
5302     /**
5303       * This calculates aggregate values in the given column:  Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
5304       * Options such as 'conditions', 'order', 'group', 'having', and 'joins' can be passed to customize the query.
5305       *
5306       * There are two basic forms of output:
5307       *   * Single aggregate value: The single value is type cast to integer for COUNT, float for AVG, and the given column's type for everything else.
5308       *   * Grouped values: This returns an ordered hash of the values and groups them by the 'group' option.  It takes a column name.
5309       *
5310       *       $values = $Person->maximum('age', array('group' => 'last_name'));
5311       *       echo $values["Drake"]
5312       *       => 43
5313       *
5314       * Options:
5315       * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array( "user_name = ?", username ). See conditions in the intro.
5316       * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
5317       *   The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
5318       * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
5319       * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
5320       * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join.
5321       * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
5322       *
5323       * Examples:
5324       *   $Person->calculate('count', 'all'); // The same as $Person->count();
5325       *   $Person->average('age'); // SELECT AVG(age) FROM people...
5326       *   $Person->minimum('age', array('conditions' => array('last_name != ?', 'Drake'))); // Selects the minimum age for everyone with a last name other than 'Drake'
5327       *   $Person->minimum('age', array('having' => 'min(age) > 17', 'group' => 'last'_name)); // Selects the minimum age for any family without any minors
5328       */
5329     public function calculate($operation, $column_name, $options = array())
5330     {
5331         $this->_validateCalculationOptions($options);
5332         $column_name = empty($options['select']) ? $column_name : $options['select'];
5333         $column_name = $column_name == 'all' ? '*' : $column_name;
5334         $column      = $this->_getColumnFor($column_name);
5335         if (!empty($options['group'])){
5336             return $this->_executeGroupedCalculation($operation, $column_name, $column, $options);
5337         }else{
5338             return $this->_executeSimpleCalculation($operation, $column_name, $column, $options);
5339         }
5340
5341         return 0;
5342     }
5343
5344     /**
5345     * @access private
5346     */
5347     public function _constructCountOptionsFromLegacyArgs($args)
5348     {
5349         $options = array();
5350         $column_name = 'all';
5351
5352         /*
5353         We need to handle
5354         count()
5355         count(options=array())
5356         count($column_name='all', $options=array())
5357         count($conditions=null, $joins=null)
5358         */
5359         if(count($args) > 2){
5360             trigger_error(Ak::t("Unexpected parameters passed to count(\$options=array())", E_USER_ERROR));
5361         }elseif(count($args) > 0){
5362             if(!empty($args[0]) && is_array($args[0])){
5363                 $options = $args[0];
5364             }elseif(!empty($args[1]) && is_array($args[1])){
5365                 $column_name = array_shift($args);
5366                 $options = array_shift($args);
5367             }else{
5368                 $options = array('conditions' => $args[0]);
5369                 if(!empty($args[1])){
5370                     $options = array_merge($options, array('joins' => $args[1]));
5371                 }
5372             }
5373         }
5374         return array($column_name, $options);
5375     }
5376
5377
5378     /**
5379     * @access private
5380     */
5381     public function _constructCalculationSql($operation, $column_name, $options)
5382     {
5383         $operation = strtolower($operation);
5384         $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name);
5385         $use_workaround = $operation == 'count' && !empty($options['distinct']) && $this->_getDatabaseType() == 'sqlite';
5386
5387         $sql = $use_workaround ?
5388         "SELECT COUNT(*) AS $aggregate_alias" : // A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
5389         "SELECT $operation(".(empty($options['distinct'])?'':'DISTINCT ')."$column_name) AS $aggregate_alias";
5390
5391
5392         $sql .= empty($options['group']) ? '' : ", {$options['group_field']} AS {$options['group_alias']}";
5393         $sql .= $use_workaround ? " FROM (SELECT DISTINCT {$column_name}" : '';
5394         $sql .=  " FROM ".$this->getTableName()." ";
5395
5396         $sql .=  empty($options['joins']) ? '' : " {$options['joins']} ";
5397
5398         empty($options['conditions']) ? null : $this->addConditions($sql, $options['conditions']);
5399
5400         if (!empty($options['group'])){
5401             $sql .=  " GROUP BY {$options['group_field']} ";
5402             $sql .= empty($options['having']) ? '' : " HAVING {$options['having']} ";
5403         }
5404
5405         $sql .= empty($options['order']) ? '' : " ORDER BY {$options['order']} ";
5406         $this->_db->addLimitAndOffset($sql, $options);
5407         $sql .= $use_workaround ? ')' : '';
5408         return $sql;
5409     }
5410
5411
5412     /**
5413     * @access private
5414     */
5415     public function _executeSimpleCalculation($operation, $column_name, $column, $options)
5416     {
5417         $value = $this->_db->selectValue($this->_constructCalculationSql($operation, $column_name, $options));
5418         return $this->_typeCastCalculatedValue($value, $column, $operation);
5419     }
5420
5421     /**
5422     * @access private
5423     */
5424     public function _executeGroupedCalculation($operation, $column_name, $column, $options)
5425     {
5426         $group_field = $options['group'];
5427         $group_alias = $this->_getColumnAliasFor($group_field);
5428         $group_column = $this->_getColumnFor($group_field);
5429         $options = array_merge(array('group_field' => $group_field, 'group_alias' => $group_alias),$options);
5430         $sql = $this->_constructCalculationSql($operation, $column_name, $options);
5431         $calculated_data = $this->_db->select($sql);
5432         $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name);
5433
5434         $all = array();
5435         foreach ($calculated_data as $row){
5436             $key = $this->_typeCastCalculatedValue($row[$group_alias], $group_column);
5437             $all[$key] = $this->_typeCastCalculatedValue($row[$aggregate_alias], $column, $operation);
5438         }
5439         return $all;
5440     }
5441
5442     /**
5443     * @access private
5444     */
5445     public function _validateCalculationOptions($options = array())
5446     {
5447         $invalid_options = array_diff(array_keys($options),$this->_calculation_options);
5448         if(!empty($invalid_options)){
5449             trigger_error(Ak::t('%options are not valid calculation options.', array('%options'=>join(', ',$invalid_options))), E_USER_ERROR);
5450         }
5451     }
5452
5453     /**
5454     * Converts a given key to the value that the database adapter returns as
5455     * as a usable column name.
5456     *   users.id #=> users_id
5457     *   sum(id) #=> sum_id
5458     *   count(distinct users.id) #=> count_distinct_users_id
5459     *   count(*) #=> count_all
5460     *
5461     * @access private
5462     */
5463     public function _getColumnAliasFor()
5464     {
5465         $args = func_get_args();
5466         $keys = strtolower(join(' ',(!empty($args) ? (is_array($args[0]) ? $args[0] : $args) : array())));
5467         return preg_replace(array('/\*/','/\W+/','/^ +/','/ +$/','/ +/'),array('all',' ','','','_'), $keys);
5468     }
5469
5470     /**
5471     * @access private
5472     */
5473     public function _getColumnFor($field)
5474     {
5475         $field_name = ltrim(substr($field,strpos($field,'.')),'.');
5476         if(in_array($field_name,$this->getColumnNames())){
5477             return $field_name;
5478         }
5479         return $field;
5480     }
5481
5482     /**
5483     * @access private
5484     */
5485     public function _typeCastCalculatedValue($value, $column, $operation = null)
5486     {
5487         $operation = strtolower($operation);
5488         if($operation == 'count'){
5489             return intval($value);
5490         }elseif ($operation == 'avg'){
5491             return floatval($value);
5492         }else{
5493             return empty($column) ? $value : AkActiveRecord::castAttributeFromDatabase($column, $value);
5494         }
5495     }
5496
5497     /*/Calculations*/
5498
5499     public function hasBeenModified()
5500     {
5501         return Ak::objectHasBeenModified($this);
5502     }
5503
5504     /**
5505     * Just freeze the attributes hash, such that associations are still accessible even on destroyed records.
5506     *
5507     * @todo implement freeze correctly for its intended use
5508     */
5509     public function freeze()
5510     {
5511         return $this->_freeze = true;
5512     }
5513
5514     public function isFrozen()
5515     {
5516         return !empty($this->_freeze);
5517     }
5518
5519     /**
5520     * Alias for getModelName()
5521     */
5522     public function getType()
5523     {
5524         return $this->getModelName();
5525     }
5526
5527     public function &objectCache()
5528     {
5529         static $cache;
5530         $false = false;
5531         $args =& func_get_args();
5532         if(count($args) == 2){
5533             if(!isset($cache[$args[0]])){
5534                 $cache[$args[0]] =& $args[1];
5535             }
5536         }elseif(!isset($cache[$args[0]])){
5537             return $false;
5538         }
5539         return $cache[$args[0]];
5540     }
5541
5542
5543     /**
5544                         Connection adapters
5545     ====================================================================
5546     Right now Akelos uses phpAdodb for bd abstraction. This are functionalities not
5547     provided in phpAdodb and that will move to a separated driver for each db
5548     engine in a future
5549     */
5550     public function _extractValueFromDefault($default)
5551     {
5552         if($this->_getDatabaseType() == 'postgre'){
5553             if(preg_match("/^'(.*)'::/", $default, $match)){
5554                 return $match[1];
5555             }
5556             // a postgre HACK; we dont know the column-type here
5557             if ($default=='true') {
5558                 return true;
5559             }
5560             if ($default=='false') {
5561                 return false;
5562             }
5563         }
5564         return $default;
5565     }
5566
5567
5568 }
5569
5570
5571 ?>
5572
Note: See TracBrowser for help on using the browser.