Adding extra charsets for ActionMailer unit tests, if you're looking to parse incomin...
[akelos.git] / lib / AkActiveRecord.php
blob32a2f239146d318c25e2889addcd6b877d4105cd
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4 // +----------------------------------------------------------------------+
5 // | Akelos Framework - http://www.akelos.org |
6 // +----------------------------------------------------------------------+
7 // | Copyright (c) 2002-2006, Akelos Media, S.L. & Bermi Ferrer Martinez |
8 // | Released under the GNU Lesser General Public License, see LICENSE.txt|
9 // +----------------------------------------------------------------------+
11 /**
12 * @package ActiveRecord
13 * @subpackage Base
14 * @component Active Record
15 * @author Bermi Ferrer <bermi a.t akelos c.om> 2004 - 2007
16 * @author Kaste 2007
17 * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org
18 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
21 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkAssociatedActiveRecord.php');
22 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkDbAdapter.php');
24 /**#@+
25 * Constants
27 // Akelos args is a short way to call functions that is only intended for fast prototyping
28 defined('AK_ENABLE_AKELOS_ARGS') ? null : define('AK_ENABLE_AKELOS_ARGS', false);
29 // Use setColumnName if available when using set('column_name', $value);
30 defined('AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT') ? null : define('AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT', true);
31 defined('AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS') ? null : define('AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS', false);
32 defined('AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS') ? null : define('AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS', AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS);
33 defined('AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS') ? null : define('AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS', AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS);
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 /**#@-*/
48 // Forces loading database schema on every call
49 if(AK_DEV_MODE && isset($_SESSION['__activeRecordColumnsSettingsCache'])){
50 unset($_SESSION['__activeRecordColumnsSettingsCache']);
53 ak_compat('array_combine');
55 /**
56 * Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with
57 * which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
58 * is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain
59 * database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
61 * See the mapping rules in table_name and the full example in README.txt for more insight.
63 * == Creation ==
65 * 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
66 * you're receiving the data from somewhere else, like a HTTP request. It works like this:
68 * <code>
69 * $user = new User(array('name' => 'David', 'occupation' => 'Code Artist'));
70 * echo $user->name; // Will print "David"
71 * </code>
73 * You can also use a parameter list initialization.:
75 * $user = new User('name->', 'David', 'occupation->', 'Code Artist');
77 * And of course you can just create a bare object and specify the attributes after the fact:
79 * <code>
80 * $user = new User();
81 * $user->name = 'David';
82 * $user->occupation = 'Code Artist';
83 * </code>
85 * == Conditions ==
87 * Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement.
88 * The array form is to be used when the condition input is tainted and requires sanitization. The string form can
89 * be used for statements that doesn't involve tainted data. Examples:
91 * <code>
92 * class User extends ActiveRecord
93 * {
94 * function authenticateUnsafely($user_name, $password)
95 * {
96 * return findFirst("user_name = '$user_name' AND password = '$password'");
97 * }
99 * function authenticateSafely($user_name, $password)
101 * return findFirst("user_name = ? AND password = ?", $user_name, $password);
104 * </code>
106 * The <tt>authenticateUnsafely</tt> method inserts the parameters directly into the query and is thus susceptible to SQL-injection
107 * attacks if the <tt>$user_name</tt> and <tt>$password</tt> parameters come directly from a HTTP request. The <tt>authenticateSafely</tt> method,
108 * 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
109 * an attacker can't escape the query and fake the login (or worse).
111 * When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth
112 * question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing
113 * the question marks with symbols and supplying a hash with values for the matching symbol keys:
115 * <code>
116 * $Company->findFirst(
117 * "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
118 * array(':id' => 3, ':name' => "37signals", ':division' => "First", ':accounting_date' => '2005-01-01')
119 * );
120 * </code>
122 * == Accessing attributes before they have been type casted ==
124 * Some times you want to be able to read the raw attribute data without having the column-determined type cast run its course first.
125 * That can be done by using the <attribute>_before_type_cast accessors that all attributes have. For example, if your Account model
126 * has a balance attribute, you can call $Account->balance_before_type_cast or $Account->id_before_type_cast.
128 * This is especially useful in validation situations where the user might supply a string for an integer field and you want to display
129 * the original string back in an error message. Accessing the attribute normally would type cast the string to 0, which isn't what you
130 * want.
132 * == Saving arrays, hashes, and other non-mappable objects in text columns ==
134 * Active Record can serialize any object in text columns. To do so, you must specify this with by setting the attribute serialize with
135 * a comma separated list of columns or an array.
136 * This makes it possible to store arrays, hashes, and other non-mappeable objects without doing any additional work. Example:
138 * <code>
139 * class User extends ActiveRecord
141 * var $serialize = 'preferences';
144 * $User = new User(array('preferences'=>array("background" => "black", "display" => 'large')));
145 * $User->find($user_id);
146 * $User->preferences // array("background" => "black", "display" => 'large')
147 * </code>
149 * == Single table inheritance ==
151 * Active Record allows inheritance by storing the name of the class in a column that by default is called "type" (can be changed
152 * by overwriting <tt>AkActiveRecord->_inheritanceColumn</tt>). This means that an inheritance looking like this:
154 * <code>
155 * class Company extends ActiveRecord{}
156 * class Firm extends Company{}
157 * class Client extends Company{}
158 * class PriorityClient extends Client{}
159 * </code>
161 * When you do $Firm->create('name =>', "akelos"), this record will be saved in the companies table with type = "Firm". You can then
162 * fetch this row again using $Company->find('first', "name = '37signals'") and it will return a Firm object.
164 * 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
165 * like normal subclasses with no special magic for differentiating between them or reloading the right type with find.
167 * Note, all the attributes for all the cases are kept in the same table. Read more:
168 * http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
170 * == Connection to multiple databases in different models ==
172 * Connections are usually created through AkActiveRecord->establishConnection and retrieved by AkActiveRecord->connection.
173 * All classes inheriting from AkActiveRecord will use this connection. But you can also set a class-specific connection.
174 * For example, if $Course is a AkActiveRecord, but resides in a different database you can just say $Course->establishConnection
175 * and $Course and all its subclasses will use this connection instead.
177 * Active Records will automatically record creation and/or update timestamps of database objects
178 * if fields of the names created_at/created_on or updated_at/updated_on are present.
179 * Date only: created_on, updated_on
180 * Date and time: created_at, updated_at
182 * This behavior can be turned off by setting <tt>$this->_recordTimestamps = false</tt>.
184 class AkActiveRecord extends AkAssociatedActiveRecord
186 /**#@+
187 * @access private
189 //var $disableAutomatedAssociationLoading = true;
190 var $_tableName;
191 var $_db;
192 var $_newRecord;
193 var $_freeze;
194 var $_dataDictionary;
195 var $_primaryKey;
196 var $_inheritanceColumn;
198 var $_associations;
200 var $_internationalize;
202 var $_errors = array();
204 var $_attributes = array();
206 var $_protectedAttributes = array();
207 var $_accessibleAttributes = array();
209 var $_recordTimestamps = true;
211 // Column description
212 var $_columnNames = array();
213 // Array of column objects for the table associated with this class.
214 var $_columns = array();
215 // Columns that can be edited/viewed
216 var $_contentColumns = array();
217 // Methods that will be dinamically loaded for the model (EXPERIMENTAL) This pretends to generate something similar to Ruby on Rails finders.
218 // If you set array('findOneByUsernameAndPassword', 'findByCompany', 'findAllByExipringDate')
219 // You'll get $User->findOneByUsernameAndPassword('admin', 'pass');
220 var $_dynamicMethods = false;
221 var $_combinedAttributes = array();
223 var $_BlobQueryStack = null;
225 var $_automated_max_length_validator = true;
226 var $_automated_validators_enabled = true;
227 var $_automated_not_null_validator = false;
228 var $_set_default_attribute_values_automatically = true;
230 // This is needed for enabling support for static active record instantation under php
231 var $_activeRecordHasBeenInstantiated = true;
233 var $__ActsLikeAttributes = array();
236 * Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations.
238 var $_defaultErrorMessages = array(
239 'inclusion' => "is not included in the list",
240 'exclusion' => "is reserved",
241 'invalid' => "is invalid",
242 'confirmation' => "doesn't match confirmation",
243 'accepted' => "must be accepted",
244 'empty' => "can't be empty",
245 'blank' => "can't be blank",
246 'too_long' => "is too long (max is %d characters)",
247 'too_short' => "is too short (min is %d characters)",
248 'wrong_length' => "is the wrong length (should be %d characters)",
249 'taken' => "has already been taken",
250 'not_a_number' => "is not a number"
253 var $__activeRecordObject = true;
255 /**#@-*/
257 function __construct()
259 $attributes = (array)func_get_args();
260 return $this->init($attributes);
263 function init($attributes = array())
265 AK_LOG_EVENTS ? ($this->Logger =& Ak::getLogger()) : null;
266 $this->_internationalize = is_null($this->_internationalize) && AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT ? count($this->getAvailableLocales()) > 1 : $this->_internationalize;
268 @$this->_instantiateDefaultObserver();
270 $this->setConnection();
272 if(!empty($this->table_name)){
273 $this->setTableName($this->table_name);
276 $this->_loadActAsBehaviours();
278 if(!empty($this->combined_attributes)){
279 foreach ($this->combined_attributes as $combined_attribute){
280 $this->addCombinedAttributeConfiguration($combined_attribute);
284 if(isset($attributes[0]) && is_array($attributes[0]) && count($attributes) === 1){
285 $attributes = $attributes[0];
286 $this->_newRecord = true;
289 // new AkActiveRecord(23); //Returns object with primary key 23
290 if(isset($attributes[0]) && count($attributes) === 1 && $attributes[0] > 0){
291 $record = $this->find($attributes[0]);
292 if(!$record){
293 return false;
294 }else {
295 $this->setAttributes($record->getAttributes(), true);
297 // This option is only used internally for loading found objects
298 }elseif(isset($attributes[0]) && isset($attributes[1]) && $attributes[0] == 'attributes' && is_array($attributes[1])){
299 foreach(array_keys($attributes[1]) as $k){
300 $attributes[1][$k] = $this->castAttributeFromDatabase($k, $attributes[1][$k]);
303 $avoid_loading_associations = isset($attributes[1]['load_associations']) ? false : !empty($this->disableAutomatedAssociationLoading);
304 $this->setAttributes($attributes[1], true);
305 }else{
306 $this->newRecord($attributes);
309 $this->_buildFinders();
310 empty($avoid_loading_associations) ? $this->loadAssociations() : null;
313 function __destruct()
319 * New objects can be instantiated as either empty (pass no construction parameter) or pre-set with attributes but not yet saved
320 * (pass an array with key names matching the associated table column names).
321 * In both instances, valid attribute keys are determined by the column names of the associated table; hence you can't
322 * have attributes that aren't part of the table columns.
324 function newRecord($attributes)
326 $this->_newRecord = true;
328 if(AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS && empty($attributes)){
329 return;
332 if(isset($attributes) && !is_array($attributes)){
333 $attributes = func_get_args();
335 $this->setAttributes($this->attributesFromColumnDefinition(),true);
336 $this->setAttributes($attributes);
341 * Returns a clone of the record that hasn't been assigned an id yet and is treated as a new record.
343 function cloneRecord()
345 $model_name = $this->getModelName();
346 $attributes = $this->getAttributesBeforeTypeCast();
347 if(isset($attributes[$this->getPrimaryKey()])){
348 unset($attributes[$this->getPrimaryKey()]);
350 return new $model_name($attributes);
355 * Returns true if this object hasn't been saved yet that is, a record for the object doesn't exist yet.
357 function isNewRecord()
359 if(!isset($this->_newRecord) && !isset($this->{$this->getPrimaryKey()})){
360 $this->_newRecord = true;
362 return $this->_newRecord;
368 * Reloads the attributes of this object from the database.
370 function reload()
373 * @todo clear cache
375 if($object = $this->find($this->getId())){
376 $this->setAttributes($object->getAttributes(), true);
377 return true;
378 }else {
379 return false;
386 Creating records
387 ====================================================================
390 * Creates an object, instantly saves it as a record (if the validation permits it), and returns it.
391 * If the save fail under validations, the unsaved object is still returned.
393 function &create($attributes = null)
395 if(!isset($this->_activeRecordHasBeenInstantiated)){
396 return Ak::handleStaticCall();
399 if(func_num_args() > 1){
400 $attributes = func_get_args();
402 $model = $this->getModelName();
404 $object =& new $model();
405 $object->setAttributes($attributes);
406 $object->save();
407 return $object;
410 function createOrUpdate($validate = true)
412 if($validate && !$this->isValid()){
413 $this->transactionFail();
414 return false;
416 return $this->isNewRecord() ? $this->_create() : $this->_update();
419 function &findOrCreateBy()
421 $args = func_get_args();
422 $Item =& Ak::call_user_func_array(array(&$this,'findFirstBy'), $args);
423 if(!$Item){
424 $attributes = array();
426 list($sql, $columns) = $this->_getFindBySqlAndColumns(array_shift($args), $args);
428 if(!empty($columns)){
429 foreach ($columns as $column){
430 $attributes[$column] = array_shift($args);
433 $Item =& $this->create($attributes);
434 $Item->has_been_created = true;
435 }else{
436 $Item->has_been_created = false;
438 $Item->has_been_found = !$Item->has_been_created;
439 return $Item;
443 * Creates a new record with values matching those of the instance attributes.
444 * Must be called as a result of a call to createOrUpdate.
446 * @access private
448 function _create()
450 if (!$this->beforeCreate() || !$this->notifyObservers('beforeCreate')){
451 return $this->transactionFail();
454 $this->_setRecordTimestamps();
456 // deprecated section
457 if($this->isLockingEnabled() && is_null($this->get('lock_version'))){
458 Ak::deprecateWarning(array("Column %lock_version_column should have a default setting. Assumed '1'.",'%lock_version_column'=>'lock_version'));
459 $this->setAttribute('lock_version',1);
460 } // end
462 $attributes = $this->getColumnsForAttributes($this->getAttributes());
463 foreach ($attributes as $column=>$value){
464 $attributes[$column] = $this->castAttributeForDatabase($column,$value);
467 $pk = $this->getPrimaryKey();
468 $table = $this->getTableName();
470 $id = $this->_db->incrementsPrimaryKeyAutomatically() ? null : $this->_db->getNextSequenceValueFor($table);
471 $attributes[$pk] = $id;
473 $attributes = array_diff($attributes, array(''));
476 $sql = 'INSERT INTO '.$table.' '.
477 '('.join(', ',array_keys($attributes)).') '.
478 'VALUES ('.join(',',array_values($attributes)).')';
480 $inserted_id = $this->_db->insert($sql, $id, $pk, $table, 'Create '.$this->getModelName());
481 if ($this->transactionHasFailed()){
482 return false;
484 $this->setId($inserted_id);
486 $this->_newRecord = false;
488 if (!$this->afterCreate() || !$this->notifyObservers('afterCreate')){
489 return $this->transactionFail();
492 return true;
495 function _setRecordTimestamps()
497 if (!$this->_recordTimestamps){
498 return;
500 if ($this->_newRecord){
501 if ($this->hasColumn('created_at')){
502 $this->setAttribute('created_at', Ak::getDate());
504 if ($this->hasColumn('created_on')){
505 $this->setAttribute('created_on', Ak::getDate(null, 'Y-m-d'));
507 }else{
508 if ($this->hasColumn('updated_at')){
509 $this->setAttribute('updated_at', Ak::getDate());
511 if ($this->hasColumn('updated_on')){
512 $this->setAttribute('updated_on', Ak::getDate(null, 'Y-m-d'));
516 if($this->_newRecord && isset($this->expires_on)){
517 if(isset($this->expires_at) && $this->hasColumn('expires_at')){
518 $this->setAttribute('expires_at',Ak::getDate(strtotime($this->expires_at) + (defined('AK_TIME_DIFFERENCE') ? AK_TIME_DIFFERENCE*60 : 0)));
519 }elseif(isset($this->expires_on) && $this->hasColumn('expires_on')){
520 $this->setAttribute('expires_on',Ak::getDate(strtotime($this->expires_on) + (defined('AK_TIME_DIFFERENCE') ? AK_TIME_DIFFERENCE*60 : 0), 'Y-m-d'));
526 /*/Creating records*/
530 Saving records
531 ====================================================================
534 * - No record exists: Creates a new record with values matching those of the object attributes.
535 * - A record does exist: Updates the record with values matching those of the object attributes.
537 function save($validate = true)
539 if($this->isFrozen()){
540 return false;
542 $result = false;
543 $this->transactionStart();
544 if($this->beforeSave() && $this->notifyObservers('beforeSave')){
545 $result = $this->createOrUpdate($validate);
546 if(!$this->transactionHasFailed()){
547 if(!$this->afterSave()){
548 $this->transactionFail();
549 }else{
550 if(!$this->notifyObservers('afterSave')){
551 $this->transactionFail();
555 }else{
556 $this->transactionFail();
559 $result = $this->transactionHasFailed() ? false : $result;
560 $this->transactionComplete();
562 return $result;
565 /*/Saving records*/
568 Counting Records
569 ====================================================================
570 See also: Counting Attributes.
574 * Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
576 * $Product->countBySql("SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id");
578 function countBySql($sql)
580 if(!isset($this->_activeRecordHasBeenInstantiated)){
581 return Ak::handleStaticCall();
583 if(!stristr($sql, 'COUNT') && stristr($sql, ' FROM ')){
584 $sql = 'SELECT COUNT(*) '.substr($sql,strpos(str_replace(' from ',' FROM ', $sql),' FROM '));
586 if(!$this->isConnected()){
587 $this->setConnection();
590 return (integer)$this->_db->selectValue($sql);
592 /*/Counting Records*/
595 Updating records
596 ====================================================================
597 See also: Callbacks.
601 * Finds the record from the passed id, instantly saves it with the passed attributes (if the validation permits it),
602 * and returns it. If the save fail under validations, the unsaved object is still returned.
604 function update($id, $attributes)
606 if(!isset($this->_activeRecordHasBeenInstantiated)){
607 return Ak::handleStaticCall();
609 if(is_array($id)){
610 $results = array();
611 foreach ($id as $idx=>$single_id){
612 $results[] = $this->update($single_id, isset($attributes[$idx]) ? $attributes[$idx] : $attributes);
614 return $results;
615 }else{
616 $object =& $this->find($id);
617 $object->updateAttributes($attributes);
618 return $object;
623 * Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
625 function updateAttribute($name, $value, $should_validate=true)
627 $this->setAttribute($name, $value);
628 return $this->save($should_validate);
633 * Updates all the attributes in from the passed array and saves the record. If the object is
634 * invalid, the saving will fail and false will be returned.
636 function updateAttributes($attributes, $object = null)
638 isset($object) ? $object->setAttributes($attributes) : $this->setAttributes($attributes);
640 return isset($object) ? $object->save() : $this->save();
644 * Updates all records with the SET-part of an SQL update statement in updates and returns an
645 * integer with the number of rows updates. A subset of the records can be selected by specifying conditions. Example:
646 * <code>$Billing->updateAll("category = 'authorized', approved = 1", "author = 'David'");</code>
648 * Important note: Conditions are not sanitized yet so beware of accepting
649 * variable conditions when using this function
651 function updateAll($updates, $conditions = null)
653 if(!isset($this->_activeRecordHasBeenInstantiated)){
654 return Ak::handleStaticCall();
657 * @todo sanitize sql conditions
659 $sql = 'UPDATE '.$this->getTableName().' SET '.$updates;
660 $this->addConditions($sql, $conditions);
661 return $this->_db->update($sql, $this->getModelName().' Update All');
666 * Updates the associated record with values matching those of the instance attributes.
667 * Must be called as a result of a call to createOrUpdate.
669 * @access private
671 function _update()
673 if(!$this->beforeUpdate() || !$this->notifyObservers('beforeUpdate')){
674 return $this->transactionFail();
677 $this->_setRecordTimestamps();
679 $lock_check_sql = '';
680 if ($this->isLockingEnabled()){
681 $previous_value = $this->lock_version;
682 $this->setAttribute('lock_version', $previous_value + 1);
683 $lock_check_sql = ' AND lock_version = '.$previous_value;
686 $quoted_attributes = $this->getAvailableAttributesQuoted();
687 $sql = 'UPDATE '.$this->getTableName().' '.
688 'SET '.join(', ', $quoted_attributes) .' '.
689 'WHERE '.$this->getPrimaryKey().'='.$this->quotedId().$lock_check_sql;
691 $affected_rows = $this->_db->update($sql,'Updating '.$this->getModelName());
692 if($this->transactionHasFailed()){
693 return false;
696 if ($this->isLockingEnabled() && $affected_rows != 1){
697 $this->setAttribute('lock_version', $previous_value);
698 trigger_error(Ak::t('Attempted to update a stale object'), E_USER_NOTICE);
699 return $this->transactionFail();
702 if(!$this->afterUpdate() || !$this->notifyObservers('afterUpdate')){
703 return $this->transactionFail();
706 return true;
709 /*/Updating records*/
714 Deleting records
715 ====================================================================
716 See also: Callbacks.
720 * Deletes the record with the given id without instantiating an object first. If an array of
721 * ids is provided, all of them are deleted.
723 function delete($id)
725 if(!isset($this->_activeRecordHasBeenInstantiated)){
726 return Ak::handleStaticCall();
728 $id = func_num_args() > 1 ? func_get_args() : $id;
729 return $this->deleteAll($this->getPrimaryKey().' IN ('.(is_array($id) ? join(', ',$id) : $id).')');
734 * Deletes all the records that matches the condition without instantiating the objects first
735 * (and hence not calling the destroy method). Example:
737 * <code>$Post->destroyAll("person_id = 5 AND (category = 'Something' OR category = 'Else')");</code>
739 * Important note: Conditions are not sanitized yet so beware of accepting
740 * variable conditions when using this function
742 function deleteAll($conditions = null)
744 if(!isset($this->_activeRecordHasBeenInstantiated)){
745 return Ak::handleStaticCall();
748 * @todo sanitize sql conditions
750 $sql = 'DELETE FROM '.$this->getTableName();
751 $this->addConditions($sql,$conditions);
752 return $this->_db->delete($sql,$this->getModelName().' Delete All');
757 * Destroys the record with the given id by instantiating the object and calling destroy
758 * (all the callbacks are the triggered). If an array of ids is provided, all of them are destroyed.
759 * Deletes the record in the database and freezes this instance to reflect that no changes should be
760 * made (since they can't be persisted).
762 function destroy($id = null)
764 if(!isset($this->_activeRecordHasBeenInstantiated)){
765 return Ak::handleStaticCall();
768 $id = func_num_args() > 1 ? func_get_args() : $id;
770 if(isset($id)){
771 $this->transactionStart();
772 $id_arr = is_array($id) ? $id : array($id);
773 if($objects = $this->find($id_arr)){
774 $results = count($objects);
775 $no_problems = true;
776 for ($i=0; $results > $i; $i++){
777 if(!$objects[$i]->destroy()){
778 $no_problems = false;
781 $this->transactionComplete();
782 return $no_problems;
783 }else {
784 $this->transactionComplete();
785 return false;
787 }else{
788 if(!$this->isNewRecord()){
789 $this->transactionStart();
790 $return = $this->_destroy() && $this->freeze();
791 $this->transactionComplete();
792 return $return;
797 function _destroy()
799 if(!$this->beforeDestroy() || !$this->notifyObservers('beforeDestroy')){
800 return $this->transactionFail();
803 $sql = 'DELETE FROM '.$this->getTableName().' WHERE '.$this->getPrimaryKey().' = '.$this->_db->quote_string($this->getId());
804 if ($this->_db->delete($sql,$this->getModelName().' Destroy') !== 1){
805 return $this->transactionFail();
808 if (!$this->afterDestroy() || !$this->notifyObservers('afterDestroy')){
809 return $this->transactionFail();
811 return true;
815 * Destroys the objects for all the records that matches the condition by instantiating
816 * each object and calling the destroy method.
818 * Example:
820 * $Person->destroyAll("last_login < '2004-04-04'");
822 function destroyAll($conditions)
824 if($objects = $this->find('all',array('conditions'=>$conditions))){
825 $results = count($objects);
826 $no_problems = true;
827 for ($i=0; $results > $i; $i++){
828 if(!$objects[$i]->destroy()){
829 $no_problems = false;
832 return $no_problems;
833 }else {
834 return false;
838 /*/Deleting records*/
844 Finding records
845 ====================================================================
849 * Returns true if the given id represents the primary key of a record in the database, false otherwise. Example:
851 * $Person->exists(5);
853 function exists($id)
855 return $this->find('first',array('conditions' => array($this->getPrimaryKey().' = '.$id))) !== false;
859 * Find operates with three different retrieval approaches:
860 * * Find by id: This can either be a specific id find(1), a list of ids find(1, 5, 6),
861 * or an array of ids find(array(5, 6, 10)). If no record can be found for all of the listed ids,
862 * then RecordNotFound will be raised.
863 * * Find first: This will return the first record matched by the options used. These options
864 * can either be specific conditions or merely an order.
865 * If no record can matched, false is returned.
866 * * Find all: This will return all the records matched by the options used. If no records are found, an empty array is returned.
868 * All approaches accepts an $option array as their last parameter. The options are:
870 * 'conditions' => An SQL fragment like "administrator = 1" or array("user_name = ?" => $username). See conditions in the intro.
871 * 'order' => An SQL fragment like "created_at DESC, name".
872 * 'limit' => An integer determining the limit on the number of rows that should be returned.
873 * 'offset' => An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
874 * 'joins' => An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = $id". (Rarely needed).
875 * 'include' => Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols
876 * named refer to already defined associations. See eager loading under Associations.
878 * Examples for find by id:
879 * <code>
880 * $Person->find(1); // returns the object for ID = 1
881 * $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
882 * $Person->find(array(7, 17)); // returns an array for objects with IDs in (7, 17)
883 * $Person->find(array(1)); // returns an array for objects the object with ID = 1
884 * $Person->find(1, array('conditions' => "administrator = 1", 'order' => "created_on DESC"));
885 * </code>
887 * Examples for find first:
888 * <code>
889 * $Person->find('first'); // returns the first object fetched by SELECT * FROM people
890 * $Person->find('first', array('conditions' => array("user_name = ':user_name'", ':user_name' => $user_name)));
891 * $Person->find('first', array('order' => "created_on DESC", 'offset' => 5));
892 * </code>
894 * Examples for find all:
895 * <code>
896 * $Person->find('all'); // returns an array of objects for all the rows fetched by SELECT * FROM people
897 * $Person->find(); // Same as $Person->find('all');
898 * $Person->find('all', array('conditions' => array("category IN (categories)", 'categories' => join(','$categories)), 'limit' => 50));
899 * $Person->find('all', array('offset' => 10, 'limit' => 10));
900 * $Person->find('all', array('include' => array('account', 'friends'));
901 * </code>
903 function &find()
905 if(!isset($this->_activeRecordHasBeenInstantiated)){
906 return Ak::handleStaticCall();
909 $args = func_get_args();
911 $options = $this->_extractOptionsFromArgs($args);
912 list($fetch,$options) = $this->_extractConditionsFromArgs($args,$options);
914 $this->_sanitizeConditionsVariables($options);
916 switch ($fetch) {
917 case 'first':
918 // HACK: php4 pass by ref
919 $result =& $this->_findInitial($options);
920 return $result;
921 break;
923 case 'all':
924 // HACK: php4 pass by ref
925 $result =& $this->_findEvery($options);
926 return $result;
927 break;
929 default:
930 // HACK: php4 pass by ref
931 $result =& $this->_findFromIds($args, $options);
932 return $result;
933 break;
935 $result = false;
936 return $result;
939 function &_findInitial($options)
941 // TODO: virtual_limit is a hack
942 // actually we fetch_all and return only the first row
943 $options = array_merge($options, array((!empty($options['include']) ?'virtual_limit':'limit')=>1));
944 $result =& $this->_findEvery($options);
946 if(!empty($result) && is_array($result)){
947 $_result =& $result[0];
948 }else{
949 $_result = false;
950 // if we return an empty array instead of false we need to change this->exists()!
951 //$_result = array();
953 return $_result;
957 function &_findEvery($options)
959 if((!empty($options['include']) && $this->hasAssociations())){
960 $result =& $this->findWithAssociations($options);
961 }else{
962 $sql = $this->constructFinderSql($options);
963 if(!empty($options['bind']) && is_array($options['bind']) && strstr($sql,'?')){
964 $sql = array_merge(array($sql),$options['bind']);
967 $result =& $this->findBySql($sql);
970 if(!empty($result) && is_array($result)){
971 $_result =& $result;
972 }else{
973 $_result = false;
975 return $_result;
979 function &_findFromIds($ids, $options)
981 $expects_array = is_array($ids[0]);
982 $ids = array_unique($expects_array ? (isset($ids[1]) ? array_merge($ids[0],$ids) : $ids[0]) : $ids);
984 $num_ids = count($ids);
986 //at this point $options['conditions'] can't be an array
987 $conditions = !empty($options['conditions']) ? ' AND '.$options['conditions'] : '';
989 switch ($num_ids){
990 case 0 :
991 trigger_error($this->t('Couldn\'t find %object_name without an ID%conditions',array('%object_name'=>$this->getModelName(),'%conditions'=>$conditions)), E_USER_ERROR);
992 break;
994 case 1 :
995 $table_name = !empty($options['include']) && $this->hasAssociations() ? '__owner' : $this->getTableName();
996 $options['conditions'] = $table_name.'.'.$this->getPrimaryKey().' = '.$ids[0].$conditions;
997 $result =& $this->_findEvery($options);
998 if (!$expects_array && $result !== false){
999 return $result[0];
1001 return $result;
1002 break;
1004 default:
1005 $without_conditions = empty($options['conditions']) ? true : false;
1006 $ids_condition = $this->getPrimaryKey().' IN ('.join(', ',$ids).')';
1007 $options['conditions'] = $ids_condition.$conditions;
1009 $result =& $this->_findEvery($options);
1010 if(is_array($result) && (count($result) != $num_ids && $without_conditions)){
1011 $result = false;
1013 return $result;
1014 break;
1019 function _extractOptionsFromArgs(&$args)
1021 $last_arg = count($args)-1;
1022 return isset($args[$last_arg]) && is_array($args[$last_arg]) && $this->_isOptionsHash($args[$last_arg]) ? array_pop($args) : array();
1025 function _isOptionsHash($options)
1027 if (isset($options[0])){
1028 return false;
1030 $valid_keys = array('conditions', 'include', 'joins', 'limit', 'offset', 'group', 'order', 'sort', 'bind', 'select','select_prefix', 'readonly');
1031 foreach (array_keys($options) as $key){
1032 if (!in_array($key,$valid_keys)){
1033 return false;
1036 return true;
1039 function _extractConditionsFromArgs($args, $options)
1041 if(empty($args)){
1042 $fetch = 'all';
1043 } else {
1044 $fetch = $args[0];
1046 $num_args = count($args);
1048 // deprecated: acts like findFirstBySQL
1049 if ($num_args === 1 && !is_numeric($args[0]) && is_string($args[0]) && $args[0] != 'all' && $args[0] != 'first'){
1050 // $Users->find("last_name = 'Williams'"); => find('first',"last_name = 'Williams'");
1051 Ak::deprecateWarning(array("AR::find('%sql') is ambiguous and therefore deprecated, use AR::find('first',%sql) instead", '%sql'=>$args[0]));
1052 $options = array('conditions'=> $args[0]);
1053 return array('first',$options);
1054 } //end
1056 // set fetch_mode to 'all' if none is given
1057 if (!is_numeric($fetch) && !is_array($fetch) && $fetch != 'all' && $fetch != 'first') {
1058 array_unshift($args, 'all');
1059 $num_args = count($args);
1061 if ($num_args > 1) {
1062 if (is_string($args[1])){
1063 // $Users->find(:fetch_mode,"first_name = ?",'Tim');
1064 $fetch = array_shift($args);
1065 $options = array_merge($options, array('conditions'=>$args)); //TODO: merge_conditions
1066 }elseif (is_array($args[1])) {
1067 // $Users->find(:fetch_mode,array('first_name = ?,'Tim'));
1068 $fetch = array_shift($args);
1069 $options = array_merge($options, array('conditions'=>$args[0])); //TODO: merge_conditions
1073 return array($fetch,$options);
1076 function _sanitizeConditionsVariables(&$options)
1078 if(!empty($options['conditions']) && is_array($options['conditions'])){
1079 if (isset($options['conditions'][0]) && strstr($options['conditions'][0], '?') && count($options['conditions']) > 1){
1080 //array('conditions' => array("name=?",$name))
1081 $pattern = array_shift($options['conditions']);
1082 $options['bind'] = array_values($options['conditions']);
1083 $options['conditions'] = $pattern;
1084 }elseif (isset($options['conditions'][0])){
1085 //array('conditions' => array("user_name = :user_name", ':user_name' => 'hilario')
1086 $pattern = array_shift($options['conditions']);
1087 $options['conditions'] = str_replace(array_keys($options['conditions']), array_values($this->getSanitizedConditionsArray($options['conditions'])),$pattern);
1088 }else{
1089 //array('conditions' => array('user_name'=>'Hilario'))
1090 $options['conditions'] = join(' AND ',(array)$this->getAttributesQuoted($options['conditions']));
1096 function &findFirst()
1098 if(!isset($this->_activeRecordHasBeenInstantiated)){
1099 return Ak::handleStaticCall();
1101 $args = func_get_args();
1102 $result =& Ak::call_user_func_array(array(&$this,'find'), array_merge(array('first'),$args));
1103 return $result;
1106 function &findAll()
1108 if(!isset($this->_activeRecordHasBeenInstantiated)){
1109 return Ak::handleStaticCall();
1111 $args = func_get_args();
1112 $result =& Ak::call_user_func_array(array(&$this,'find'), array_merge(array('all'),$args));
1113 return $result;
1118 * Works like find_all, but requires a complete SQL string. Examples:
1119 * $Post->findBySql("SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id");
1120 * $Post->findBySql(array("SELECT * FROM posts WHERE author = ? AND created_on > ?", $author_id, $start_date));
1122 function &findBySql($sql, $limit = null, $offset = null, $bindings = null)
1124 if ($limit || $offset){
1125 Ak::deprecateWarning("You're calling AR::findBySql with \$limit or \$offset parameters. This has been deprecated.");
1126 $this->_db->addLimitAndOffset($sql, array('limit'=>$limit,'offset'=>$offset));
1128 if(!isset($this->_activeRecordHasBeenInstantiated)){
1129 return Ak::handleStaticCall();
1131 $objects = array();
1132 $records = $this->_db->select ($sql,'selecting');
1133 foreach ($records as $record){
1134 $objects[] =& $this->instantiate($this->getOnlyAvailableAttributes($record), false);
1136 return $objects;
1140 * This function pretends to emulate RoR finders until AkActiveRecord::addMethod becomes stable on future PHP versions.
1141 * @todo use PHP5 __call method for handling the magic finder methods like findFirstByUnsenameAndPassword('bermi','pass')
1143 function &findFirstBy()
1145 if(!isset($this->_activeRecordHasBeenInstantiated)){
1146 return Ak::handleStaticCall();
1148 $args = func_get_args();
1149 array_unshift($args,'first');
1150 $result =& Ak::call_user_func_array(array(&$this,'findBy'), $args);
1151 return $result;
1154 function &findLastBy()
1156 if(!isset($this->_activeRecordHasBeenInstantiated)){
1157 return Ak::handleStaticCall();
1159 $args = func_get_args();
1160 $options = $this->_extractOptionsFromArgs($args);
1161 $options['order'] = $this->getPrimaryKey().' DESC';
1162 array_push($args, $options);
1163 $result =& Ak::call_user_func_array(array(&$this,'findFirstBy'), $args);
1164 return $result;
1167 function &findAllBy()
1169 if(!isset($this->_activeRecordHasBeenInstantiated)){
1170 return Ak::handleStaticCall();
1172 $args = func_get_args();
1173 array_unshift($args,'all');
1174 $result =& Ak::call_user_func_array(array(&$this,'findBy'), $args);
1175 return $result;
1179 * This method allows you to use finders in a more flexible way like:
1181 * findBy('username AND password', $username, $password);
1182 * findBy('age > ? AND name:contains', 18, 'Joe');
1183 * findBy('is_active = true AND session_id', session_id());
1186 function &findBy()
1188 if(!isset($this->_activeRecordHasBeenInstantiated)){
1189 return Ak::handleStaticCall();
1191 $args = func_get_args();
1192 $find_by_sql = array_shift($args);
1193 if($find_by_sql == 'all' || $find_by_sql == 'first'){
1194 $fetch = $find_by_sql;
1195 $find_by_sql = array_shift($args);
1196 }else{
1197 $fetch = 'all';
1200 $options = $this->_extractOptionsFromArgs($args);
1202 $query_values = $args;
1203 $query_arguments_count = count($query_values);
1205 list($sql, $requested_args) = $this->_getFindBySqlAndColumns($find_by_sql, $query_values);
1207 if($query_arguments_count != count($requested_args)){
1208 trigger_error(Ak::t('Argument list did not match expected set. Requested arguments are:').join(', ',$requested_args),E_USER_ERROR);
1209 $false = false;
1210 return $false;
1213 $true_bool_values = array(true,1,'true','True','TRUE','1','y','Y','yes','Yes','YES','s','Si','SI','V','v','T','t');
1215 foreach ($requested_args as $k=>$v){
1216 switch ($this->getColumnType($v)) {
1217 case 'boolean':
1218 $query_values[$k] = in_array($query_values[$k],$true_bool_values) ? true : false;
1219 break;
1221 case 'date':
1222 case 'datetime':
1223 $query_values[$k] = str_replace('/','-', $this->castAttributeForDatabase($k,$query_values[$k],false));
1224 break;
1226 default:
1227 break;
1231 $conditions = array($sql);
1232 foreach ($query_values as $bind_value){
1233 $conditions[] = $bind_value;
1236 * @todo merge_conditions
1238 $options['conditions'] = $conditions;
1240 $result =& Ak::call_user_func_array(array(&$this,'find'), array($fetch,$options));
1241 return $result;
1245 function _getFindBySqlAndColumns($find_by_sql, &$query_values)
1247 $sql = str_replace(array('(',')','||','|','&&','&',' '),array(' ( ',' ) ',' OR ',' OR ',' AND ',' AND ',' '), $find_by_sql);
1248 $operators = array('AND','and','(',')','&','&&','NOT','<>','OR','|','||');
1249 $pieces = explode(' ',$sql);
1250 $pieces = array_diff($pieces,array(' ',''));
1251 $params = array_diff($pieces,$operators);
1252 $operators = array_diff($pieces,$params);
1254 $new_sql = '';
1255 $parameter_count = 0;
1256 $requested_args = array();
1257 foreach ($pieces as $piece){
1258 if(in_array($piece,$params) && $this->hasColumn($piece)){
1259 $new_sql .= $piece.' = ? ';
1260 $requested_args[$parameter_count] = $piece;
1261 $parameter_count++;
1262 }elseif (!in_array($piece,$operators)){
1264 if(strstr($piece,':')){
1265 $_tmp_parts = explode(':',$piece);
1266 if($this->hasColumn($_tmp_parts[0])){
1267 $query_values[$parameter_count] = isset($query_values[$parameter_count]) ? $query_values[$parameter_count] : $this->get($_tmp_parts[0]);
1268 switch (strtolower($_tmp_parts[1])) {
1269 case 'like':
1270 case '%like%':
1271 case 'is':
1272 case 'has':
1273 case 'contains':
1274 $query_values[$parameter_count] = '%'.$query_values[$parameter_count].'%';
1275 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1276 break;
1277 case 'like_left':
1278 case 'like%':
1279 case 'begins':
1280 case 'begins_with':
1281 case 'starts':
1282 case 'starts_with':
1283 $query_values[$parameter_count] = $query_values[$parameter_count].'%';
1284 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1285 break;
1286 case 'like_right':
1287 case '%like':
1288 case 'ends':
1289 case 'ends_with':
1290 case 'finishes':
1291 case 'finishes_with':
1292 $query_values[$parameter_count] = '%'.$query_values[$parameter_count];
1293 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1294 break;
1295 default:
1296 $query_values[$parameter_count] = $query_values[$parameter_count];
1297 $new_sql .= $_tmp_parts[0].' '.$_tmp_parts[1].' ? ';
1298 break;
1300 $requested_args[$parameter_count] = $_tmp_parts[0];
1301 $parameter_count++;
1302 }else {
1303 $new_sql .= $_tmp_parts[0];
1305 }else{
1306 $new_sql .= $piece.' ';
1308 }else{
1309 $new_sql .= $piece.' ';
1313 return array($new_sql, $requested_args);
1318 * Given a condition that uses bindings like "user = ? AND created_at > ?" will return a
1319 * string replacing the "?" bindings with the column values for current Active Record
1321 * @return string
1323 function _getVariableSqlCondition($variable_condition)
1325 $query_values = array();
1326 list($sql, $requested_columns) = $this->_getFindBySqlAndColumns($variable_condition, $query_values);
1327 $replacements = array();
1328 $sql = preg_replace('/((('.join($requested_columns,'|').') = \?) = \?)/','$2', $sql);
1329 foreach ($requested_columns as $attribute){
1330 $replacements[$attribute] = $this->castAttributeForDatabase($attribute, $this->get($attribute));
1332 return trim(preg_replace('/('.join('|',array_keys($replacements)).')\s+([^\?]+)\s+\?/e', "isset(\$replacements['\\1']) ? '\\1 \\2 '.\$replacements['\\1']:'\\1 \\2 null'", $sql));
1336 function constructFinderSql($options, $select_from_prefix = 'default')
1338 $sql = isset($options['select_prefix']) ? $options['select_prefix'] : ($select_from_prefix == 'default' ? 'SELECT '.(!empty($options['joins'])?$this->getTableName().'.':'') .'* FROM '.$this->getTableName() : $select_from_prefix);
1339 $sql .= !empty($options['joins']) ? ' '.$options['joins'] : '';
1341 $this->addConditions($sql, isset($options['conditions']) ? $options['conditions'] : array());
1343 // Create an alias for order
1344 if(empty($options['order']) && !empty($options['sort'])){
1345 $options['order'] = $options['sort'];
1348 $sql .= !empty($options['group']) ? ' GROUP BY '.$options['group'] : '';
1349 $sql .= !empty($options['order']) ? ' ORDER BY '.$options['order'] : '';
1351 $this->_db->addLimitAndOffset($sql,$options);
1353 return $sql;
1358 * Adds a sanitized version of $conditions to the $sql string. Note that the passed $sql string is changed.
1360 function addConditions(&$sql, $conditions = null, $table_alias = null)
1362 $concat = empty($sql) ? '' : ' WHERE ';
1363 if (empty($conditions) && $this->_getDatabaseType() == 'sqlite') $conditions = '1'; // sqlite HACK
1364 if(!empty($conditions)){
1365 $sql .= $concat.$conditions;
1366 $concat = ' AND ';
1369 if($this->getInheritanceColumn() !== false && $this->descendsFromActiveRecord($this)){
1370 $type_condition = $this->typeCondition($table_alias);
1371 $sql .= !empty($type_condition) ? $concat.$type_condition : '';
1373 return $sql;
1377 * Gets a sanitized version of the input array. Each element will be escaped
1379 function getSanitizedConditionsArray($conditions_array)
1381 $result = array();
1382 foreach ($conditions_array as $k=>$v){
1383 $k = str_replace(':','',$k); // Used for Oracle type bindings
1384 if($this->hasColumn($k)){
1385 $v = $this->castAttributeForDatabase($k, $v);
1386 $result[$k] = $v;
1389 return $result;
1394 * This functions is used to get the conditions from an AkRequest object
1396 function getConditions($conditions, $prefix = '', $model_name = null)
1398 $model_name = isset($model_name) ? $model_name : $this->getModelName();
1399 $model_conditions = !empty($conditions[$model_name]) ? $conditions[$model_name] : $conditions;
1400 if(is_a($this->$model_name)){
1401 $model_instance =& $this->$model_name;
1402 }else{
1403 $model_instance =& $this;
1405 $new_conditions = array();
1406 if(is_array($model_conditions)){
1407 foreach ($model_conditions as $col=>$value){
1408 if($model_instance->hasColumn($col)){
1409 $new_conditions[$prefix.$col] = $value;
1413 return $new_conditions;
1418 * @access private
1420 function _quoteColumnName($column_name)
1422 return $this->_db->nameQuote.$column_name.$this->_db->nameQuote;
1429 * EXPERIMENTAL: Will allow to create finders when PHP includes aggregate_methods as a stable feature on PHP4, for PHP5 we might use __call
1431 * @access private
1433 function _buildFinders($finderFunctions = array('find','findFirst'))
1435 if(!$this->_dynamicMethods){
1436 return;
1438 $columns = !is_array($this->_dynamicMethods) ? array_keys($this->getColumns()) : $this->_dynamicMethods;
1439 $class_name = 'ak_'.md5(serialize($columns));
1440 if(!class_exists($class_name)){
1441 $permutations = Ak::permute($columns);
1442 $implementations = '';
1443 foreach ($finderFunctions as $finderFunction){
1444 foreach ($permutations as $permutation){
1445 $permutation = array_map(array('AkInflector','camelize'),$permutation);
1446 foreach ($permutation as $k=>$v){
1447 $method_name = $finderFunction.'By'.join($permutation,'And');
1448 $implementation = 'function &'.$method_name.'(';
1449 $first_param = '';
1450 $params = '';
1451 $i = 1;
1452 foreach ($permutation as $column){
1453 $column = AkInflector::underscore($column);
1454 $params .= "$$column, ";
1455 $first_param .= "$column ";
1456 $i++;
1458 $implementation .= trim($params,' ,')."){\n";
1459 $implementation .= '$options = func_num_args() == '.$i.' ? func_get_arg('.($i-1).') : array();'."\n";
1460 $implementation .= 'return $this->'.$finderFunction.'By(\''.$first_param.'\', '.trim($params,' ,').", \$options);\n }\n";
1461 $implementations[$method_name] = $implementation;
1462 array_shift($permutation);
1466 eval('class '.$class_name.' { '.join("\n",$implementations).' } ');
1469 aggregate_methods(&$this, $class_name);
1474 * Finder methods must instantiate through this method to work with the single-table inheritance model and
1475 * eager loading associations.
1476 * that makes it possible to create objects of different types from the same table.
1478 function &instantiate($record, $set_as_new = true)
1480 $inheritance_column = $this->getInheritanceColumn();
1481 if(!empty($record[$inheritance_column])){
1482 $inheritance_column = $record[$inheritance_column];
1483 $inheritance_model_name = AkInflector::camelize($inheritance_column);
1484 @require_once(AkInflector::toModelFilename($inheritance_model_name));
1485 if(!class_exists($inheritance_model_name)){
1486 trigger_error($this->t("The single-table inheritance mechanism failed to locate the subclass: '%class_name'. ".
1487 "This error is raised because the column '%column' is reserved for storing the class in case of inheritance. ".
1488 "Please rename this column if you didn't intend it to be used for storing the inheritance class ".
1489 "or overwrite #{self.to_s}.inheritance_column to use another column for that information.",
1490 array('%class_name'=>$inheritance_model_name, '%column'=>$this->getInheritanceColumn())),E_USER_ERROR);
1494 $model_name = isset($inheritance_model_name) ? $inheritance_model_name : $this->getModelName();
1495 $object =& new $model_name('attributes', $record);
1497 $object->_newRecord = $set_as_new;
1499 (AK_CLI && AK_ENVIRONMENT == 'development') ? $object ->toString() : null;
1501 return $object;
1504 /*/Finding records*/
1509 Table inheritance
1510 ====================================================================
1512 function descendsFromActiveRecord(&$object)
1514 if(substr(strtolower(get_parent_class($object)),-12) == 'activerecord'){
1515 return true;
1517 if(!method_exists($object, 'getInheritanceColumn')){
1518 return false;
1520 $inheritance_column = $object->getInheritanceColumn();
1521 return !empty($inheritance_column);
1525 * Gets the column name for use with single table inheritance. Can be overridden in subclasses.
1527 function getInheritanceColumn()
1529 return empty($this->_inheritanceColumn) ? ($this->hasColumn('type') ? 'type' : false ) : $this->_inheritanceColumn;
1533 * Defines the column name for use with single table inheritance. Can be overridden in subclasses.
1535 function setInheritanceColumn($column_name)
1537 if(!$this->hasColumn($column_name)){
1538 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);
1539 return false;
1540 }elseif($this->getColumnType($column_name) != 'string'){
1541 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);
1542 return false;
1543 }else{
1544 $this->_inheritanceColumn = $column_name;
1545 return true;
1550 function getSubclasses()
1552 $current_class = get_class($this);
1553 $subclasses = array();
1554 $classes = get_declared_classes();
1556 while ($class = array_shift($classes)) {
1557 $parent_class = get_parent_class($class);
1558 if($parent_class == $current_class || in_array($parent_class,$subclasses)){
1559 $subclasses[] = $class;
1560 }elseif(!empty($parent_class)){
1561 $classes[] = $parent_class;
1564 $subclasses = array_unique(array_map(array(&$this,'_getModelName'),$subclasses));
1565 return $subclasses;
1569 function typeCondition($table_alias = null)
1571 $inheritance_column = $this->getInheritanceColumn();
1572 $type_condition = array();
1573 $table_name = $this->getTableName();
1574 $available_types = array_merge(array($this->getModelName()),$this->getSubclasses());
1575 foreach ($available_types as $subclass){
1576 $type_condition[] = ' '.($table_alias != null ? $table_alias : $table_name).'.'.$inheritance_column.' = \''.AkInflector::humanize(AkInflector::underscore($subclass)).'\' ';
1578 return empty($type_condition) ? '' : '('.join('OR',$type_condition).') ';
1581 /*/Table inheritance*/
1586 Setting Attributes
1587 ====================================================================
1588 See also: Getting Attributes, Model Attributes, Toggling Attributes, Counting Attributes.
1590 function setAttribute($attribute, $value, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS, $compose_after_set = true)
1592 if($attribute[0] == '_'){
1593 return false;
1596 if($this->isFrozen()){
1597 return false;
1599 if($inspect_for_callback_child_method === true && method_exists($this,'set'.AkInflector::camelize($attribute))){
1600 static $watchdog;
1601 $watchdog[$attribute] = @$watchdog[$attribute]+1;
1602 if($watchdog[$attribute] == 5000){
1603 if((!defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION')) || defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_SET_RECURSION){
1604 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);
1605 return false;
1608 $this->{$attribute.'_before_type_cast'} = $value;
1609 return $this->{'set'.AkInflector::camelize($attribute)}($value);
1611 if($this->hasAttribute($attribute)){
1612 $this->{$attribute.'_before_type_cast'} = $value;
1613 $this->$attribute = $value;
1614 if($compose_after_set && !empty($this->_combinedAttributes) && !$this->requiredForCombination($attribute)){
1615 $combined_attributes = $this->_getCombinedAttributesWhereThisAttributeIsUsed($attribute);
1616 foreach ($combined_attributes as $combined_attribute){
1617 $this->composeCombinedAttribute($combined_attribute);
1620 if ($compose_after_set && $this->isCombinedAttribute($attribute)){
1621 $this->decomposeCombinedAttribute($attribute);
1623 }elseif(substr($attribute,-12) == 'confirmation' && $this->hasAttribute(substr($attribute,0,-13))){
1624 $this->$attribute = $value;
1627 if($this->_internationalize){
1628 if(is_array($value)){
1629 $this->setAttributeLocales($attribute, $value);
1630 }elseif(is_string($inspect_for_callback_child_method)){
1631 $this->setAttributeByLocale($attribute, $value, $inspect_for_callback_child_method);
1632 }else{
1633 $this->_groupInternationalizedAttribute($attribute, $value);
1636 return true;
1639 function set($attribute, $value = null, $inspect_for_callback_child_method = true, $compose_after_set = true)
1641 if(is_array($attribute)){
1642 return $this->setAttributes($attribute);
1644 return $this->setAttribute($attribute, $value, $inspect_for_callback_child_method, $compose_after_set);
1648 * Allows you to set all the attributes at once by passing in an array with
1649 * keys matching the attribute names (which again matches the column names).
1650 * Sensitive attributes can be protected from this form of mass-assignment by
1651 * using the $this->setProtectedAttributes method. Or you can alternatively
1652 * specify which attributes can be accessed in with the $this->setAccessibleAttributes method.
1653 * Then all the attributes not included in that won?t be allowed to be mass-assigned.
1655 function setAttributes($attributes, $override_attribute_protection = false)
1657 $this->parseAkelosArgs($attributes);
1658 if(!$override_attribute_protection){
1659 $attributes = $this->removeAttributesProtectedFromMassAssignment($attributes);
1661 if(!empty($attributes) && is_array($attributes)){
1662 foreach ($attributes as $k=>$v){
1663 $this->setAttribute($k, $v);
1669 function setId($value)
1671 if($this->isFrozen()){
1672 return false;
1674 $pk = $this->getPrimaryKey();
1675 $this->$pk = $value;
1676 return true;
1680 /*/Setting Attributes*/
1683 Getting Attributes
1684 ====================================================================
1685 See also: Setting Attributes, Model Attributes, Toggling Attributes, Counting Attributes.
1688 function getAttribute($attribute, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS)
1690 if($attribute[0] == '_'){
1691 return false;
1694 if($inspect_for_callback_child_method === true && method_exists($this,'get'.AkInflector::camelize($attribute))){
1695 static $watchdog;
1696 $watchdog[@$attribute] = @$watchdog[$attribute]+1;
1697 if($watchdog[$attribute] == 5000){
1698 if((!defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION')) || defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_GET_RECURSION){
1699 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);
1700 return false;
1703 $value = $this->{'get'.AkInflector::camelize($attribute)}();
1704 return $this->getInheritanceColumn() === $attribute ? AkInflector::humanize(AkInflector::underscore($value)) : $value;
1706 if(isset($this->$attribute) || (!isset($this->$attribute) && $this->isCombinedAttribute($attribute))){
1707 if($this->hasAttribute($attribute)){
1708 if (!empty($this->_combinedAttributes) && $this->isCombinedAttribute($attribute)){
1709 $this->composeCombinedAttribute($attribute);
1711 return isset($this->$attribute) ? $this->$attribute : null;
1712 }elseif($this->_internationalize && $this->_isInternationalizeCandidate($attribute)){
1713 if(!empty($this->$attribute) && is_string($this->$attribute)){
1714 return $this->$attribute;
1716 $current_locale = $this->getCurrentLocale();
1717 if(!empty($this->$attribute[$current_locale]) && is_array($this->$attribute)){
1718 return $this->$attribute[$current_locale];
1720 return $this->getAttribute($current_locale.'_'.$attribute);
1724 if($this->_internationalize){
1725 return $this->getAttributeByLocale($attribute, is_bool($inspect_for_callback_child_method) ? $this->getCurrentLocale() : $inspect_for_callback_child_method);
1727 return null;
1730 function get($attribute = null, $inspect_for_callback_child_method = true)
1732 return !isset($attribute) ? $this->getAttributes($inspect_for_callback_child_method) : $this->getAttribute($attribute, $inspect_for_callback_child_method);
1736 * Returns an array of all the attributes with their names as keys and clones of their objects as values in case they are objects.
1738 function getAttributes()
1740 $attributes = array();
1741 $available_attributes = $this->getAvailableAttributes();
1742 foreach ($available_attributes as $available_attribute){
1743 $attribute = $this->getAttribute($available_attribute['name']);
1744 $attributes[$available_attribute['name']] = AK_PHP5 && is_object($attribute) ? clone($attribute) : $attribute;
1747 if($this->_internationalize){
1748 $current_locale = $this->getCurrentLocale();
1749 foreach ($this->getInternationalizedColumns() as $column=>$languages){
1750 if(empty($attributes[$column]) && isset($attributes[$current_locale.'_'.$column]) && in_array($current_locale,$languages)){
1751 $attributes[$column] = $attributes[$current_locale.'_'.$column];
1756 return $attributes;
1761 * 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.
1763 function getId()
1765 return $this->{$this->getPrimaryKey()};
1768 /*/Getting Attributes*/
1773 Toggling Attributes
1774 ====================================================================
1775 See also: Setting Attributes, Getting Attributes.
1778 * Turns an attribute that's currently true into false and vice versa. Returns attribute value.
1780 function toggleAttribute($attribute)
1782 $value = $this->getAttribute($attribute);
1783 $new_value = $value ? false : true;
1784 $this->setAttribute($attribute, $new_value);
1785 return $new_value;
1790 * Toggles the attribute and saves the record.
1792 function toggleAttributeAndSave($attribute)
1794 $value = $this->toggleAttribute($attribute);
1795 if($this->updateAttribute($attribute, $value)){
1796 return $value;
1798 return null;
1801 /*/Toggling Attributes*/
1805 Counting Attributes
1806 ====================================================================
1807 See also: Counting Records, Setting Attributes, Getting Attributes.
1811 * Increments the specified counter by one. So $DiscussionBoard->incrementCounter("post_count",
1812 * $discussion_board_id); would increment the "post_count" counter on the board responding to
1813 * $discussion_board_id. This is used for caching aggregate values, so that they doesn't need to
1814 * be computed every time. Especially important for looping over a collection where each element
1815 * require a number of aggregate values. Like the $DiscussionBoard that needs to list both the number of posts and comments.
1817 function incrementCounter($counter_name, $id, $difference = 1)
1819 return $this->updateAll("$counter_name = $counter_name + $difference", $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 1;
1823 * Works like AkActiveRecord::incrementCounter, but decrements instead.
1825 function decrementCounter($counter_name, $id, $difference = 1)
1827 return $this->updateAll("$counter_name = $counter_name - $difference", $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 1;
1831 * Initializes the attribute to zero if null and subtracts one. Only makes sense for number-based attributes. Returns attribute value.
1833 function decrementAttribute($attribute)
1835 if(!isset($this->$attribute)){
1836 $this->$attribute = 0;
1838 return $this->$attribute -= 1;
1842 * Decrements the attribute and saves the record.
1844 function decrementAndSaveAttribute($attribute)
1846 return $this->updateAttribute($attribute,$this->decrementAttribute($attribute));
1851 * Initializes the attribute to zero if null and adds one. Only makes sense for number-based attributes. Returns attribute value.
1853 function incrementAttribute($attribute)
1855 if(!isset($this->$attribute)){
1856 $this->$attribute = 0;
1858 return $this->$attribute += 1;
1862 * Increments the attribute and saves the record.
1864 function incrementAndSaveAttribute($attribute)
1866 return $this->updateAttribute($attribute,$this->incrementAttribute($attribute));
1869 /*/Counting Attributes*/
1872 Protecting attributes
1873 ====================================================================
1877 * If this macro is used, only those attributed named in it will be accessible
1878 * for mass-assignment, such as new ModelName($attributes) and $this->attributes($attributes).
1879 * This is the more conservative choice for mass-assignment protection.
1880 * If you'd rather start from an all-open default and restrict attributes as needed,
1881 * have a look at AkActiveRecord::setProtectedAttributes().
1883 function setAccessibleAttributes()
1885 $args = func_get_args();
1886 $this->_accessibleAttributes = array_unique(array_merge((array)$this->_accessibleAttributes, $args));
1890 * Attributes named in this macro are protected from mass-assignment, such as
1891 * new ModelName($attributes) and $this->attributes(attributes). Their assignment
1892 * will simply be ignored. Instead, you can use the direct writer methods to do assignment.
1893 * This is meant to protect sensitive attributes to be overwritten by URL/form hackers.
1895 * Example:
1896 * <code>
1897 * class Customer extends ActiveRecord
1899 * function Customer()
1901 * $this->setProtectedAttributes('credit_rating');
1905 * $Customer = new Customer('name' => 'David', 'credit_rating' => 'Excellent');
1906 * $Customer->credit_rating // => null
1907 * $Customer->attributes(array('description' => 'Jolly fellow', 'credit_rating' => 'Superb'));
1908 * $Customer->credit_rating // => null
1910 * $Customer->credit_rating = 'Average'
1911 * $Customer->credit_rating // => 'Average'
1912 * </code>
1914 function setProtectedAttributes()
1916 $args = func_get_args();
1917 $this->_protectedAttributes = array_unique(array_merge((array)$this->_protectedAttributes, $args));
1920 function removeAttributesProtectedFromMassAssignment($attributes)
1922 if(!empty($this->_accessibleAttributes) && is_array($this->_accessibleAttributes) && is_array($attributes)){
1923 foreach (array_keys($attributes) as $k){
1924 if(!in_array($k,$this->_accessibleAttributes)){
1925 unset($attributes[$k]);
1928 }elseif (!empty($this->_protectedAttributes) && is_array($this->_protectedAttributes) && is_array($attributes)){
1929 foreach (array_keys($attributes) as $k){
1930 if(in_array($k,$this->_protectedAttributes)){
1931 unset($attributes[$k]);
1935 return $attributes;
1938 /*/Protecting attributes*/
1942 Model Attributes
1943 ====================================================================
1944 See also: Getting Attributes, Setting Attributes.
1947 * Returns an array of all the attributes that have been specified for serialization as keys and the objects as values.
1949 function getSerializedAttributes()
1951 return isset($this->_serializedAttributes) ? $this->_serializedAttributes : array();
1954 function getAvailableAttributes()
1956 return array_merge($this->getColumns(), $this->getAvailableCombinedAttributes());
1959 function getAttributeCaption($attribute)
1961 return $this->t(AkInflector::humanize($attribute));
1965 * This function is useful in case you need to know if attributes have been assigned to an object.
1967 function hasAttributesDefined()
1969 $attributes = join('',$this->getAttributes());
1970 return empty($attributes);
1975 * Returns the primary key field.
1977 function getPrimaryKey()
1979 if(!isset($this->_primaryKey)){
1980 $this->setPrimaryKey();
1982 return $this->_primaryKey;
1985 function getColumnNames()
1987 if(empty($this->_columnNames)){
1988 $columns = $this->getColumns();
1989 foreach ($columns as $column_name=>$details){
1990 $this->_columnNames[$column_name] = isset($details->columnName) ? $this->t($details->columnName) : $this->getAttributeCaption($column_name);
1993 return $this->_columnNames;
1998 * Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count",
1999 * and columns used for single table inheritance has been removed.
2001 function getContentColumns()
2003 $inheritance_column = $this->getInheritanceColumn();
2004 $columns = $this->getColumns();
2005 foreach ($columns as $name=>$details){
2006 if((substr($name,-3) == '_id' || substr($name,-6) == '_count') ||
2007 !empty($details['primaryKey']) || ($inheritance_column !== false && $inheritance_column == $name)){
2008 unset($columns[$name]);
2011 return $columns;
2015 * Returns an array of names for the attributes available on this object sorted alphabetically.
2017 function getAttributeNames()
2019 if(!isset($this->_activeRecordHasBeenInstantiated)){
2020 return Ak::handleStaticCall();
2022 $attributes = array_keys($this->getAvailableAttributes());
2023 $names = array_combine($attributes,array_map(array(&$this,'getAttributeCaption'), $attributes));
2024 natsort($names);
2025 return $names;
2030 * Returns true if the specified attribute has been set by the user or by a database load and is neither null nor empty?
2032 function isAttributePresent($attribute)
2034 $value = $this->getAttribute($attribute);
2035 return !empty($value);
2039 * Returns true if given attribute exists for this Model.
2041 * @param string $attribute
2042 * @return boolean
2044 function hasAttribute ($attribute)
2046 empty($this->_columns) ? $this->getColumns() : $this->_columns; // HINT: only used by HasAndBelongsToMany joinObjects, if the table is not present yet!
2047 return isset($this->_columns[$attribute]) || (!empty($this->_combinedAttributes) && $this->isCombinedAttribute($attribute));
2050 /*/Model Attributes*/
2054 Combined attributes
2055 ====================================================================
2057 * The Akelos Framework has a handy way to represent combined fields.
2058 * You can add a new attribute to your models using a printf patter to glue
2059 * multiple parameters in a single one.
2061 * For example, If we set...
2062 * $this->addCombinedAttributeConfiguration('name', "%s %s", 'first_name', 'last_name');
2063 * $this->addCombinedAttributeConfiguration('date', "%04d-%02d-%02d", 'year', 'month', 'day');
2064 * $this->setAttributes('first_name=>','John','last_name=>','Smith','year=>',2005,'month=>',9,'day=>',27);
2066 * $this->name // will have "John Smith" as value and
2067 * $this->date // will be 2005-09-27
2069 * On the other hand if you do
2071 * $this->setAttribute('date', '2008-11-30');
2073 * All the 'year', 'month' and 'day' getters will be fired (if they exist) the following attributes will be set
2075 * $this->year // will be 2008
2076 * $this->month // will be 11 and
2077 * $this->day // will be 27
2079 * Sometimes you might need a pattern for composing and another for decomposing attributes. In this case you can specify
2080 * an array as the pattern values, where first element will be the composing pattern and second element will be used
2081 * for decomposing.
2083 * You can also specify a callback method from this object function instead of a pattern. You can also assign a callback
2084 * for composing and another for decomposing by passing their names as an array like on the patterns.
2086 * <?php
2087 * class User extends ActiveRecord
2088 * {
2089 * function User()
2091 * // You can use a multiple patterns array where "%s, %s" will be used for combining fields and "%[^,], %s" will be used
2092 * // for decomposing fields. (as you can see you can also use regular expressions on your patterns)
2093 * $User->addCombinedAttributeConfiguration('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name');
2095 * //Here we set email_link so compose_email_link() will be triggered for building up the field and parse_email_link will
2096 * // be used for getting the fields out
2097 * $User->addCombinedAttributeConfiguration('email_link', array("compose_email_link","parse_email_link"), 'email', 'name');
2099 * // We need to tell the ActiveRecord to load it's magic (see the example below for a simpler solution)
2100 * $attributes = (array)func_get_args();
2101 * return $this->init($attributes);
2104 * function compose_email_link()
2106 * $args = func_get_arg(0);
2107 * return "<a href=\'mailto:{$args[\'email\']}\'>{$args[\'name\']}</a>";
2108 * }
2109 * function parse_email_link($email_link)
2110 * {
2111 * $results = sscanf($email_link, "<a href=\'mailto:%[^\']\'>%[^<]</a>");
2112 * return array(\'email\'=>$results[0],\'name\'=>$results[1]);
2113 * }
2115 * }
2116 * ?>
2118 * You can also simplify your live by declaring the combined attributes as a class variable like:
2119 * <?php
2120 * class User extends ActiveRecord
2121 * {
2122 * var $combined_attributes array(
2123 * array('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name')
2124 * array('email_link', array("compose_email_link","parse_email_link"), 'email', 'name')
2125 * );
2127 * // ....
2128 * }
2129 * ?>
2134 * Returns true if given attribute is a combined attribute for this Model.
2136 * @param string $attribute
2137 * @return boolean
2139 function isCombinedAttribute ($attribute)
2141 return !empty($this->_combinedAttributes) && isset($this->_combinedAttributes[$attribute]);
2144 function addCombinedAttributeConfiguration($attribute)
2146 $args = is_array($attribute) ? $attribute : func_get_args();
2147 $columns = array_slice($args,2);
2148 $invalid_columns = array();
2149 foreach ($columns as $colum){
2150 if(!$this->hasAttribute($colum)){
2151 $invalid_columns[] = $colum;
2154 if(!empty($invalid_columns)){
2155 trigger_error(Ak::t('There was an error while setting the composed field "%field_name", the following mapping column/s "%columns" do not exist',
2156 array('%field_name'=>$args[0],'%columns'=>join(', ',$invalid_columns))), E_USER_ERROR);
2157 }else{
2158 $attribute = array_shift($args);
2159 $this->_combinedAttributes[$attribute] = $args;
2160 $this->composeCombinedAttribute($attribute);
2164 function composeCombinedAttributes()
2167 if(!empty($this->_combinedAttributes)){
2168 $attributes = array_keys($this->_combinedAttributes);
2169 foreach ($attributes as $attribute){
2170 $this->composeCombinedAttribute($attribute);
2175 function composeCombinedAttribute($combined_attribute)
2177 if($this->isCombinedAttribute($combined_attribute)){
2178 $config = $this->_combinedAttributes[$combined_attribute];
2179 $pattern = array_shift($config);
2181 $pattern = is_array($pattern) ? $pattern[0] : $pattern;
2182 $got = array();
2184 foreach ($config as $attribute){
2185 if(isset($this->$attribute)){
2186 $got[$attribute] = $this->getAttribute($attribute);
2189 if(count($got) === count($config)){
2190 $this->$combined_attribute = method_exists($this, $pattern) ? $this->{$pattern}($got) : vsprintf($pattern, $got);
2196 * @access private
2198 function _getCombinedAttributesWhereThisAttributeIsUsed($attribute)
2200 $result = array();
2201 foreach ($this->_combinedAttributes as $combined_attribute=>$settings){
2202 if(in_array($attribute,$settings)){
2203 $result[] = $combined_attribute;
2206 return $result;
2210 function requiredForCombination($attribute)
2212 foreach ($this->_combinedAttributes as $settings){
2213 if(in_array($attribute,$settings)){
2214 return true;
2217 return false;
2220 function hasCombinedAttributes()
2222 return count($this->getCombinedSubattributes()) === 0 ? false :true;
2225 function getCombinedSubattributes($attribute)
2227 $result = array();
2228 if(is_array($this->_combinedAttributes[$attribute])){
2229 $attributes = $this->_combinedAttributes[$attribute];
2230 array_shift($attributes);
2231 foreach ($attributes as $attribute_to_check){
2232 if(isset($this->_combinedAttributes[$attribute_to_check])){
2233 $result[] = $attribute_to_check;
2237 return $result;
2240 function decomposeCombinedAttributes()
2242 if(!empty($this->_combinedAttributes)){
2243 $attributes = array_keys($this->_combinedAttributes);
2244 foreach ($attributes as $attribute){
2245 $this->decomposeCombinedAttribute($attribute);
2250 function decomposeCombinedAttribute($combined_attribute, $used_on_combined_fields = false)
2252 if(isset($this->$combined_attribute) && $this->isCombinedAttribute($combined_attribute)){
2253 $config = $this->_combinedAttributes[$combined_attribute];
2254 $pattern = array_shift($config);
2255 $pattern = is_array($pattern) ? $pattern[1] : $pattern;
2257 if(method_exists($this, $pattern)){
2258 $pieces = $this->{$pattern}($this->$combined_attribute);
2259 if(is_array($pieces)){
2260 foreach ($pieces as $k=>$v){
2261 $is_combined = $this->isCombinedAttribute($k);
2262 if($is_combined){
2263 $this->decomposeCombinedAttribute($k);
2265 $this->setAttribute($k, $v, true, !$is_combined);
2267 if($is_combined && !$used_on_combined_fields){
2268 $combined_attributes_contained_on_this_attribute = $this->getCombinedSubattributes($combined_attribute);
2269 if(count($combined_attributes_contained_on_this_attribute)){
2270 $this->decomposeCombinedAttribute($combined_attribute, true);
2274 }else{
2275 $got = sscanf($this->$combined_attribute, $pattern);
2276 for ($x=0; $x<count($got); $x++){
2277 $attribute = $config[$x];
2278 $is_combined = $this->isCombinedAttribute($attribute);
2279 if($is_combined){
2280 $this->decomposeCombinedAttribute($attribute);
2282 $this->setAttribute($attribute, $got[$x], true, !$is_combined);
2288 function getAvailableCombinedAttributes()
2290 $combined_attributes = array();
2291 foreach ($this->_combinedAttributes as $attribute=>$details){
2292 $combined_attributes[$attribute] = array('name'=>$attribute, 'type'=>'string', 'path' => array_shift($details), 'uses'=>$details);
2294 return !empty($this->_combinedAttributes) && is_array($this->_combinedAttributes) ? $combined_attributes : array();
2297 /*/Combined attributes*/
2303 Database connection
2304 ====================================================================
2307 * Establishes the connection to the database. Accepts either a profile name specified in config/config.php or
2308 * an array as input where the 'type' key must be specified with the name of a database adapter (in lower-case)
2309 * example for regular databases (MySQL, Postgresql, etc):
2311 * $AkActiveRecord->establishConnection('development');
2312 * $AkActiveRecord->establishConnection('super_user');
2314 * $AkActiveRecord->establishConnection(
2315 * array(
2316 * 'type' => "mysql",
2317 * 'host' => "localhost",
2318 * 'username' => "myuser",
2319 * 'password' => "mypass",
2320 * 'database' => "somedatabase"
2321 * ));
2323 * Example for SQLite database:
2325 * $AkActiveRecord->establishConnection(
2326 * array(
2327 * 'type' => "sqlite",
2328 * 'dbfile' => "path/to/dbfile"
2332 function &establishConnection($specification_or_profile = AK_DEFAULT_DATABASE_PROFILE)
2334 $adapter =& AkDbAdapter::getInstance($specification_or_profile);
2335 return $this->setConnection(&$adapter);
2340 * Returns true if a connection that's accessible to this class have already been opened.
2342 function isConnected()
2344 return isset($this->_db);
2348 * Returns the connection currently associated with the class. This can also be used to
2349 * "borrow" the connection to do database work unrelated to any of the specific Active Records.
2351 function &getConnection()
2353 return $this->_db;
2357 * Sets the connection for the class.
2359 function &setConnection($db_adapter = null)
2361 if (is_null($db_adapter)){
2362 $db_adapter =& AkDbAdapter::getInstance();
2364 return $this->_db =& $db_adapter;
2368 * @access private
2370 function _getDatabaseType()
2372 return $this->_db->type();
2374 /*/Database connection*/
2378 Table Settings
2379 ====================================================================
2380 See also: Database Reflection.
2384 * Defines the primary key field ? can be overridden in subclasses.
2386 function setPrimaryKey($primary_key = 'id')
2388 if(!$this->hasColumn($primary_key)){
2389 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);
2390 }else {
2391 $this->_primaryKey = $primary_key;
2396 function getTableName($modify_for_associations = true)
2398 if(!isset($this->_tableName)){
2399 // We check if we are on a inheritance Table Model
2400 $this->getClassForDatabaseTableMapping();
2401 if(!isset($this->_tableName)){
2402 $this->setTableName();
2406 if($modify_for_associations && isset($this->_associationTablePrefixes[$this->_tableName])){
2407 return $this->_associationTablePrefixes[$this->_tableName];
2410 return $this->_tableName;
2413 function setTableName($table_name = null, $check_for_existence = AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES, $check_mode = false)
2415 static $available_tables;
2416 if(empty($table_name)){
2417 $table_name = AkInflector::tableize($this->getModelName());
2419 if($check_for_existence){
2420 if(!isset($available_tables) || $check_mode){
2421 if(!isset($this->_db)){
2422 $this->setConnection();
2424 if(empty($_SESSION['__activeRecordColumnsSettingsCache']['available_tables']) ||
2425 !AK_ACTIVE_RECORD_ENABLE_PERSISTENCE){
2426 $_SESSION['__activeRecordColumnsSettingsCache']['available_tables'] = $this->_db->availableTables();
2428 $available_tables = $_SESSION['__activeRecordColumnsSettingsCache']['available_tables'];
2430 if(!in_array($table_name,(array)$available_tables)){
2431 if(!$check_mode){
2432 trigger_error(Ak::t('Unable to set "%table_name" table for the model "%model".'.
2433 ' There is no "%table_name" available into current database layout.'.
2434 ' Set AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES constant to false in order to'.
2435 ' avoid table name validation',array('%table_name'=>$table_name,'%model'=>$this->getModelName())),E_USER_WARNING);
2437 return false;
2440 $this->_tableName = $table_name;
2441 return true;
2445 function getOnlyAvailableAttributes($attributes)
2447 $table_name = $this->getTableName();
2448 $ret_attributes = array();
2449 if(!empty($attributes) && is_array($attributes)){
2450 $available_attributes = $this->getAvailableAttributes();
2452 $keys = array_keys($attributes);
2453 $size = sizeOf($keys);
2454 for ($i=0; $i < $size; $i++){
2455 $k = str_replace($table_name.'.','',$keys[$i]);
2456 if(isset($available_attributes[$k]['name'][$k])){
2457 $ret_attributes[$k] =& $attributes[$keys[$i]];
2461 return $ret_attributes;
2464 function getColumnsForAttributes($attributes)
2466 $ret_attributes = array();
2467 $table_name = $this->getTableName();
2468 if(!empty($attributes) && is_array($attributes)){
2469 $columns = $this->getColumns();
2470 foreach ($attributes as $k=>$v){
2471 $k = str_replace($table_name.'.','',$k);
2472 if(isset($columns[$k]['name'][$k])){
2473 $ret_attributes[$k] = $v;
2477 return $ret_attributes;
2481 * Returns true if given attribute exists for this Model.
2483 * @param string $name Name of table to look in
2484 * @return boolean
2486 function hasColumn($column)
2488 empty($this->_columns) ? $this->getColumns() : $this->_columns;
2489 return isset($this->_columns[$column]);
2493 /*/Table Settings*/
2496 Database Reflection
2497 ====================================================================
2498 See also: Table Settings, Type Casting.
2503 * Initializes the attributes array with keys matching the columns from the linked table and
2504 * the values matching the corresponding default value of that column, so
2505 * that a new instance, or one populated from a passed-in array, still has all the attributes
2506 * that instances loaded from the database would.
2508 function attributesFromColumnDefinition()
2510 $attributes = array();
2512 foreach ((array)$this->getColumns() as $column_name=>$column_settings){
2513 if (!isset($column_settings['primaryKey']) && isset($column_settings['hasDefault'])) {
2514 $attributes[$column_name] = $this->_extractValueFromDefault($column_settings['defaultValue']);
2515 } else {
2516 $attributes[$column_name] = null;
2519 return $attributes;
2523 * Gets information from the database engine about a single table
2525 * @access private
2527 function _databaseTableInternals($table)
2529 if(empty($_SESSION['__activeRecordColumnsSettingsCache']['database_table_'.$table.'_internals']) || !AK_ACTIVE_RECORD_ENABLE_PERSISTENCE){
2530 $_SESSION['__activeRecordColumnsSettingsCache']['database_table_'.$table.'_internals'] = $this->_db->getColumnDetails($table);
2532 $cache[$table] = $_SESSION['__activeRecordColumnsSettingsCache']['database_table_'.$table.'_internals'];
2534 return $cache[$table];
2537 function getColumnsWithRegexBoundaries()
2539 $columns = array_keys($this->getColumns());
2540 foreach ($columns as $k=>$column){
2541 $columns[$k] = '/([^\.])\b('.$column.')\b/';
2543 return $columns;
2548 * If is the first time we use a model this function will run the installer for the model if it exists
2550 * @access private
2552 function _runCurrentModelInstallerIfExists(&$column_objects)
2554 static $installed_models = array();
2555 if(!defined('AK_AVOID_AUTOMATIC_ACTIVE_RECORD_INSTALLERS') && !in_array($this->getModelName(), $installed_models)){
2556 $installed_models[] = $this->getModelName();
2557 require_once(AK_LIB_DIR.DS.'AkInstaller.php');
2558 $installer_name = $this->getModelName().'Installer';
2559 $installer_file = AK_APP_DIR.DS.'installers'.DS.AkInflector::underscore($installer_name).'.php';
2560 if(file_exists($installer_file)){
2561 require_once($installer_file);
2562 if(class_exists($installer_name)){
2563 $Installer = new $installer_name();
2564 if(method_exists($Installer,'install')){
2565 $Installer->install();
2566 $column_objects = $this->_databaseTableInternals($this->getTableName());
2567 return !empty($column_objects);
2572 return false;
2577 * Returns an array of column objects for the table associated with this class.
2579 function getColumns($force_reload = false)
2581 if(empty($this->_columns) || $force_reload){
2582 $this->_columns = $this->getColumnSettings($force_reload);
2585 return (array)$this->_columns;
2588 function getColumnSettings($force_reload = false)
2590 if(empty($this->_columnsSettings) || $force_reload){
2591 $this->loadColumnsSettings($force_reload);
2592 $this->initiateColumnsToNull();
2594 return isset($this->_columnsSettings) ? $this->_columnsSettings : array();
2597 function loadColumnsSettings($force_reload = false)
2599 if(is_null($this->_db)){
2600 $this->setConnection();
2602 $this->_columnsSettings = $force_reload ? null : $this->_getPersistedTableColumnSettings();
2604 if(empty($this->_columnsSettings) || !AK_ACTIVE_RECORD_ENABLE_PERSISTENCE){
2605 if(empty($this->_dataDictionary)){
2606 $this->_dataDictionary =& $this->_db->getDictionary();
2609 $column_objects = $this->_databaseTableInternals($this->getTableName());
2611 if( !isset($this->_avoidTableNameValidation) &&
2612 !is_array($column_objects) &&
2613 !$this->_runCurrentModelInstallerIfExists($column_objects)){
2614 trigger_error(Ak::t('Ooops! Could not fetch details for the table %table_name.', array('%table_name'=>$this->getTableName())), E_USER_ERROR);
2615 return false;
2616 }elseif (empty($column_objects)){
2617 $this->_runCurrentModelInstallerIfExists($column_objects);
2619 if(is_array($column_objects)){
2620 foreach (array_keys($column_objects) as $k){
2621 $this->setColumnSettings($column_objects[$k]->name, $column_objects[$k]);
2624 if(!empty($this->_columnsSettings)){
2625 $this->_persistTableColumnSettings();
2628 return isset($this->_columnsSettings) ? $this->_columnsSettings : array();
2633 function setColumnSettings($column_name, $column_object)
2635 $this->_columnsSettings[$column_name] = array();
2636 $this->_columnsSettings[$column_name]['name'] = $column_object->name;
2638 if($this->_internationalize && $this->_isInternationalizeCandidate($column_object->name)){
2639 $this->_addInternationalizedColumn($column_object->name);
2642 $this->_columnsSettings[$column_name]['type'] = $this->getAkelosDataType($column_object);
2643 if(!empty($column_object->primary_key)){
2644 $this->_primaryKey = empty($this->_primaryKey) ? $column_object->name : $this->_primaryKey;
2645 $this->_columnsSettings[$column_name]['primaryKey'] = true;
2647 if(!empty($column_object->auto_increment)){
2648 $this->_columnsSettings[$column_name]['autoIncrement'] = true;
2650 if(!empty($column_object->has_default)){
2651 $this->_columnsSettings[$column_name]['hasDefault'] = true;
2653 if(!empty($column_object->not_null)){
2654 $this->_columnsSettings[$column_name]['notNull'] = true;
2656 if(!empty($column_object->max_length) && $column_object->max_length > 0){
2657 $this->_columnsSettings[$column_name]['maxLength'] = $column_object->max_length;
2659 if(!empty($column_object->scale) && $column_object->scale > 0){
2660 $this->_columnsSettings[$column_name]['scale'] = $column_object->scale;
2662 if(isset($column_object->default_value)){
2663 $this->_columnsSettings[$column_name]['defaultValue'] = $column_object->default_value;
2669 * Resets all the cached information about columns, which will cause they to be reloaded on the next request.
2671 function resetColumnInformation()
2673 if(isset($_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName()])){
2674 unset($_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName()]);
2676 $this->_clearPersitedColumnSettings();
2677 $this->_columnNames = $this->_columns = $this->_columnsSettings = $this->_contentColumns = array();
2681 * @access private
2683 function _getColumnsSettings()
2685 return $_SESSION['__activeRecordColumnsSettingsCache'];
2689 * @access private
2691 function _getModelColumnSettings()
2693 return $_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName()];
2697 * @access private
2699 function _persistTableColumnSettings()
2701 $_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName().'_column_settings'] = $this->_columnsSettings;
2705 * @access private
2707 function _getPersistedTableColumnSettings()
2709 $model_name = $this->getModelName();
2710 if(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA && !isset($_SESSION['__activeRecordColumnsSettingsCache']) && AK_CACHE_HANDLER > 0){
2711 $this->_loadPersistedColumnSetings();
2713 return isset($_SESSION['__activeRecordColumnsSettingsCache'][$model_name.'_column_settings']) ?
2714 $_SESSION['__activeRecordColumnsSettingsCache'][$model_name.'_column_settings'] : false;
2718 * @access private
2720 function _clearPersitedColumnSettings()
2722 if(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA && AK_CACHE_HANDLER > 0){
2723 $Cache =& Ak::cache();
2724 $Cache->init(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE);
2725 $Cache->clean('AkActiveRecord');
2730 * @access private
2732 function _savePersitedColumnSettings()
2734 if(isset($_SESSION['__activeRecordColumnsSettingsCache'])){
2735 $Cache =& Ak::cache();
2736 $Cache->init(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE);
2737 $Cache->save(serialize($_SESSION['__activeRecordColumnsSettingsCache']), 'active_record_db_cache', 'AkActiveRecord');
2742 * @access private
2744 function _loadPersistedColumnSetings()
2746 if(!isset($_SESSION['__activeRecordColumnsSettingsCache'])){
2747 $Cache =& Ak::cache();
2748 $Cache->init(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE);
2749 if($serialized_column_settings = $Cache->get('active_record_db_cache', 'AkActiveRecord') && !empty($serialized_column_settings)){
2750 $_SESSION['__activeRecordColumnsSettingsCache'] = @unserialize($serialized_column_settings);
2752 }elseif(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA){
2753 register_shutdown_function(array($this,'_savePersitedColumnSettings'));
2755 }else{
2756 $_SESSION['__activeRecordColumnsSettingsCache'] = array();
2761 function initiateAttributeToNull($attribute)
2763 if(!isset($this->$attribute)){
2764 $this->$attribute = null;
2768 function initiateColumnsToNull()
2770 if(isset($this->_columnsSettings) && is_array($this->_columnsSettings)){
2771 array_map(array(&$this,'initiateAttributeToNull'),array_keys($this->_columnsSettings));
2777 * Akelos data types are mapped to phpAdodb data types
2779 * Returns the Akelos data type for an Adodb Column Object
2781 * 'C'=>'string', // Varchar, capped to 255 characters.
2782 * 'X' => 'text' // Larger varchar, capped to 4000 characters (to be compatible with Oracle).
2783 * 'XL' => 'text' // For Oracle, returns CLOB, otherwise the largest varchar size.
2785 * 'C2' => 'string', // Multibyte varchar
2786 * 'X2' => 'string', // Multibyte varchar (largest size)
2788 * 'B' => 'binary', // BLOB (binary large object)
2790 * 'D' => array('date', 'datetime'), // Date (some databases do not support this, and we return a datetime type)
2791 * 'T' => array('datetime', 'timestamp'), //Datetime or Timestamp
2792 * 'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1)
2793 * 'I' => // Integer (mapped to I4)
2794 * 'I1' => 'integer', // 1-byte integer
2795 * 'I2' => 'integer', // 2-byte integer
2796 * 'I4' => 'integer', // 4-byte integer
2797 * 'I8' => 'integer', // 8-byte integer
2798 * 'F' => 'float', // Floating point number
2799 * 'N' => 'integer' // Numeric or decimal number
2801 * @return string One of this 'string','text','integer','float','datetime','timestamp',
2802 * 'time', 'name','date', 'binary', 'boolean'
2804 function getAkelosDataType(&$adodb_column_object)
2806 $config_var_name = AkInflector::variablize($adodb_column_object->name.'_data_type');
2807 if(!empty($this->{$config_var_name})){
2808 return $this->{$config_var_name};
2810 if(stristr($adodb_column_object->type, 'BLOB')){
2811 return 'binary';
2813 if(!empty($adodb_column_object->auto_increment)) {
2814 return 'serial';
2816 $meta_type = $this->_dataDictionary->MetaType($adodb_column_object);
2817 $adodb_data_types = array(
2818 'C'=>'string', // Varchar, capped to 255 characters.
2819 'X' => 'text', // Larger varchar, capped to 4000 characters (to be compatible with Oracle).
2820 'XL' => 'text', // For Oracle, returns CLOB, otherwise the largest varchar size.
2822 'C2' => 'string', // Multibyte varchar
2823 'X2' => 'string', // Multibyte varchar (largest size)
2825 'B' => 'binary', // BLOB (binary large object)
2827 'D' => array('date'), // Date
2828 'T' => array('datetime', 'timestamp'), //Datetime or Timestamp
2829 'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1)
2830 'R' => 'serial', // Serial Integer
2831 'I' => 'integer', // Integer (mapped to I4)
2832 'I1' => 'integer', // 1-byte integer
2833 'I2' => 'integer', // 2-byte integer
2834 'I4' => 'integer', // 4-byte integer
2835 'I8' => 'integer', // 8-byte integer
2836 'F' => 'float', // Floating point number
2837 'N' => 'decimal' // Numeric or decimal number
2840 $result = !isset($adodb_data_types[$meta_type]) ?
2841 'string' :
2842 (is_array($adodb_data_types[$meta_type]) ? $adodb_data_types[$meta_type][0] : $adodb_data_types[$meta_type]);
2844 if($result == 'text'){
2845 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 )){
2846 return 'string';
2850 if($this->_getDatabaseType() == 'mysql'){
2851 if($result == 'integer' && stristr($adodb_column_object->type, 'TINYINT')){
2852 return 'boolean';
2854 }elseif($this->_getDatabaseType() == 'postgre'){
2855 if($adodb_column_object->type == 'timestamp' || $result == 'datetime'){
2856 $adodb_column_object->max_length = 19;
2858 }elseif($this->_getDatabaseType() == 'sqlite'){
2859 if($result == 'integer' && (int)$adodb_column_object->max_length === 1 && stristr($adodb_column_object->type, 'TINYINT')){
2860 return 'boolean';
2861 }elseif($result == 'integer' && stristr($adodb_column_object->type, 'DOUBLE')){
2862 return 'float';
2866 if($result == 'datetime' && substr($adodb_column_object->name,-3) == '_on'){
2867 $result = 'date';
2870 return $result;
2875 * This method retrieves current class name that will be used to map
2876 * your database to this object.
2878 function getClassForDatabaseTableMapping()
2880 $class_name = get_class($this);
2881 if(is_subclass_of($this,'akactiverecord') || is_subclass_of($this,'AkActiveRecord')){
2882 $parent_class = get_parent_class($this);
2883 while (substr(strtolower($parent_class),-12) != 'activerecord'){
2884 $class_name = $parent_class;
2885 $parent_class = get_parent_class($parent_class);
2889 $class_name = $this->_getModelName($class_name);
2890 // This is an Active Record Inheritance so we set current table to parent table.
2891 if(!empty($class_name) && strtolower($class_name) != 'activerecord'){
2892 $this->_inheritanceClassName = $class_name;
2893 @$this->setTableName(AkInflector::tableize($class_name), false);
2896 return $class_name;
2899 function getDisplayField()
2901 return empty($this->displayField) && $this->hasAttribute('name') ? 'name' : (isset($this->displayField) && $this->hasAttribute($this->displayField) ? $this->displayField : $this->getPrimaryKey());
2904 function setDisplayField($attribute_name)
2906 if($this->hasAttribute($attribute_name)){
2907 $this->displayField = $attribute_name;
2908 return true;
2909 }else {
2910 return false;
2917 /*/Database Reflection*/
2920 Localization
2921 ====================================================================
2924 function t($string, $array = null)
2926 return Ak::t($string, $array, AkInflector::underscore($this->getModelName()));
2929 function getInternationalizedColumns()
2931 static $cache;
2932 $model = $this->getModelName();
2933 $available_locales = $this->getAvailableLocales();
2934 if(empty($cache[$model])){
2935 $cache[$model] = array();
2936 foreach ($this->getColumnSettings() as $column_name=>$details){
2937 if(!empty($details['i18n'])){
2938 $_tmp_pos = strpos($column_name,'_');
2939 $column = substr($column_name,$_tmp_pos+1);
2940 $lang = substr($column_name,0,$_tmp_pos);
2941 if(in_array($lang, $available_locales)){
2942 $cache[$model][$column] = empty($cache[$model][$column]) ? array($lang) :
2943 array_merge($cache[$model][$column] ,array($lang));
2949 return $cache[$model];
2952 function getAvailableLocales()
2954 static $available_locales;
2955 if(empty($available_locales)){
2956 if(defined('AK_ACTIVE_RECORD_DEFAULT_LOCALES')){
2957 $available_locales = Ak::stringToArray(AK_ACTIVE_RECORD_DEFAULT_LOCALES);
2958 }else{
2959 $available_locales = Ak::langs();
2962 return $available_locales;
2965 function getCurrentLocale()
2967 static $current_locale;
2968 if(empty($current_locale)){
2969 $current_locale = Ak::lang();
2970 $available_locales = $this->getAvailableLocales();
2971 if(!in_array($current_locale, $available_locales)){
2972 $current_locale = array_shift($available_locales);
2975 return $current_locale;
2979 function getAttributeByLocale($attribute, $locale)
2981 $internationalizable_columns = $this->getInternationalizedColumns();
2982 if(!empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){
2983 return $this->getAttribute($locale.'_'.$attribute);
2987 function getAttributeLocales($attribute)
2989 $attribute_locales = array();
2990 foreach ($this->getAvailableLocales() as $locale){
2991 if($this->hasColumn($locale.'_'.$attribute)){
2992 $attribute_locales[$locale] = $this->getAttributeByLocale($attribute, $locale);
2995 return $attribute_locales;
2998 function setAttributeByLocale($attribute, $value, $locale)
3000 $internationalizable_columns = $this->getInternationalizedColumns();
3002 if($this->_isInternationalizeCandidate($locale.'_'.$attribute) && !empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){
3003 $this->setAttribute($locale.'_'.$attribute, $value);
3008 function setAttributeLocales($attribute, $values = array())
3010 foreach ($values as $locale=>$value){
3011 $this->setAttributeByLocale($attribute, $value, $locale);
3016 * @access private
3018 function _delocalizeAttribute($attribute)
3020 return $this->_isInternationalizeCandidate($attribute) ? substr($attribute,3) : $attribute;
3024 * @access private
3026 function _isInternationalizeCandidate($column_name)
3028 $pos = strpos($column_name,'_');
3029 return $pos === 2 && in_array(substr($column_name,0,$pos),$this->getAvailableLocales());
3033 * @access private
3035 function _addInternationalizedColumn($column_name)
3037 $this->_columnsSettings[$column_name]['i18n'] = true;
3042 * Adds an internationalized attribute to an array containing other locales for the same column name
3044 * Example:
3045 * es_title and en_title will be available user title = array('es'=>'...', 'en' => '...')
3047 * @access private
3049 function _groupInternationalizedAttribute($attribute, $value)
3051 if($this->_internationalize && $this->_isInternationalizeCandidate($attribute)){
3052 if(!empty($this->$attribute)){
3053 $_tmp_pos = strpos($attribute,'_');
3054 $column = substr($attribute,$_tmp_pos+1);
3055 $lang = substr($attribute,0,$_tmp_pos);
3056 $this->$column = empty($this->$column) ? array() : $this->$column;
3057 if(empty($this->$column) || (!empty($this->$column) && is_array($this->$column))){
3058 $this->$column = empty($this->$column) ? array($lang=>$value) : array_merge($this->$column,array($lang=>$value));
3064 /*/Localization*/
3070 Type Casting
3071 ====================================================================
3072 See also: Database Reflection.
3075 function getAttributesBeforeTypeCast()
3077 $attributes_array = array();
3078 $available_attributes = $this->getAvailableAttributes();
3079 foreach ($available_attributes as $attribute){
3080 $attribute_value = $this->getAttributeBeforeTypeCast($attribute['name']);
3081 if(!empty($attribute_value)){
3082 $attributes_array[$attribute['name']] = $attribute_value;
3085 return $attributes_array;
3089 function getAttributeBeforeTypeCast($attribute)
3091 if(isset($this->{$attribute.'_before_type_cast'})){
3092 return $this->{$attribute.'_before_type_cast'};
3094 return null;
3097 function quotedId()
3099 return $this->castAttributeForDatabase($this->getPrimaryKey(), $this->getId());
3103 * Specifies that the attribute by the name of attr_name should be serialized before saving to the database and unserialized after loading from the database. If class_name is specified, the serialized object must be of that class on retrieval, as a new instance of the object will be loaded with serialized values.
3105 function setSerializeAttribute($attr_name, $class_name = null)
3107 if($this->hasColumn($attr_name)){
3108 $this->_serializedAttributes[$attr_name] = $class_name;
3112 function getAvailableAttributesQuoted()
3114 return $this->getAttributesQuoted($this->getAttributes());
3118 function getAttributesQuoted($attributes_array)
3120 $set = array();
3121 $attributes_array = $this->getSanitizedConditionsArray($attributes_array);
3122 foreach (array_diff($attributes_array,array('')) as $k=>$v){
3123 $set[$k] = $k.'='.$v;
3126 return $set;
3129 function getColumnType($column_name)
3131 empty($this->_columns) ? $this->getColumns() : null;
3132 return empty($this->_columns[$column_name]['type']) ? false : $this->_columns[$column_name]['type'];
3135 function getColumnScale($column_name)
3137 empty($this->_columns) ? $this->getColumns() : null;
3138 return empty($this->_columns[$column_name]['scale']) ? false : $this->_columns[$column_name]['scale'];
3141 function castAttributeForDatabase($column_name, $value, $add_quotes = true)
3143 $result = '';
3144 switch ($this->getColumnType($column_name)) {
3145 case 'datetime':
3146 if(!empty($value)){
3147 $date_time = $this->_db->quote_datetime(Ak::getTimestamp($value));
3148 $result = $add_quotes ? $date_time : trim($date_time ,"'");
3149 }else{
3150 $result = 'null';
3152 break;
3154 case 'date':
3155 if(!empty($value)){
3156 $date = $this->_db->quote_date(Ak::getTimestamp($value));
3157 $result = $add_quotes ? $date : trim($date, "'");
3158 }else{
3159 $result = 'null';
3161 break;
3163 case 'boolean':
3164 $result = is_null($value) ? 'null' : (!empty($value) ? "'1'" : "'0'");
3165 break;
3167 case 'binary':
3168 if($this->_getDatabaseType() == 'postgre'){
3169 $result = is_null($value) ? 'null::bytea ' : " '".$this->_db->escape_blob($value)."'::bytea ";
3170 }else{
3171 $result = is_null($value) ? 'null' : ($add_quotes ? $this->_db->quote_string($value) : $value);
3173 break;
3175 case 'decimal':
3176 if(is_null($value)){
3177 $result = 'null';
3178 }else{
3179 if($scale = $this->getColumnScale($column_name)){
3180 $value = number_format($value, $scale, '.', '');
3182 $result = $add_quotes ? $this->_db->quote_string($value) : $value;
3184 break;
3186 case 'serial':
3187 case 'integer':
3188 $result = (is_null($value) || $value==='') ? 'null' : (integer)$value;
3189 break;
3191 case 'float':
3192 $result = (empty($value) && $value !== 0) ? 'null' : (is_numeric($value) ? $value : $this->_db->quote_string($value));
3193 $result = !empty($this->_columns[$column_name]['notNull']) && $result == 'null' && $this->_getDatabaseType() == 'sqlite' ? '0' : $result;
3194 break;
3196 default:
3197 $result = is_null($value) ? 'null' : ($add_quotes ? $this->_db->quote_string($value) : $value);
3198 break;
3201 // !! nullable vs. not nullable !!
3202 return empty($this->_columns[$column_name]['notNull']) ? ($result === '' ? "''" : $result) : ($result === 'null' ? '' : $result);
3205 function castAttributeFromDatabase($column_name, $value)
3207 if($this->hasColumn($column_name)){
3208 $column_type = $this->getColumnType($column_name);
3210 if($column_type){
3211 if('integer' == $column_type){
3212 return is_null($value) ? null : (integer)$value;
3213 //return is_null($value) ? null : $value; // maybe for bigint we can do this
3214 }elseif('boolean' == $column_type){
3215 if (is_null($value)) {
3216 return null;
3218 if ($this->_getDatabaseType()=='postgre'){
3219 return $value=='t' ? true : false;
3221 return (integer)$value === 1 ? true : false;
3222 }elseif(!empty($value) && 'date' == $column_type && strstr(trim($value),' ')){
3223 return substr($value,0,10) == '0000-00-00' ? null : str_replace(substr($value,strpos($value,' ')), '', $value);
3224 }elseif (!empty($value) && 'datetime' == $column_type && substr($value,0,10) == '0000-00-00'){
3225 return null;
3226 }elseif ('binary' == $column_type && $this->_getDatabaseType() == 'postgre'){
3227 $value = $this->_db->unescape_blob($value);
3228 $value = empty($value) || trim($value) == 'null' ? null : $value;
3232 return $value;
3237 * Joins date arguments into a single attribute. Like the array generated by the date_helper, so
3238 * array('published_on(1i)' => 2002, 'published_on(2i)' => 'January', 'published_on(3i)' => 24)
3239 * Will be converted to array('published_on'=>'2002-01-24')
3241 * @access private
3243 function _castDateParametersFromDateHelper_(&$params)
3245 if(empty($params)){
3246 return;
3248 $date_attributes = array();
3249 foreach ($params as $k=>$v) {
3250 if(preg_match('/^([A-Za-z0-9_]+)\(([1-5]{1})i\)$/',$k,$match)){
3251 $date_attributes[$match[1]][$match[2]] = $v;
3252 $this->$k = $v;
3253 unset($params[$k]);
3256 foreach ($date_attributes as $attribute=>$date){
3257 $params[$attribute] = trim(@$date[1].'-'.@$date[2].'-'.@$date[3].' '.@$date[4].':'.@$date[5].':'.@$date[6],' :-');
3262 * @access private
3264 function _addBlobQueryStack($column_name, $blob_value)
3266 $this->_BlobQueryStack[$column_name] = $blob_value;
3270 * @access private
3272 function _updateBlobFields($condition)
3274 if(!empty($this->_BlobQueryStack) && is_array($this->_BlobQueryStack)){
3275 foreach ($this->_BlobQueryStack as $column=>$value){
3276 $this->_db->UpdateBlob($this->getTableName(), $column, $value, $condition);
3278 $this->_BlobQueryStack = null;
3282 /*/Type Casting*/
3285 Optimistic Locking
3286 ====================================================================
3288 * Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
3289 * record increments the lock_version column and the locking facilities ensure that records instantiated twice
3290 * will let the last one saved return false on save() if the first was also updated. Example:
3292 * $p1 = new Person(1);
3293 * $p2 = new Person(1);
3295 * $p1->first_name = "Michael";
3296 * $p1->save();
3298 * $p2->first_name = "should fail";
3299 * $p2->save(); // Returns false
3301 * You're then responsible for dealing with the conflict by checking the return value of save(); and either rolling back, merging,
3302 * or otherwise apply the business logic needed to resolve the conflict.
3304 * You must ensure that your database schema defaults the lock_version column to 0.
3306 * This behavior can be turned off by setting <tt>AkActiveRecord::lock_optimistically = false</tt>.
3308 function isLockingEnabled()
3310 return (!isset($this->lock_optimistically) || $this->lock_optimistically !== false) && $this->hasColumn('lock_version');
3312 /*/Optimistic Locking*/
3316 Callbacks
3317 ====================================================================
3318 See also: Observers.
3320 * Callbacks are hooks into the life-cycle of an Active Record object that allows you to trigger logic
3321 * before or after an alteration of the object state. This can be used to make sure that associated and
3322 * dependent objects are deleted when destroy is called (by overwriting beforeDestroy) or to massage attributes
3323 * before they're validated (by overwriting beforeValidation). As an example of the callbacks initiated, consider
3324 * the AkActiveRecord->save() call:
3326 * - (-) save()
3327 * - (-) needsValidation()
3328 * - (1) beforeValidation()
3329 * - (2) beforeValidationOnCreate() / beforeValidationOnUpdate()
3330 * - (-) validate()
3331 * - (-) validateOnCreate()
3332 * - (4) afterValidation()
3333 * - (5) afterValidationOnCreate() / afterValidationOnUpdate()
3334 * - (6) beforeSave()
3335 * - (7) beforeCreate() / beforeUpdate()
3336 * - (-) create()
3337 * - (8) afterCreate() / afterUpdate()
3338 * - (9) afterSave()
3339 * - (10) afterDestroy()
3340 * - (11) beforeDestroy()
3343 * That's a total of 15 callbacks, which gives you immense power to react and prepare for each state in the
3344 * Active Record lifecycle.
3346 * Examples:
3347 * class CreditCard extends ActiveRecord
3349 * // Strip everything but digits, so the user can specify "555 234 34" or
3350 * // "5552-3434" or both will mean "55523434"
3351 * function beforeValidationOnCreate
3353 * if(!empty($this->number)){
3354 * $this->number = ereg_replace('[^0-9]*','',$this->number);
3359 * class Subscription extends ActiveRecord
3361 * // Note: This is not implemented yet
3362 * var $beforeCreate = 'recordSignup';
3364 * function recordSignup()
3366 * $this->signed_up_on = date("Y-m-d");
3370 * class Firm extends ActiveRecord
3372 * //Destroys the associated clients and people when the firm is destroyed
3373 * // Note: This is not implemented yet
3374 * var $beforeDestroy = array('destroyAssociatedPeople', 'destroyAssociatedClients');
3376 * function destroyAssociatedPeople()
3378 * $Person = new Person();
3379 * $Person->destroyAll("firm_id=>", $this->id);
3382 * function destroyAssociatedClients()
3384 * $Client = new Client();
3385 * $Client->destroyAll("client_of=>", $this->id);
3390 * == Canceling callbacks ==
3392 * If a before* callback returns false, all the later callbacks and the associated action are cancelled. If an after* callback returns
3393 * false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
3394 * defined as methods on the model, which are called last.
3396 * Override this methods to hook Active Records
3398 * @access public
3401 function beforeCreate(){return true;}
3402 function beforeValidation(){return true;}
3403 function beforeValidationOnCreate(){return true;}
3404 function beforeValidationOnUpdate(){return true;}
3405 function beforeSave(){return true;}
3406 function beforeUpdate(){return true;}
3407 function afterUpdate(){return true;}
3408 function afterValidation(){return true;}
3409 function afterValidationOnCreate(){return true;}
3410 function afterValidationOnUpdate(){return true;}
3411 function afterCreate(){return true;}
3412 function afterDestroy(){return true;}
3413 function beforeDestroy(){return true;}
3414 function afterSave(){return true;}
3416 /*/Callbacks*/
3420 Transactions
3421 ====================================================================
3423 * Transaction support for database operations
3425 * Transactions are enabled automatically for Active record objects, But you can nest transactions within models.
3426 * This transactions are nested, and only the outermost will be executed
3428 * $User->transactionStart();
3429 * $User->create('username'=>'Bermi');
3430 * $Members->create('username'=>'Bermi');
3432 * if(!checkSomething()){
3433 * $User->transactionFail();
3436 * $User->transactionComplete();
3439 function transactionStart()
3441 return $this->_db->startTransaction();
3444 function transactionComplete()
3446 return $this->_db->stopTransaction();
3449 function transactionFail()
3451 $this->_db->failTransaction();
3452 return false;
3455 function transactionHasFailed()
3457 return $this->_db->hasTransactionFailed();
3460 /*/Transactions*/
3466 Validators
3467 ====================================================================
3468 See also: Error Handling.
3470 * Active Records implement validation by overwriting AkActiveRecord::validate (or the variations, validateOnCreate and
3471 * validateOnUpdate). Each of these methods can inspect the state of the object, which usually means ensuring
3472 * that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
3474 * Example:
3476 * class Person extends ActiveRecord
3478 * function validate()
3480 * $this->addErrorOnEmpty(array('first_name', 'last_name'));
3481 * if(!preg_match('/[0-9]{4,12}/', $this->phone_number)){
3482 * $this->addError("phone_number", "has invalid format");
3486 * function validateOnCreate() // is only run the first time a new object is saved
3488 * if(!isValidDiscount($this->membership_discount)){
3489 * $this->addError("membership_discount", "has expired");
3493 * function validateOnUpdate()
3495 * if($this->countChangedAttributes() == 0){
3496 * $this->addErrorToBase("No changes have occurred");
3501 * $Person = new Person(array("first_name" => "David", "phone_number" => "what?"));
3502 * $Person->save(); // => false (and doesn't do the save);
3503 * $Person->hasErrors(); // => false
3504 * $Person->countErrors(); // => 2
3505 * $Person->getErrorsOn("last_name"); // => "can't be empty"
3506 * $Person->getErrorsOn("phone_number"); // => "has invalid format"
3507 * $Person->yieldEachFullError(); // => "Last name can't be empty \n Phone number has invalid format"
3509 * $Person->setAttributes(array("last_name" => "Heinemeier", "phone_number" => "555-555"));
3510 * $Person->save(); // => true (and person is now saved in the database)
3512 * An "_errors" array is available for every Active Record.
3517 * Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
3519 * Model:
3520 * class Person extends ActiveRecord
3522 * function validate()
3524 * $this->validatesConfirmationOf('password');
3525 * $this->validatesConfirmationOf('email_address', "should match confirmation");
3529 * View:
3530 * <?=$form_helper->password_field("person", "password"); ?>
3531 * <?=$form_helper->password_field("person", "password_confirmation"); ?>
3533 * The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual.
3534 * It exists only as an in-memory variable for validating the password. This check is performed only if password_confirmation
3535 * is not null.
3538 function validatesConfirmationOf($attribute_names, $message = 'confirmation')
3540 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3541 $attribute_names = Ak::toArray($attribute_names);
3542 foreach ($attribute_names as $attribute_name){
3543 $attribute_accessor = $attribute_name.'_confirmation';
3544 if(isset($this->$attribute_accessor) && @$this->$attribute_accessor != @$this->$attribute_name){
3545 $this->addError($attribute_name, $message);
3551 * Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
3553 * class Person extends ActiveRecord
3555 * function validateOnCreate()
3557 * $this->validatesAcceptanceOf('terms_of_service');
3558 * $this->validatesAcceptanceOf('eula', "must be abided");
3562 * The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed only if
3563 * terms_of_service is not null.
3566 * @param accept 1
3567 * Specifies value that is considered accepted. The default value is a string "1", which makes it easy to relate to an HTML checkbox.
3569 function validatesAcceptanceOf($attribute_names, $message = 'accepted', $accept = 1)
3571 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3573 $attribute_names = Ak::toArray($attribute_names);
3574 foreach ($attribute_names as $attribute_name){
3575 if(@$this->$attribute_name != $accept){
3576 $this->addError($attribute_name, $message);
3582 * Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
3584 * class Book extends ActiveRecord
3586 * var $has_many = 'pages';
3587 * var $belongs_to = 'library';
3589 * function validate(){
3590 * $this->validatesAssociated(array('pages', 'library'));
3595 * Warning: If, after the above definition, you then wrote:
3597 * class Page extends ActiveRecord
3599 * var $belongs_to = 'book';
3600 * function validate(){
3601 * $this->validatesAssociated('book');
3605 * ...this would specify a circular dependency and cause infinite recursion.
3607 * NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
3608 * is both present and guaranteed to be valid, you also need to use validatesPresenceOf.
3610 function validatesAssociated($attribute_names, $message = 'invalid')
3612 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3613 $attribute_names = Ak::toArray($attribute_names);
3614 foreach ($attribute_names as $attribute_name){
3615 if(!empty($this->$attribute_name)){
3616 if(is_array($this->$attribute_name)){
3617 foreach(array_keys($this->$attribute_name) as $k){
3618 if(method_exists($this->{$attribute_name}[$k],'isValid') && !$this->{$attribute_name}[$k]->isValid()){
3619 $this->addError($attribute_name, $message);
3622 }elseif (method_exists($this->$attribute_name,'isValid') && !$this->$attribute_name->isValid()){
3623 $this->addError($attribute_name, $message);
3629 function isBlank($value = null)
3631 return trim((string)$value) == '';
3635 * Validates that the specified attributes are not blank (as defined by AkActiveRecord::isBlank()).
3637 function validatesPresenceOf($attribute_names, $message = 'blank')
3639 $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 $this->addErrorOnBlank($attribute_name, $message);
3648 * Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
3650 * class Person extends ActiveRecord
3652 * function validate()
3654 * $this->validatesLengthOf('first_name', array('maximum'=>30));
3655 * $this->validatesLengthOf('last_name', array('maximum'=>30,'message'=> "less than %d if you don't mind"));
3656 * $this->validatesLengthOf('last_name', array('within'=>array(7, 32)));
3657 * $this->validatesLengthOf('last_name', array('in'=>array(6, 20), 'too_long' => "pick a shorter name", 'too_short' => "pick a longer name"));
3658 * $this->validatesLengthOf('fav_bra_size', array('minimum'=>1, 'too_short'=>"please enter at least %d character"));
3659 * $this->validatesLengthOf('smurf_leader', array('is'=>4, 'message'=>"papa is spelled with %d characters... don't play me."));
3663 * 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.
3665 * Configuration options:
3666 * <tt>minimum</tt> - The minimum size of the attribute
3667 * <tt>maximum</tt> - The maximum size of the attribute
3668 * <tt>is</tt> - The exact size of the attribute
3669 * <tt>within</tt> - A range specifying the minimum and maximum size of the attribute
3670 * <tt>in</tt> - A synonym(or alias) for :within
3671 * <tt>allow_null</tt> - Attribute may be null; skip validation.
3673 * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default "is" "is too long (max is %d characters)")
3674 * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default "is" "is too short (min is %d characters)")
3675 * <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)")
3676 * <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
3678 function validatesLengthOf($attribute_names, $options = array())
3680 // Merge given options with defaults.
3681 $default_options = array(
3682 'too_long' => $this->_defaultErrorMessages['too_long'],
3683 'too_short' => $this->_defaultErrorMessages['too_short'],
3684 'wrong_length' => $this->_defaultErrorMessages['wrong_length'],
3685 'allow_null' => false
3688 $range_options = array();
3689 foreach ($options as $k=>$v){
3690 if(in_array($k,array('minimum','maximum','is','in','within'))){
3691 $range_options[$k] = $v;
3692 $option = $k;
3693 $option_value = $v;
3697 // Ensure that one and only one range option is specified.
3698 switch (count($range_options)) {
3699 case 0:
3700 trigger_error(Ak::t('Range unspecified. Specify the "within", "maximum", "minimum, or "is" option.'), E_USER_ERROR);
3701 return false;
3702 break;
3703 case 1:
3704 $options = array_merge($default_options, $options);
3705 break;
3706 default:
3707 trigger_error(Ak::t('Too many range options specified. Choose only one.'), E_USER_ERROR);
3708 return false;
3709 break;
3713 switch ($option) {
3714 case 'within':
3715 case 'in':
3716 if(empty($option_value) || !is_array($option_value) || count($option_value) != 2 || !is_numeric($option_value[0]) || !is_numeric($option_value[1])){
3717 trigger_error(Ak::t('%option must be a Range (array(min, max))',array('%option',$option)), E_USER_ERROR);
3718 return false;
3720 $attribute_names = Ak::toArray($attribute_names);
3722 foreach ($attribute_names as $attribute_name){
3723 if((!empty($option['allow_null']) && !isset($this->$attribute_name)) || (Ak::size($this->$attribute_name)) < $option_value[0]){
3724 $this->addError($attribute_name, sprintf($options['too_short'], $option_value[0]));
3725 }elseif((!empty($option['allow_null']) && !isset($this->$attribute_name)) || (Ak::size($this->$attribute_name)) > $option_value[1]){
3726 $this->addError($attribute_name, sprintf($options['too_long'], $option_value[1]));
3729 break;
3731 case 'is':
3732 case 'minimum':
3733 case 'maximum':
3735 if(empty($option_value) || !is_numeric($option_value) || $option_value <= 0){
3736 trigger_error(Ak::t('%option must be a nonnegative Integer',array('%option',$option_value)), E_USER_ERROR);
3737 return false;
3740 // Declare different validations per option.
3741 $validity_checks = array('is' => "==", 'minimum' => ">=", 'maximum' => "<=");
3742 $message_options = array('is' => 'wrong_length', 'minimum' => 'too_short', 'maximum' => 'too_long');
3744 $message = sprintf(!empty($options['message']) ? $options['message'] : $options[$message_options[$option]],$option_value);
3746 $attribute_names = Ak::toArray($attribute_names);
3747 foreach ($attribute_names as $attribute_name){
3748 if((!$options['allow_null'] && !isset($this->$attribute_name)) ||
3749 eval("return !(".Ak::size(@$this->$attribute_name)." {$validity_checks[$option]} $option_value);")){
3750 $this->addError($attribute_name, $message);
3753 break;
3754 default:
3755 break;
3758 return true;
3761 function validatesSizeOf($attribute_names, $options = array())
3763 return validatesLengthOf($attribute_names, $options);
3767 * Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
3768 * can be named "davidhh".
3770 * class Person extends ActiveRecord
3772 * function validate()
3774 * $this->validatesUniquenessOf('passport_number');
3775 * $this->validatesUniquenessOf('user_name', array('scope' => "account_id"));
3779 * It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
3780 * making sure that a teacher can only be on the schedule once per semester for a particular class.
3782 * class TeacherSchedule extends ActiveRecord
3784 * function validate()
3786 * $this->validatesUniquenessOf('passport_number');
3787 * $this->validatesUniquenessOf('teacher_id', array('scope' => array("semester_id", "class_id"));
3792 * 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
3793 * attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
3795 * Configuration options:
3796 * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
3797 * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope"
3798 * <tt>case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (true by default).
3799 * <tt>if</tt> - Specifies a method to call or a string to evaluate to determine if the validation should
3800 * occur (e.g. 'if' => 'allowValidation', or 'if' => '$this->signup_step > 2'). The
3801 * method, or string should return or evaluate to a true or false value.
3803 function validatesUniquenessOf($attribute_names, $options = array())
3805 $default_options = array('case_sensitive'=>true, 'message'=>'taken');
3806 $options = array_merge($default_options, $options);
3808 if(!empty($options['if'])){
3809 if(method_exists($this,$options['if'])){
3810 if($this->{$options['if']}() === false){
3811 return true;
3813 }else {
3814 eval('$__eval_result = ('.rtrim($options['if'],';').');');
3815 if(empty($__eval_result)){
3816 return true;
3821 $message = isset($this->_defaultErrorMessages[$options['message']]) ? $this->t($this->_defaultErrorMessages[$options['message']]) : $options['message'];
3822 unset($options['message']);
3824 foreach ((array)$attribute_names as $attribute_name){
3825 $value = isset($this->$attribute_name) ? $this->$attribute_name : null;
3827 if($value === null || ($options['case_sensitive'] || !$this->hasColumn($attribute_name))){
3828 $condition_sql = $this->getTableName().'.'.$attribute_name.' '.$this->getAttributeCondition($value);
3829 $condition_params = array($value);
3830 }else{
3831 $condition_sql = 'LOWER('.$this->getTableName().'.'.$attribute_name.') '.$this->getAttributeCondition($value);
3832 $condition_params = array(is_array($value) ? array_map('utf8_strtolower',$value) : utf8_strtolower($value));
3835 if(!empty($options['scope'])){
3836 foreach ((array)$options['scope'] as $scope_item){
3837 $scope_value = $this->get($scope_item);
3838 $condition_sql .= ' AND '.$this->getTableName().'.'.$scope_item.' '.$this->getAttributeCondition($scope_value);
3839 $condition_params[] = $scope_value;
3843 if(!$this->isNewRecord()){
3844 $condition_sql .= ' AND '.$this->getTableName().'.'.$this->getPrimaryKey().' <> ?';
3845 $condition_params[] = $this->getId();
3847 array_unshift($condition_params,$condition_sql);
3848 if ($this->find('first', array('conditions' => $condition_params))){
3849 $this->addError($attribute_name, $message);
3857 * Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
3858 * provided.
3860 * <code>
3861 * class Person extends ActiveRecord
3863 * function validate()
3865 * $this->validatesFormatOf('email', "/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/");
3868 * </code>
3870 * A regular expression must be provided or else an exception will be raised.
3872 * There are some regular expressions bundled with the Akelos Framework.
3873 * 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.
3874 * This are predefined perl-like regular extensions.
3876 * * AK_NOT_EMPTY_REGULAR_EXPRESSION ---> /.+/
3877 * * 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
3878 * * AK_NUMBER_REGULAR_EXPRESSION ---> /^[0-9]+$/
3879 * * AK_PHONE_REGULAR_EXPRESSION ---> /^([\+]?[(]?[\+]?[ ]?[0-9]{2,3}[)]?[ ]?)?[0-9 ()\-]{4,25}$/
3880 * * 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}$/
3881 * * 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])$/
3882 * * AK_POST_CODE_REGULAR_EXPRESSION ---> /^[0-9A-Za-z -]{2,7}$/
3884 * 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.
3886 * Params:
3887 * <tt>$message</tt> - A custom error message (default is: "is invalid")
3888 * <tt>$regular_expression</tt> - The regular expression used to validate the format with (note: must be supplied!)
3890 function validatesFormatOf($attribute_names, $regular_expression, $message = 'invalid', $regex_function = 'preg_match')
3892 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3894 $attribute_names = Ak::toArray($attribute_names);
3895 foreach ($attribute_names as $attribute_name){
3896 if(!isset($this->$attribute_name) || !$regex_function($regular_expression, $this->$attribute_name)){
3897 $this->addError($attribute_name, $message);
3903 * Validates whether the value of the specified attribute is available in a particular array of elements.
3905 * class Person extends ActiveRecord
3907 * function validate()
3909 * $this->validatesInclusionOf('gender', array('male', 'female'), "woah! what are you then!??!!");
3910 * $this->validatesInclusionOf('age', range(0, 99));
3913 * Parameters:
3914 * <tt>$array_of_ possibilities</tt> - An array of available items
3915 * <tt>$message</tt> - Specifies a customer error message (default is: "is not included in the list")
3916 * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false)
3918 function validatesInclusionOf($attribute_names, $array_of_possibilities, $message = 'inclusion', $allow_null = false)
3920 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3922 $attribute_names = Ak::toArray($attribute_names);
3923 foreach ($attribute_names as $attribute_name){
3924 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 )){
3925 $this->addError($attribute_name, $message);
3931 * Validates that the value of the specified attribute is not in a particular array of elements.
3933 * class Person extends ActiveRecord
3935 * function validate()
3937 * $this->validatesExclusionOf('username', array('admin', 'superuser'), "You don't belong here");
3938 * $this->validatesExclusionOf('age', range(30,60), "This site is only for under 30 and over 60");
3942 * Parameters:
3943 * <tt>$array_of_possibilities</tt> - An array of items that the value shouldn't be part of
3944 * <tt>$message</tt> - Specifies a customer error message (default is: "is reserved")
3945 * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false)
3947 function validatesExclusionOf($attribute_names, $array_of_possibilities, $message = 'exclusion', $allow_null = false)
3949 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3951 $attribute_names = Ak::toArray($attribute_names);
3952 foreach ($attribute_names as $attribute_name){
3954 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 )){
3955 $this->addError($attribute_name, $message);
3964 * Validates whether the value of the specified attribute is numeric.
3966 * class Person extends ActiveRecord
3968 * function validate()
3970 * $this->validatesNumericalityOf('value');
3974 * Parameters:
3975 * <tt>$message</tt> - A custom error message (default is: "is not a number")
3976 * <tt>$only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
3977 * <tt>$allow_null</tt> Skip validation if attribute is null (default is false).
3979 function validatesNumericalityOf($attribute_names, $message = 'not_a_number', $only_integer = false, $allow_null = false)
3981 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
3983 $attribute_names = Ak::toArray($attribute_names);
3984 foreach ($attribute_names as $attribute_name){
3985 if (isset($this->$attribute_name)){
3986 $value = $this->$attribute_name;
3987 if ($only_integer){
3988 $is_int = is_numeric($value) && (int)$value == $value;
3989 $has_error = !$is_int;
3990 }else{
3991 $has_error = !is_numeric($value);
3993 }else{
3994 $has_error = $allow_null ? false : true;
3997 if ($has_error){
3998 $this->addError($attribute_name, $message);
4006 * Returns true if no errors were added otherwise false.
4008 function isValid()
4010 $this->clearErrors();
4011 if($this->beforeValidation() && $this->notifyObservers('beforeValidation')){
4014 if($this->_set_default_attribute_values_automatically){
4015 //$this->_setDefaultAttributeValuesAutomatically();
4018 $this->validate();
4020 if($this->_automated_validators_enabled){
4021 //$this->_runAutomatedValidators();
4024 $this->afterValidation();
4025 $this->notifyObservers('afterValidation');
4027 if ($this->isNewRecord()){
4028 if($this->beforeValidationOnCreate()){
4029 $this->notifyObservers('beforeValidationOnCreate');
4030 $this->validateOnCreate();
4031 $this->afterValidationOnCreate();
4032 $this->notifyObservers('afterValidationOnCreate');
4034 }else{
4035 if($this->beforeValidationOnUpdate()){
4036 $this->notifyObservers('beforeValidationOnUpdate');
4037 $this->validateOnUpdate();
4038 $this->afterValidationOnUpdate();
4039 $this->notifyObservers('afterValidationOnUpdate');
4044 return !$this->hasErrors();
4048 * By default the Active Record will validate for the maximum length for database columns. You can
4049 * disable the automated validators by setting $this->_automated_validators_enabled to false.
4050 * Specific validators are (for now):
4051 * $this->_automated_max_length_validator = true; // true by default, but you can set it to false on your model
4052 * $this->_automated_not_null_validator = false; // disabled by default
4054 * @access private
4056 function _runAutomatedValidators()
4058 foreach ($this->_columns as $column_name=>$column_settings){
4059 if($this->_automated_max_length_validator &&
4060 empty($column_settings['primaryKey']) &&
4061 !empty($this->$column_name) &&
4062 !empty($column_settings['maxLength']) && $column_settings['maxLength'] > 0 &&
4063 strlen($this->$column_name) > $column_settings['maxLength']){
4064 $this->addError($column_name, sprintf($this->_defaultErrorMessages['too_long'], $column_settings['maxLength']));
4065 }elseif($this->_automated_not_null_validator && empty($column_settings['primaryKey']) && !empty($column_settings['notNull']) && (!isset($this->$column_name) || is_null($this->$column_name))){
4066 $this->addError($column_name,'empty');
4072 * $this->_set_default_attribute_values_automatically = true; // This enables automated attribute setting from database definition
4074 * @access private
4076 function _setDefaultAttributeValuesAutomatically()
4078 foreach ($this->_columns as $column_name=>$column_settings){
4079 if(empty($column_settings['primaryKey']) && isset($column_settings['hasDefault']) && $column_settings['hasDefault'] && (!isset($this->$column_name) || is_null($this->$column_name))){
4080 if(empty($column_settings['defaultValue'])){
4081 if($column_settings['type'] == 'integer' && empty($column_settings['notNull'])){
4082 $this->$column_name = 0;
4083 }elseif(($column_settings['type'] == 'string' || $column_settings['type'] == 'text') && empty($column_settings['notNull'])){
4084 $this->$column_name = '';
4086 }else {
4087 $this->$column_name = $column_settings['defaultValue'];
4094 * Overwrite this method for validation checks on all saves and use addError($field, $message); for invalid attributes.
4096 function validate()
4101 * Overwrite this method for validation checks used only on creation.
4103 function validateOnCreate()
4108 * Overwrite this method for validation checks used only on updates.
4110 function validateOnUpdate()
4114 /*/Validators*/
4118 Observers
4119 ====================================================================
4120 See also: Callbacks.
4124 * $state store the state of this observable object
4126 * @access private
4128 var $_observable_state;
4131 * @access private
4133 function _instantiateDefaultObserver()
4135 $default_observer_name = ucfirst($this->getModelName().'Observer');
4136 if(class_exists($default_observer_name)){
4137 //$Observer =& new $default_observer_name($this);
4138 Ak::singleton($default_observer_name, $this);
4143 * Calls the $method using the reference to each
4144 * registered observer.
4145 * @return true (this is used internally for triggering observers on default callbacks)
4147 function notifyObservers ($method = null)
4149 $observers =& $this->getObservers();
4150 $observer_count = count($observers);
4152 if(!empty($method)){
4153 $this->setObservableState($method);
4156 $model_name = $this->getModelName();
4157 for ($i=0; $i<$observer_count; $i++) {
4158 if(in_array($model_name, $observers[$i]->_observing)){
4159 if(method_exists($observers[$i], $method)){
4160 $observers[$i]->$method($this);
4161 }else{
4162 $observers[$i]->update($this->getObservableState(), &$this);
4164 }else{
4165 $observers[$i]->update($this->getObservableState(), &$this);
4168 $this->setObservableState('');
4170 return true;
4174 function setObservableState($state_message)
4176 $this->_observable_state = $state_message;
4179 function getObservableState()
4181 return $this->_observable_state;
4185 * Register the reference to an object object
4186 * @return void
4188 function &addObserver(&$observer)
4190 static $observers, $registered_observers;
4191 $observer_class_name = get_class($observer);
4192 if(!isset($registered_observers[$observer_class_name]) && func_num_args() == 1){
4193 $observers[] =& $observer;
4194 $registered_observers[$observer_class_name] = count($observers);
4196 return $observers;
4200 * Register the reference to an object object
4201 * @return void
4203 function &getObservers()
4205 $observers =& $this->addObserver(&$this, false);
4206 return $observers;
4209 /*/Observers*/
4215 Error Handling
4216 ====================================================================
4217 See also: Validators.
4222 * Returns the Errors array that holds all information about attribute error messages.
4224 function getErrors()
4226 return $this->_errors;
4230 * Adds an error to the base object instead of any particular attribute. This is used
4231 * to report errors that doesn't tie to any specific attribute, but rather to the object
4232 * as a whole. These error messages doesn't get prepended with any field name when iterating
4233 * with yieldEachFullError, so they should be complete sentences.
4235 function addErrorToBase($message)
4237 $this->addError($this->getModelName(), $message);
4241 * Returns errors assigned to base object through addToBase according to the normal rules of getErrorsOn($attribute).
4243 function getBaseErrors()
4245 $errors = $this->getErrors();
4246 return (array)@$errors[$this->getModelName()];
4251 * Adds an error message ($message) to the ($attribute), which will be returned on a call to <tt>getErrorsOn($attribute)</tt>
4252 * for the same attribute and ensure that this error object returns false when asked if <tt>hasErrors</tt>. More than one
4253 * error can be added to the same $attribute in which case an array will be returned on a call to <tt>getErrorsOn($attribute)</tt>.
4254 * If no $message is supplied, "invalid" is assumed.
4256 function addError($attribute, $message = 'invalid')
4258 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
4259 $this->_errors[$attribute][] = $message;
4263 * Will add an error message to each of the attributes in $attributes that is empty.
4265 function addErrorOnEmpty($attribute_names, $message = 'empty')
4267 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
4268 $attribute_names = Ak::toArray($attribute_names);
4269 foreach ($attribute_names as $attribute){
4270 if(empty($this->$attribute)){
4271 $this->addError($attribute, $message);
4277 * Will add an error message to each of the attributes in $attributes that is blank (using $this->isBlank).
4279 function addErrorOnBlank($attribute_names, $message = 'blank')
4281 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message;
4282 $attribute_names = Ak::toArray($attribute_names);
4283 foreach ($attribute_names as $attribute){
4284 if($this->isBlank(@$this->$attribute)){
4285 $this->addError($attribute, $message);
4291 * Will add an error message to each of the attributes in $attributes that has a length outside of the passed boundary $range.
4292 * If the length is above the boundary, the too_long_message message will be used. If below, the too_short_message.
4294 function addErrorOnBoundaryBreaking($attribute_names, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short')
4296 $too_long_message = isset($this->_defaultErrorMessages[$too_long_message]) ? $this->_defaultErrorMessages[$too_long_message] : $too_long_message;
4297 $too_short_message = isset($this->_defaultErrorMessages[$too_short_message]) ? $this->_defaultErrorMessages[$too_short_message] : $too_short_message;
4299 $attribute_names = Ak::toArray($attribute_names);
4300 foreach ($attribute_names as $attribute){
4301 if(@$this->$attribute < $range_begin){
4302 $this->addError($attribute, $too_short_message);
4304 if(@$this->$attribute > $range_end){
4305 $this->addError($attribute, $too_long_message);
4311 function addErrorOnBoundryBreaking ($attributes, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short')
4313 $this->addErrorOnBoundaryBreaking($attributes, $range_begin, $range_end, $too_long_message, $too_short_message);
4317 * Returns true if the specified $attribute has errors associated with it.
4319 function isInvalid($attribute)
4321 return $this->getErrorsOn($attribute);
4325 * Returns false, if no errors are associated with the specified $attribute.
4326 * Returns the error message, if one error is associated with the specified $attribute.
4327 * Returns an array of error messages, if more than one error is associated with the specified $attribute.
4329 function getErrorsOn($attribute)
4331 if (empty($this->_errors[$attribute])){
4332 return false;
4333 }elseif (count($this->_errors[$attribute]) == 1){
4334 $k = array_keys($this->_errors[$attribute]);
4335 return $this->_errors[$attribute][$k[0]];
4336 }else{
4337 return $this->_errors[$attribute];
4343 * Yields each attribute and associated message per error added.
4345 function yieldEachError()
4347 foreach ($this->_errors as $errors){
4348 foreach ($errors as $error){
4349 $this->yieldError($error);
4354 function yieldError($message)
4356 $messages = is_array($message) ? $message : array($message);
4357 foreach ($messages as $message){
4358 echo "<div class='error'><p>$message</p></div>\n";
4364 * Yields each full error message added. So Person->addError("first_name", "can't be empty") will be returned
4365 * through iteration as "First name can't be empty".
4367 function yieldEachFullError()
4369 $full_messages = $this->getFullErrorMessages();
4370 foreach ($full_messages as $full_message){
4371 $this->yieldError($full_message);
4377 * Returns all the full error messages in an array.
4379 function getFullErrorMessages()
4381 $full_messages = array();
4383 foreach ($this->_errors as $attribute=>$errors){
4384 $full_messages[$attribute] = array();
4385 foreach ($errors as $error){
4386 $full_messages[$attribute][] = $this->t('%attribute_name %error', array(
4387 '%attribute_name'=>AkInflector::humanize($this->_delocalizeAttribute($attribute)),
4388 '%error'=>$error
4392 return $full_messages;
4396 * Returns true if no errors have been added.
4398 function hasErrors()
4400 return !empty($this->_errors);
4404 * Removes all the errors that have been added.
4406 function clearErrors()
4408 $this->_errors = array();
4412 * Returns the total number of errors added. Two errors added to the same attribute will be counted as such
4413 * with this as well.
4415 function countErrors()
4417 $error_count = 0;
4418 foreach ($this->_errors as $errors){
4419 $error_count = count($errors)+$error_count;
4422 return $error_count;
4426 function errorsToString($print = false)
4428 $result = "\n<div id='errors'>\n<ul class='error'>\n";
4429 foreach ($this->getFullErrorMessages() as $error){
4430 $result .= is_array($error) ? "<li class='error'>".join('</li><li class=\'error\'>',$error)."</li>\n" : "<li class='error'>$error</li>\n";
4432 $result .= "</ul>\n</div>\n";
4434 if($print){
4435 echo $result;
4437 return $result;
4440 /*/Error Handling*/
4445 Act as Behaviours
4446 ====================================================================
4447 See also: Acts as List, Acts as Tree, Acts as Nested Set.
4451 * actAs provides a method for extending Active Record models.
4453 * Example:
4454 * $this->actsAs('list', array('scope' => 'todo_list'));
4456 function actsAs($behaviour, $options = array())
4458 $class_name = $this->_getActAsClassName($behaviour);
4459 $underscored_place_holder = AkInflector::underscore($behaviour);
4460 $camelized_place_holder = AkInflector::camelize($underscored_place_holder);
4462 if($this->$underscored_place_holder =& $this->_getActAsInstance($class_name, $options)){
4463 $this->$camelized_place_holder =& $this->$underscored_place_holder;
4464 if($this->$underscored_place_holder->init($options)){
4465 $this->__ActsLikeAttributes[$underscored_place_holder] = $underscored_place_holder;
4471 * @access private
4473 function _getActAsClassName($behaviour)
4475 $class_name = AkInflector::camelize($behaviour);
4476 return file_exists(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkActsAsBehaviours'.DS.'AkActsAs'.$class_name.'.php') && !class_exists('ActsAs'.$class_name) ?
4477 'AkActsAs'.$class_name : 'ActsAs'.$class_name;
4481 * @access private
4483 function &_getActAsInstance($class_name, $options)
4485 if(!class_exists($class_name)){
4486 if(substr($class_name,0,2) == 'Ak'){
4487 include_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkActsAsBehaviours'.DS.$class_name.'.php');
4488 }else{
4489 include_once(AK_APP_PLUGINS_DIR.DS.AkInflector::underscore($class_name).DS.'lib'.DS.$class_name.'.php');
4492 if(!class_exists($class_name)){
4493 trigger_error(Ak::t('The class %class used for handling an "act_as %class" does not exist',array('%class'=>$class_name)), E_USER_ERROR);
4494 $false = false;
4495 return $false;
4496 }else{
4497 $ActAsInstance =& new $class_name($this, $options);
4498 return $ActAsInstance;
4503 * @access private
4505 function _loadActAsBehaviours()
4507 $this->act_as = !empty($this->acts_as) ? $this->acts_as : (empty($this->act_as) ? false : $this->act_as);
4508 if(!empty($this->act_as)){
4509 if(is_string($this->act_as)){
4510 $this->act_as = array_unique(array_diff(array_map('trim',explode(',',$this->act_as.',')), array('')));
4511 foreach ($this->act_as as $type){
4512 $this->actsAs($type);
4514 }elseif (is_array($this->act_as)){
4515 foreach ($this->act_as as $type=>$options){
4516 $this->actsAs($type, $options);
4523 * Returns a comma separated list of possible acts like (active record, nested set, list)....
4525 function actsLike()
4527 $result = 'active record';
4528 foreach ($this->__ActsLikeAttributes as $type){
4529 if(!empty($this->$type) && is_object($this->$type) && method_exists($this->{$type}, 'getType')){
4530 $result .= ','.$this->{$type}->getType();
4533 return $result;
4536 /*/Act as Behaviours*/
4539 Debugging
4540 ====================================================================
4544 function dbug()
4546 if(!$this->isConnected()){
4547 $this->setConnection();
4549 $this->_db->connection->debug = $this->_db->connection->debug ? false : true;
4550 $this->db_debug =& $this->_db->connection->debug;
4553 function toString($print = false)
4555 $result = '';
4556 if(!AK_CLI || (AK_ENVIRONMENT == 'testing' && !AK_CLI)){
4557 $result = "<h2>Details for ".AkInflector::humanize(AkInflector::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()."</h2>\n<dl>\n";
4558 foreach ($this->getColumnNames() as $column=>$caption){
4559 $result .= "<dt>$caption</dt>\n<dd>".$this->getAttribute($column)."</dd>\n";
4561 $result .= "</dl>\n<hr />";
4562 if($print){
4563 echo $result;
4565 }elseif(AK_ENVIRONMENT == 'development'){
4566 $result = "\n".
4567 str_replace("\n"," ",var_export($this->getAttributes(),true));
4568 $result .= "\n";
4569 echo $result;
4570 return '';
4571 }elseif (AK_CLI){
4572 $result = "\n-------\n Details for ".AkInflector::humanize(AkInflector::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()." ==\n\n/==\n";
4573 foreach ($this->getColumnNames() as $column=>$caption){
4574 $result .= "\t * $caption: ".$this->getAttribute($column)."\n";
4576 $result .= "\n\n-------\n";
4577 if($print){
4578 echo $result;
4581 return $result;
4584 function dbugging($trace_this_on_debug_mode = null)
4586 if(!empty($this->_db->debug) && !empty($trace_this_on_debug_mode)){
4587 $message = !is_scalar($trace_this_on_debug_mode) ? var_export($trace_this_on_debug_mode, true) : (string)$trace_this_on_debug_mode;
4588 Ak::trace($message);
4590 return !empty($this->_db->debug);
4595 function debug ($data = 'active_record_class', $_functions=0)
4597 if(!AK_DEBUG && !AK_DEV_MODE){
4598 return;
4601 $data = $data == 'active_record_class' ? (AK_PHP5 ? clone($this) : $this) : $data;
4603 if($_functions!=0) {
4604 $sf=1;
4605 } else {
4606 $sf=0 ;
4609 if (isset ($data)) {
4610 if (is_array($data) || is_object($data)) {
4612 if (count ($data)) {
4613 echo AK_CLI ? "/--\n" : "<ol>\n";
4614 while (list ($key,$value) = each ($data)) {
4615 if($key{0} == '_'){
4616 continue;
4618 $type=gettype($value);
4619 if ($type=="array") {
4620 AK_CLI ? printf ("\t* (%s) %s:\n",$type, $key) :
4621 printf ("<li>(%s) <b>%s</b>:\n",$type, $key);
4622 ob_start();
4623 Ak::debug ($value,$sf);
4624 $lines = explode("\n",ob_get_clean()."\n");
4625 foreach ($lines as $line){
4626 echo "\t".$line."\n";
4628 }elseif($type == "object"){
4629 if(method_exists($value,'hasColumn') && $value->hasColumn($key)){
4630 $value->toString(true);
4631 AK_CLI ? printf ("\t* (%s) %s:\n",$type, $key) :
4632 printf ("<li>(%s) <b>%s</b>:\n",$type, $key);
4633 ob_start();
4634 Ak::debug ($value,$sf);
4635 $lines = explode("\n",ob_get_clean()."\n");
4636 foreach ($lines as $line){
4637 echo "\t".$line."\n";
4640 }elseif (eregi ("function", $type)) {
4641 if ($sf) {
4642 AK_CLI ? printf ("\t* (%s) %s:\n",$type, $key, $value) :
4643 printf ("<li>(%s) <b>%s</b> </li>\n",$type, $key, $value);
4645 } else {
4646 if (!$value) {
4647 $value="(none)";
4649 AK_CLI ? printf ("\t* (%s) %s = %s\n",$type, $key, $value) :
4650 printf ("<li>(%s) <b>%s</b> = %s</li>\n",$type, $key, $value);
4653 echo AK_CLI ? "\n--/\n" : "</ol>fin.\n";
4654 } else {
4655 echo "(empty)";
4661 /*/Debugging*/
4666 Utilities
4667 ====================================================================
4670 * Selects and filters a search result to include only specified columns
4672 * $people_for_select = $People->select($People->find(),'name','email');
4674 * Now $people_for_select will hold an array with
4675 * array (
4676 * array ('name' => 'Jose','email' => 'jose@example.com'),
4677 * array ('name' => 'Alicia','email' => 'alicia@example.com'),
4678 * array ('name' => 'Hilario','email' => 'hilario@example.com'),
4679 * array ('name' => 'Bermi','email' => 'bermi@example.com')
4680 * );
4682 function select(&$source_array)
4684 $resulting_array = array();
4685 if(!empty($source_array) && is_array($source_array) && func_num_args() > 1) {
4686 (array)$args = array_filter(array_slice(func_get_args(),1),array($this,'hasColumn'));
4687 foreach ($source_array as $source_item){
4688 $item_fields = array();
4689 foreach ($args as $arg){
4690 $item_fields[$arg] =& $source_item->get($arg);
4692 $resulting_array[] =& $item_fields;
4695 return $resulting_array;
4700 * Collect is a function for selecting items from double depth array
4701 * like the ones returned by the AkActiveRecord. This comes useful when you just need some
4702 * fields for generating tables, select lists with only desired fields.
4704 * $people_for_select = Ak::select($People->find(),'id','email');
4706 * Returns something like:
4707 * array (
4708 * array ('10' => 'jose@example.com'),
4709 * array ('15' => 'alicia@example.com'),
4710 * array ('16' => 'hilario@example.com'),
4711 * array ('18' => 'bermi@example.com')
4712 * );
4714 function collect(&$source_array, $key_index, $value_index)
4716 $resulting_array = array();
4717 if(!empty($source_array) && is_array($source_array)) {
4718 foreach ($source_array as $source_item){
4719 $resulting_array[$source_item->get($key_index)] = $source_item->get($value_index);
4722 return $resulting_array;
4725 function toJson()
4727 return Ak::toJson($this->getAttributes());
4731 * converts to yaml-strings
4733 * examples:
4734 * User::toYaml($users->find('all'));
4735 * $Bermi->toYaml();
4737 * @param array of ActiveRecords[optional] $data
4739 function toYaml($data = null)
4741 return Ak::convert('active_record', 'yaml', empty($data) ? $this : $data);
4746 * Parses an special formated array as a list of keys and values
4748 * This function generates an array with values and keys from an array with numeric keys.
4750 * This allows to parse an array to a function in the following manner.
4751 * create('first_name->', 'Bermi', 'last_name->', 'Ferrer');
4752 * //Previous code will be the same that
4753 * create(array('first_name'=>'Bermi', 'last_name'=> 'Ferrer'));
4755 * Use this syntax only for quick testings, not for production environments. If the number of arguments varies, the result might be unpredictable.
4757 * This function syntax is disabled by default. You need to define('AK_ENABLE_AKELOS_ARGS', true)
4758 * if you need this functionality.
4760 * @deprecated
4762 function parseAkelosArgs(&$args)
4764 if(!AK_ENABLE_AKELOS_ARGS){
4765 $this->_castDateParametersFromDateHelper_($args);
4766 return ;
4768 $k = array_keys($args);
4769 if(isset($k[1]) && substr($args[$k[0]],-1) == '>'){
4770 $size = sizeOf($k);
4771 $params = array();
4772 for($i = 0; $i < $size; $i++ ) {
4773 $v = $args[$k[$i]];
4774 if(!isset($key) && is_string($args[$k[$i]]) && substr($v,-1) == '>'){
4775 $key = rtrim($v, '=-> ');
4776 }elseif(isset($key)) {
4777 $params[$key] = $v;
4778 unset($key);
4779 }else{
4780 $params[$k[$i]] = $v;
4783 if(!empty($params)){
4784 $args = $params;
4787 $this->_castDateParametersFromDateHelper_($args);
4790 * Gets an array from a string.
4792 * Acts like Php explode() function but uses any of this as valid separators ' AND ',' and ',' + ',' ',',',';'
4794 function getArrayFromAkString($string)
4796 if(is_array($string)){
4797 return $string;
4799 $string = str_replace(array(' AND ',' and ',' + ',' ',',',';'),array('|','|','|','','|','|'),trim($string));
4800 return strstr($string,'|') ? explode('|', $string) : array($string);
4802 /*/Utilities*/
4805 function getAttributeCondition($argument)
4807 if(is_array($argument)){
4808 return 'IN (?)';
4809 }elseif (is_null($argument)){
4810 return 'IS ?';
4811 }else{
4812 return '= ?';
4818 Calculations
4819 ====================================================================
4823 * @access private
4825 var $_calculation_options = array('conditions', 'joins', 'order', 'select', 'group', 'having', 'distinct', 'limit', 'offset');
4828 * Count operates using three different approaches.
4830 * * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
4831 * * Count by conditions or joins
4832 * * Count using options will find the row count matched by the options used.
4834 * The last approach, count using options, accepts an option hash as the only parameter. The options are:
4836 * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array("user_name = ?", $username ). See conditions in the intro.
4837 * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
4838 * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
4839 * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
4840 * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join.
4841 * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
4843 * Examples for counting all:
4844 * $Person->count(); // returns the total count of all people
4846 * Examples for count by +conditions+ and +joins+ (this has been deprecated):
4847 * $Person->count("age > 26"); // returns the number of people older than 26
4848 * $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(*).
4850 * Examples for count with options:
4851 * $Person->count('conditions' => "age > 26");
4852 * $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.
4853 * $Person->count('id', 'conditions' => "age > 26"); // Performs a COUNT(id)
4854 * $Person->count('all', 'conditions' => "age > 26"); // Performs a COUNT(*) ('all' is an alias for '*')
4856 * Note: $Person->count('all') will not work because it will use 'all' as the condition. Use $Person->count() instead.
4858 function count()
4860 $args = func_get_args();
4861 list($column_name, $options) = $this->_constructCountOptionsFromLegacyArgs($args);
4862 return $this->calculate('count', $column_name, $options);
4866 * Calculates average value on a given column. The value is returned as a float. See #calculate for examples with options.
4868 * $Person->average('age');
4870 function average($column_name, $options = array())
4872 return $this->calculate('avg', $column_name, $options);
4876 * 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.
4878 * $Person->minimum('age');
4880 function minimum($column_name, $options = array())
4882 return $this->calculate('min', $column_name, $options);
4886 * 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.
4888 * $Person->maximum('age');
4890 function maximum($column_name, $options = array())
4892 return $this->calculate('max', $column_name, $options);
4896 * 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.
4898 * $Person->sum('age');
4900 function sum($column_name, $options = array())
4902 return $this->calculate('sum', $column_name, $options);
4906 * This calculates aggregate values in the given column: Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
4907 * Options such as 'conditions', 'order', 'group', 'having', and 'joins' can be passed to customize the query.
4909 * There are two basic forms of output:
4910 * * 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.
4911 * * Grouped values: This returns an ordered hash of the values and groups them by the 'group' option. It takes a column name.
4913 * $values = $Person->maximum('age', array('group' => 'last_name'));
4914 * echo $values["Drake"]
4915 * => 43
4917 * Options:
4918 * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array( "user_name = ?", username ). See conditions in the intro.
4919 * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
4920 * The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
4921 * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
4922 * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
4923 * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join.
4924 * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
4926 * Examples:
4927 * $Person->calculate('count', 'all'); // The same as $Person->count();
4928 * $Person->average('age'); // SELECT AVG(age) FROM people...
4929 * $Person->minimum('age', array('conditions' => array('last_name != ?', 'Drake'))); // Selects the minimum age for everyone with a last name other than 'Drake'
4930 * $Person->minimum('age', array('having' => 'min(age) > 17', 'group' => 'last'_name)); // Selects the minimum age for any family without any minors
4932 function calculate($operation, $column_name, $options = array())
4934 $this->_validateCalculationOptions($options);
4935 $column_name = empty($options['select']) ? $column_name : $options['select'];
4936 $column_name = $column_name == 'all' ? '*' : $column_name;
4937 $column = $this->_getColumnFor($column_name);
4938 if (!empty($options['group'])){
4939 return $this->_executeGroupedCalculation($operation, $column_name, $column, $options);
4940 }else{
4941 return $this->_executeSimpleCalculation($operation, $column_name, $column, $options);
4944 return 0;
4948 * @access private
4950 function _constructCountOptionsFromLegacyArgs($args)
4952 $options = array();
4953 $column_name = 'all';
4956 We need to handle
4957 count()
4958 count(options=array())
4959 count($column_name='all', $options=array())
4960 count($conditions=null, $joins=null)
4962 if(count($args) > 2){
4963 trigger_error(Ak::t("Unexpected parameters passed to count(\$options=array())", E_USER_ERROR));
4964 }elseif(count($args) > 0){
4965 if(!empty($args[0]) && is_array($args[0])){
4966 $options = $args[0];
4967 }elseif(!empty($args[1]) && is_array($args[1])){
4968 $column_name = array_shift($args);
4969 $options = array_shift($args);
4970 }else{
4971 $options = array('conditions' => $args[0]);
4972 if(!empty($args[1])){
4973 $options = array_merge($options, array('joins' => $args[1]));
4977 return array($column_name, $options);
4982 * @access private
4984 function _constructCalculationSql($operation, $column_name, $options)
4986 $operation = strtolower($operation);
4987 $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name);
4988 $use_workaround = $operation == 'count' && !empty($options['distinct']) && $this->_getDatabaseType() == 'sqlite';
4990 $sql = $use_workaround ?
4991 "SELECT COUNT(*) AS $aggregate_alias" : // A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
4992 "SELECT $operation(".(empty($options['distinct'])?'':'DISTINCT ')."$column_name) AS $aggregate_alias";
4995 $sql .= empty($options['group']) ? '' : ", {$options['group_field']} AS {$options['group_alias']}";
4996 $sql .= $use_workaround ? " FROM (SELECT DISTINCT {$column_name}" : '';
4997 $sql .= " FROM ".$this->getTableName()." ";
4999 $sql .= empty($options['joins']) ? '' : " {$options['joins']} ";
5001 empty($options['conditions']) ? null : $this->addConditions($sql, $options['conditions']);
5003 if (!empty($options['group'])){
5004 $sql .= " GROUP BY {$options['group_field']} ";
5005 $sql .= empty($options['having']) ? '' : " HAVING {$options['having']} ";
5008 $sql .= empty($options['order']) ? '' : " ORDER BY {$options['order']} ";
5009 $this->_db->addLimitAndOffset($sql, $options);
5010 $sql .= $use_workaround ? ')' : '';
5011 return $sql;
5016 * @access private
5018 function _executeSimpleCalculation($operation, $column_name, $column, $options)
5020 $value = $this->_db->selectValue($this->_constructCalculationSql($operation, $column_name, $options));
5021 return $this->_typeCastCalculatedValue($value, $column, $operation);
5025 * @access private
5027 function _executeGroupedCalculation($operation, $column_name, $column, $options)
5029 $group_field = $options['group'];
5030 $group_alias = $this->_getColumnAliasFor($group_field);
5031 $group_column = $this->_getColumnFor($group_field);
5032 $options = array_merge(array('group_field' => $group_field, 'group_alias' => $group_alias),$options);
5033 $sql = $this->_constructCalculationSql($operation, $column_name, $options);
5034 $calculated_data = $this->_db->select($sql);
5035 $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name);
5037 $all = array();
5038 foreach ($calculated_data as $row){
5039 $key = $this->_typeCastCalculatedValue($row[$group_alias], $group_column);
5040 $all[$key] = $this->_typeCastCalculatedValue($row[$aggregate_alias], $column, $operation);
5042 return $all;
5046 * @access private
5048 function _validateCalculationOptions($options = array())
5050 $invalid_options = array_diff(array_keys($options),$this->_calculation_options);
5051 if(!empty($invalid_options)){
5052 trigger_error(Ak::t('%options are not valid calculation options.', array('%options'=>join(', ',$invalid_options))), E_USER_ERROR);
5057 * Converts a given key to the value that the database adapter returns as
5058 * as a usable column name.
5059 * users.id #=> users_id
5060 * sum(id) #=> sum_id
5061 * count(distinct users.id) #=> count_distinct_users_id
5062 * count(*) #=> count_all
5064 * @access private
5066 function _getColumnAliasFor()
5068 $args = func_get_args();
5069 $keys = strtolower(join(' ',(!empty($args) ? (is_array($args[0]) ? $args[0] : $args) : array())));
5070 return preg_replace(array('/\*/','/\W+/','/^ +/','/ +$/','/ +/'),array('all',' ','','','_'), $keys);
5074 * @access private
5076 function _getColumnFor($field)
5078 $field_name = ltrim(substr($field,strpos($field,'.')),'.');
5079 if(in_array($field_name,$this->getColumnNames())){
5080 return $field_name;
5082 return $field;
5086 * @access private
5088 function _typeCastCalculatedValue($value, $column, $operation = null)
5090 $operation = strtolower($operation);
5091 if($operation == 'count'){
5092 return intval($value);
5093 }elseif ($operation == 'avg'){
5094 return floatval($value);
5095 }else{
5096 return empty($column) ? $value : AkActiveRecord::castAttributeFromDatabase($column, $value);
5100 /*/Calculations*/
5102 function hasBeenModified()
5104 return Ak::objectHasBeenModified($this);
5108 * Just freeze the attributes hash, such that associations are still accessible even on destroyed records.
5110 * @todo implement freeze correctly for its intended use
5112 function freeze()
5114 return $this->_freeze = true;
5117 function isFrozen()
5119 return !empty($this->_freeze);
5123 * Alias for getModelName()
5125 function getType()
5127 return $this->getModelName();
5130 function &objectCache()
5132 static $cache;
5133 $args =& func_get_args();
5134 if(count($args) == 2){
5135 if(!isset($cache[$args[0]])){
5136 $cache[$args[0]] =& $args[1];
5138 }elseif(!isset($cache[$args[0]])){
5139 return false;
5141 return $cache[$args[0]];
5146 Connection adapters
5147 ====================================================================
5148 Right now Akelos uses phpAdodb for bd abstraction. This are functionalities not
5149 provided in phpAdodb and that will move to a separated driver for each db
5150 engine in a future
5152 function _extractValueFromDefault($default)
5154 if($this->_getDatabaseType() == 'postgre'){
5155 if(preg_match("/^'(.*)'::/", $default, $match)){
5156 return $match[1];
5158 // a postgre HACK; we dont know the column-type here
5159 if ($default=='true') {
5160 return true;
5162 if ($default=='false') {
5163 return false;
5166 return $default;