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 // +----------------------------------------------------------------------+
12 * @package ActiveRecord
14 * @component Active Record
15 * @author Bermi Ferrer <bermi a.t akelos c.om> 2004 - 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');
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}$/");
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');
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.
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:
69 * $user = new User(array('name' => 'David', 'occupation' => 'Code Artist'));
70 * echo $user->name; // Will print "David"
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:
81 * $user->name = 'David';
82 * $user->occupation = 'Code Artist';
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:
92 * class User extends ActiveRecord
94 * function authenticateUnsafely($user_name, $password)
96 * return findFirst("user_name = '$user_name' AND password = '$password'");
99 * function authenticateSafely($user_name, $password)
101 * return findFirst("user_name = ? AND password = ?", $user_name, $password);
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:
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')
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
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:
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')
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:
155 * class Company extends ActiveRecord{}
156 * class Firm extends Company{}
157 * class Client extends Company{}
158 * class PriorityClient extends Client{}
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
189 //var $disableAutomatedAssociationLoading = true;
194 var $_dataDictionary;
196 var $_inheritanceColumn;
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;
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]);
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);
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)){
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.
375 if($object = $this->find($this->getId())){
376 $this->setAttributes($object->getAttributes(), true);
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);
410 function createOrUpdate($validate = true)
412 if($validate && !$this->isValid()){
413 $this->transactionFail();
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);
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;
436 $Item->has_been_created
= false;
438 $Item->has_been_found
= !$Item->has_been_created
;
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.
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);
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()){
484 $this->setId($inserted_id);
486 $this->_newRecord
= false;
488 if (!$this->afterCreate() ||
!$this->notifyObservers('afterCreate')){
489 return $this->transactionFail();
495 function _setRecordTimestamps()
497 if (!$this->_recordTimestamps
){
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'));
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*/
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()){
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();
550 if(!$this->notifyObservers('afterSave')){
551 $this->transactionFail();
556 $this->transactionFail();
559 $result = $this->transactionHasFailed() ?
false : $result;
560 $this->transactionComplete();
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*/
596 ====================================================================
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();
611 foreach ($id as $idx=>$single_id){
612 $results[] = $this->update($single_id, isset($attributes[$idx]) ?
$attributes[$idx] : $attributes);
616 $object =& $this->find($id);
617 $object->updateAttributes($attributes);
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.
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()){
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();
709 /*/Updating records*/
715 ====================================================================
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.
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;
771 $this->transactionStart();
772 $id_arr = is_array($id) ?
$id : array($id);
773 if($objects = $this->find($id_arr)){
774 $results = count($objects);
776 for ($i=0; $results > $i; $i++
){
777 if(!$objects[$i]->destroy()){
778 $no_problems = false;
781 $this->transactionComplete();
784 $this->transactionComplete();
788 if(!$this->isNewRecord()){
789 $this->transactionStart();
790 $return = $this->_destroy() && $this->freeze();
791 $this->transactionComplete();
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();
815 * Destroys the objects for all the records that matches the condition by instantiating
816 * each object and calling the destroy method.
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);
827 for ($i=0; $results > $i; $i++
){
828 if(!$objects[$i]->destroy()){
829 $no_problems = false;
838 /*/Deleting 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);
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:
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"));
887 * Examples for find first:
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));
894 * Examples for find all:
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'));
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);
918 // HACK: php4 pass by ref
919 $result =& $this->_findInitial($options);
924 // HACK: php4 pass by ref
925 $result =& $this->_findEvery($options);
930 // HACK: php4 pass by ref
931 $result =& $this->_findFromIds($args, $options);
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];
950 // if we return an empty array instead of false we need to change this->exists()!
951 //$_result = array();
957 function &_findEvery($options)
959 $limit = isset($options['limit']) ?
$options['limit'] : null;
960 $offset = isset($options['offset']) ?
$options['offset'] : null;
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 if((!empty($options['include']) && $this->hasAssociations())){
968 $result =& $this->findWithAssociations($options);
970 $result =& $this->findBySql($sql);
973 if(!empty($result) && is_array($result)){
982 function &_findFromIds($ids, $options)
984 $expects_array = is_array($ids[0]);
985 $ids = array_unique($expects_array ?
(isset($ids[1]) ?
array_merge($ids[0],$ids) : $ids[0]) : $ids);
987 $num_ids = count($ids);
989 //at this point $options['conditions'] can't be an array
990 $conditions = !empty($options['conditions']) ?
' AND '.$options['conditions'] : '';
994 trigger_error($this->t('Couldn\'t find %object_name without an ID%conditions',array('%object_name'=>$this->getModelName(),'%conditions'=>$conditions)), E_USER_ERROR
);
998 $table_name = !empty($options['include']) && $this->hasAssociations() ?
'__owner' : $this->getTableName();
999 $options['conditions'] = $table_name.'.'.$this->getPrimaryKey().' = '.$ids[0].$conditions;
1000 $result =& $this->_findEvery($options);
1001 if (!$expects_array && $result !== false){
1008 $without_conditions = empty($options['conditions']) ?
true : false;
1009 $ids_condition = $this->getPrimaryKey().' IN ('.join(', ',$ids).')';
1010 $options['conditions'] = $ids_condition.$conditions;
1012 $result =& $this->_findEvery($options);
1013 if(is_array($result) && (count($result) != $num_ids && $without_conditions)){
1022 function _extractOptionsFromArgs(&$args)
1024 $last_arg = count($args)-1;
1025 return isset($args[$last_arg]) && is_array($args[$last_arg]) && $this->_isOptionsHash($args[$last_arg]) ?
array_pop($args) : array();
1028 function _isOptionsHash($options)
1030 if (isset($options[0])){
1033 $valid_keys = array('conditions', 'include', 'joins', 'limit', 'offset', 'order', 'sort', 'bind', 'select','select_prefix', 'readonly');
1034 foreach (array_keys($options) as $key){
1035 if (!in_array($key,$valid_keys)){
1042 function _extractConditionsFromArgs($args, $options)
1049 $num_args = count($args);
1051 // deprecated: acts like findFirstBySQL
1052 if ($num_args === 1 && !is_numeric($args[0]) && is_string($args[0]) && $args[0] != 'all' && $args[0] != 'first'){
1053 // $Users->find("last_name = 'Williams'"); => find('first',"last_name = 'Williams'");
1054 Ak
::deprecateWarning(array("AR::find('%sql') is ambiguous and therefore deprecated, use AR::find('first',%sql) instead", '%sql'=>$args[0]));
1055 $options = array('conditions'=> $args[0]);
1056 return array('first',$options);
1059 // set fetch_mode to 'all' if none is given
1060 if (!is_numeric($fetch) && !is_array($fetch) && $fetch != 'all' && $fetch != 'first') {
1061 array_unshift($args, 'all');
1062 $num_args = count($args);
1064 if ($num_args > 1) {
1065 if (is_string($args[1])){
1066 // $Users->find(:fetch_mode,"first_name = ?",'Tim');
1067 $fetch = array_shift($args);
1068 $options = array_merge($options, array('conditions'=>$args)); //TODO: merge_conditions
1069 }elseif (is_array($args[1])) {
1070 // $Users->find(:fetch_mode,array('first_name = ?,'Tim'));
1071 $fetch = array_shift($args);
1072 $options = array_merge($options, array('conditions'=>$args[0])); //TODO: merge_conditions
1076 return array($fetch,$options);
1079 function _sanitizeConditionsVariables(&$options)
1081 if(!empty($options['conditions']) && is_array($options['conditions'])){
1082 if (isset($options['conditions'][0]) && strstr($options['conditions'][0], '?') && count($options['conditions']) > 1){
1083 //array('conditions' => array("name=?",$name))
1084 $pattern = array_shift($options['conditions']);
1085 $options['bind'] = array_values($options['conditions']);
1086 $options['conditions'] = $pattern;
1087 }elseif (isset($options['conditions'][0])){
1088 //array('conditions' => array("user_name = :user_name", ':user_name' => 'hilario')
1089 $pattern = array_shift($options['conditions']);
1090 $options['conditions'] = str_replace(array_keys($options['conditions']), array_values($this->getSanitizedConditionsArray($options['conditions'])),$pattern);
1092 //array('conditions' => array('user_name'=>'Hilario'))
1093 $options['conditions'] = join(' AND ',(array)$this->getAttributesQuoted($options['conditions']));
1099 function _validateFindOptions(&$options)
1101 $valid_keys = array('conditions', 'include', 'joins', 'limit', 'offset', 'order', 'bind', 'select','select_prefix', 'readonly');
1102 foreach (array_keys($options) as $key){
1103 if (!in_array($key,$valid_keys)) unset($options[$key]);
1107 function &findFirst()
1109 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1110 return Ak
::handleStaticCall();
1112 $args = func_get_args();
1113 $result =& Ak
::call_user_func_array(array(&$this,'find'), array_merge(array('first'),$args));
1119 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1120 return Ak
::handleStaticCall();
1122 $args = func_get_args();
1123 $result =& Ak
::call_user_func_array(array(&$this,'find'), array_merge(array('all'),$args));
1129 * Works like find_all, but requires a complete SQL string. Examples:
1130 * $Post->findBySql("SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id");
1131 * $Post->findBySql(array("SELECT * FROM posts WHERE author = ? AND created_on > ?", $author_id, $start_date));
1133 function &findBySql($sql, $limit = null, $offset = null, $bindings = null)
1135 if ($limit ||
$offset){
1136 Ak
::deprecateWarning("You're calling AR::findBySql with \$limit or \$offset parameters. This has been deprecated.");
1137 $this->_db
->addLimitAndOffset($sql, array('limit'=>$limit,'offset'=>$offset));
1139 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1140 return Ak
::handleStaticCall();
1143 $records = $this->_db
->select ($sql,'selecting');
1144 foreach ($records as $record){
1145 $objects[] =& $this->instantiate($this->getOnlyAvailableAttributes($record), false);
1151 * This function pretends to emulate RoR finders until AkActiveRecord::addMethod becomes stable on future PHP versions.
1152 * @todo use PHP5 __call method for handling the magic finder methods like findFirstByUnsenameAndPassword('bermi','pass')
1154 function &findFirstBy()
1156 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1157 return Ak
::handleStaticCall();
1159 $args = func_get_args();
1160 array_unshift($args,'first');
1161 $result =& Ak
::call_user_func_array(array(&$this,'findBy'), $args);
1165 function &findLastBy()
1167 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1168 return Ak
::handleStaticCall();
1170 $args = func_get_args();
1171 $options = $this->_extractOptionsFromArgs($args);
1172 $options['order'] = $this->getPrimaryKey().' DESC';
1173 array_push($args, $options);
1174 $result =& Ak
::call_user_func_array(array(&$this,'findFirstBy'), $args);
1178 function &findAllBy()
1180 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1181 return Ak
::handleStaticCall();
1183 $args = func_get_args();
1184 array_unshift($args,'all');
1185 $result =& Ak
::call_user_func_array(array(&$this,'findBy'), $args);
1190 * This method allows you to use finders in a more flexible way like:
1192 * findBy('username AND password', $username, $password);
1193 * findBy('age > ? AND name:contains', 18, 'Joe');
1194 * findBy('is_active = true AND session_id', session_id());
1199 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1200 return Ak
::handleStaticCall();
1202 $args = func_get_args();
1203 $find_by_sql = array_shift($args);
1204 if($find_by_sql == 'all' ||
$find_by_sql == 'first'){
1205 $fetch = $find_by_sql;
1206 $find_by_sql = array_shift($args);
1211 $options = $this->_extractOptionsFromArgs($args);
1213 $query_values = $args;
1214 $query_arguments_count = count($query_values);
1216 list($sql, $requested_args) = $this->_getFindBySqlAndColumns($find_by_sql, $query_values);
1218 if($query_arguments_count != count($requested_args)){
1219 trigger_error(Ak
::t('Argument list did not match expected set. Requested arguments are:').join(', ',$requested_args),E_USER_ERROR
);
1224 $true_bool_values = array(true,1,'true','True','TRUE','1','y','Y','yes','Yes','YES','s','Si','SI','V','v','T','t');
1226 foreach ($requested_args as $k=>$v){
1227 switch ($this->getColumnType($v)) {
1229 $query_values[$k] = in_array($query_values[$k],$true_bool_values) ?
true : false;
1234 $query_values[$k] = str_replace('/','-', $this->castAttributeForDatabase($k,$query_values[$k],false));
1242 $conditions = array($sql);
1243 foreach ($query_values as $bind_value){
1244 $conditions[] = $bind_value;
1247 * @todo merge_conditions
1249 $options['conditions'] = $conditions;
1251 $result =& Ak
::call_user_func_array(array(&$this,'find'), array($fetch,$options));
1256 function _getFindBySqlAndColumns($find_by_sql, &$query_values)
1258 $sql = str_replace(array('(',')','||','|','&&','&',' '),array(' ( ',' ) ',' OR ',' OR ',' AND ',' AND ',' '), $find_by_sql);
1259 $operators = array('AND','and','(',')','&','&&','NOT','<>','OR','|','||');
1260 $pieces = explode(' ',$sql);
1261 $pieces = array_diff($pieces,array(' ',''));
1262 $params = array_diff($pieces,$operators);
1263 $operators = array_diff($pieces,$params);
1266 $parameter_count = 0;
1267 $requested_args = array();
1268 foreach ($pieces as $piece){
1269 if(in_array($piece,$params) && $this->hasColumn($piece)){
1270 $new_sql .= $piece.' = ? ';
1271 $requested_args[$parameter_count] = $piece;
1273 }elseif (!in_array($piece,$operators)){
1275 if(strstr($piece,':')){
1276 $_tmp_parts = explode(':',$piece);
1277 if($this->hasColumn($_tmp_parts[0])){
1278 $query_values[$parameter_count] = isset($query_values[$parameter_count]) ?
$query_values[$parameter_count] : $this->get($_tmp_parts[0]);
1279 switch (strtolower($_tmp_parts[1])) {
1285 $query_values[$parameter_count] = '%'.$query_values[$parameter_count].'%';
1286 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1294 $query_values[$parameter_count] = $query_values[$parameter_count].'%';
1295 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1302 case 'finishes_with':
1303 $query_values[$parameter_count] = '%'.$query_values[$parameter_count];
1304 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1307 $query_values[$parameter_count] = $query_values[$parameter_count];
1308 $new_sql .= $_tmp_parts[0].' '.$_tmp_parts[1].' ? ';
1311 $requested_args[$parameter_count] = $_tmp_parts[0];
1314 $new_sql .= $_tmp_parts[0];
1317 $new_sql .= $piece.' ';
1320 $new_sql .= $piece.' ';
1324 return array($new_sql, $requested_args);
1329 * Given a condition that uses bindings like "user = ? AND created_at > ?" will return a
1330 * string replacing the "?" bindings with the column values for current Active Record
1334 function _getVariableSqlCondition($variable_condition)
1336 $query_values = array();
1337 list($sql, $requested_columns) = $this->_getFindBySqlAndColumns($variable_condition, $query_values);
1338 $replacements = array();
1339 $sql = preg_replace('/((('.join($requested_columns,'|').') = \?) = \?)/','$2', $sql);
1340 foreach ($requested_columns as $attribute){
1341 $replacements[$attribute] = $this->castAttributeForDatabase($attribute, $this->get($attribute));
1343 return trim(preg_replace('/('.join('|',array_keys($replacements)).')\s+([^\?]+)\s+\?/e', "isset(\$replacements['\\1']) ? '\\1 \\2 '.\$replacements['\\1']:'\\1 \\2 null'", $sql));
1347 function constructFinderSql($options, $select_from_prefix = 'default')
1349 $sql = isset($options['select_prefix']) ?
$options['select_prefix'] : ($select_from_prefix == 'default' ?
'SELECT * FROM '.$this->getTableName() : $select_from_prefix);
1350 $sql .= !empty($options['joins']) ?
' '.$options['joins'] : '';
1352 $this->addConditions($sql, isset($options['conditions']) ?
$options['conditions'] : array());
1354 // Create an alias for order
1355 if(empty($options['order']) && !empty($options['sort'])){
1356 $options['order'] = $options['sort'];
1359 $sql .= !empty($options['order']) ?
' ORDER BY '.$options['order'] : '';
1361 $this->_db
->addLimitAndOffset($sql,$options);
1368 * Adds a sanitized version of $conditions to the $sql string. Note that the passed $sql string is changed.
1370 function addConditions(&$sql, $conditions = null)
1372 $concat = empty($sql) ?
'' : ' WHERE ';
1373 if (empty($conditions) && $this->_getDatabaseType() == 'sqlite') $conditions = '1'; // sqlite HACK
1374 if(!empty($conditions)){
1375 $sql .= $concat.$conditions;
1379 if($this->getInheritanceColumn() !== false && $this->descendsFromActiveRecord($this)){
1380 $type_condition = $this->typeCondition();
1381 $sql .= !empty($type_condition) ?
$concat.$type_condition : '';
1387 * Gets a sanitized version of the input array. Each element will be escaped
1389 function getSanitizedConditionsArray($conditions_array)
1392 foreach ($conditions_array as $k=>$v){
1393 $k = str_replace(':','',$k); // Used for Oracle type bindings
1394 if($this->hasColumn($k)){
1395 $v = $this->castAttributeForDatabase($k, $v);
1404 * This functions is used to get the conditions from an AkRequest object
1406 function getConditions($conditions, $prefix = '', $model_name = null)
1408 $model_name = isset($model_name) ?
$model_name : $this->getModelName();
1409 $model_conditions = !empty($conditions[$model_name]) ?
$conditions[$model_name] : $conditions;
1410 if(is_a($this->$model_name)){
1411 $model_instance =& $this->$model_name;
1413 $model_instance =& $this;
1415 $new_conditions = array();
1416 if(is_array($model_conditions)){
1417 foreach ($model_conditions as $col=>$value){
1418 if($model_instance->hasColumn($col)){
1419 $new_conditions[$prefix.$col] = $value;
1423 return $new_conditions;
1430 function _quoteColumnName($column_name)
1432 return $this->_db
->nameQuote
.$column_name.$this->_db
->nameQuote
;
1439 * EXPERIMENTAL: Will allow to create finders when PHP includes aggregate_methods as a stable feature on PHP4, for PHP5 we might use __call
1443 function _buildFinders($finderFunctions = array('find','findFirst'))
1445 if(!$this->_dynamicMethods
){
1448 $columns = !is_array($this->_dynamicMethods
) ?
array_keys($this->getColumns()) : $this->_dynamicMethods
;
1449 $class_name = 'ak_'.md5(serialize($columns));
1450 if(!class_exists($class_name)){
1451 $permutations = Ak
::permute($columns);
1452 $implementations = '';
1453 foreach ($finderFunctions as $finderFunction){
1454 foreach ($permutations as $permutation){
1455 $permutation = array_map(array('AkInflector','camelize'),$permutation);
1456 foreach ($permutation as $k=>$v){
1457 $method_name = $finderFunction.'By'.join($permutation,'And');
1458 $implementation = 'function &'.$method_name.'(';
1462 foreach ($permutation as $column){
1463 $column = AkInflector
::underscore($column);
1464 $params .= "$$column, ";
1465 $first_param .= "$column ";
1468 $implementation .= trim($params,' ,')."){\n";
1469 $implementation .= '$options = func_num_args() == '.$i.' ? func_get_arg('.($i-1).') : array();'."\n";
1470 $implementation .= 'return $this->'.$finderFunction.'By(\''.$first_param.'\', '.trim($params,' ,').", \$options);\n }\n";
1471 $implementations[$method_name] = $implementation;
1472 array_shift($permutation);
1476 eval('class '.$class_name.' { '.join("\n",$implementations).' } ');
1479 aggregate_methods(&$this, $class_name);
1484 * Finder methods must instantiate through this method to work with the single-table inheritance model and
1485 * eager loading associations.
1486 * that makes it possible to create objects of different types from the same table.
1488 function &instantiate($record, $set_as_new = true)
1490 $inheritance_column = $this->getInheritanceColumn();
1491 if(!empty($record[$inheritance_column])){
1492 $inheritance_column = $record[$inheritance_column];
1493 $inheritance_model_name = AkInflector
::camelize($inheritance_column);
1494 @require_once
(AkInflector
::toModelFilename($inheritance_model_name));
1495 if(!class_exists($inheritance_model_name)){
1496 trigger_error($this->t("The single-table inheritance mechanism failed to locate the subclass: '%class_name'. ".
1497 "This error is raised because the column '%column' is reserved for storing the class in case of inheritance. ".
1498 "Please rename this column if you didn't intend it to be used for storing the inheritance class ".
1499 "or overwrite #{self.to_s}.inheritance_column to use another column for that information.",
1500 array('%class_name'=>$inheritance_model_name, '%column'=>$this->getInheritanceColumn())),E_USER_ERROR
);
1504 $model_name = isset($inheritance_model_name) ?
$inheritance_model_name : $this->getModelName();
1505 $object =& new $model_name('attributes', $record);
1507 $object->_newRecord
= $set_as_new;
1509 (AK_CLI
&& AK_ENVIRONMENT
== 'development') ?
$object ->toString() : null;
1514 /*/Finding records*/
1520 ====================================================================
1522 function descendsFromActiveRecord(&$object)
1524 if(substr(strtolower(get_parent_class($object)),-12) == 'activerecord'){
1527 if(!method_exists($object, 'getInheritanceColumn')){
1530 $inheritance_column = $object->getInheritanceColumn();
1531 return !empty($inheritance_column);
1535 * Gets the column name for use with single table inheritance. Can be overridden in subclasses.
1537 function getInheritanceColumn()
1539 return empty($this->_inheritanceColumn
) ?
($this->hasColumn('type') ?
'type' : false ) : $this->_inheritanceColumn
;
1543 * Defines the column name for use with single table inheritance. Can be overridden in subclasses.
1545 function setInheritanceColumn($column_name)
1547 if(!$this->hasColumn($column_name)){
1548 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
);
1550 }elseif($this->getColumnType($column_name) != 'string'){
1551 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
);
1554 $this->_inheritanceColumn
= $column_name;
1560 function getSubclasses()
1562 $current_class = get_class($this);
1563 $subclasses = array();
1564 $classes = get_declared_classes();
1566 while ($class = array_shift($classes)) {
1567 $parent_class = get_parent_class($class);
1568 if($parent_class == $current_class ||
in_array($parent_class,$subclasses)){
1569 $subclasses[] = $class;
1570 }elseif(!empty($parent_class)){
1571 $classes[] = $parent_class;
1574 $subclasses = array_unique(array_map(array(&$this,'_getModelName'),$subclasses));
1579 function typeCondition()
1581 $inheritance_column = $this->getInheritanceColumn();
1582 $type_condition = array();
1583 $table_name = $this->getTableName();
1584 $available_types = array_merge(array($this->getModelName()),$this->getSubclasses());
1585 foreach ($available_types as $subclass){
1586 $type_condition[] = ' '.$table_name.'.'.$inheritance_column.' = \''.AkInflector
::humanize(AkInflector
::underscore($subclass)).'\' ';
1588 return empty($type_condition) ?
'' : '('.join('OR',$type_condition).') ';
1591 /*/Table inheritance*/
1597 ====================================================================
1598 See also: Getting Attributes, Model Attributes, Toggling Attributes, Counting Attributes.
1600 function setAttribute($attribute, $value, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS
, $compose_after_set = true)
1602 if($attribute[0] == '_'){
1606 if($this->isFrozen()){
1609 if($inspect_for_callback_child_method === true && method_exists($this,'set'.AkInflector
::camelize($attribute))){
1611 $watchdog[$attribute] = @$watchdog[$attribute]+
1;
1612 if($watchdog[$attribute] == 5000){
1613 if((!defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION')) ||
defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_SET_RECURSION
){
1614 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
);
1618 $this->{$attribute.'_before_type_cast'} = $value;
1619 return $this->{'set'.AkInflector
::camelize($attribute)}($value);
1621 if($this->hasAttribute($attribute)){
1622 $this->{$attribute.'_before_type_cast'} = $value;
1623 $this->$attribute = $value;
1624 if($compose_after_set && !empty($this->_combinedAttributes
) && !$this->requiredForCombination($attribute)){
1625 $combined_attributes = $this->_getCombinedAttributesWhereThisAttributeIsUsed($attribute);
1626 foreach ($combined_attributes as $combined_attribute){
1627 $this->composeCombinedAttribute($combined_attribute);
1630 if ($compose_after_set && $this->isCombinedAttribute($attribute)){
1631 $this->decomposeCombinedAttribute($attribute);
1633 }elseif(substr($attribute,-12) == 'confirmation' && $this->hasAttribute(substr($attribute,0,-13))){
1634 $this->$attribute = $value;
1637 if($this->_internationalize
){
1638 if(is_array($value)){
1639 $this->setAttributeLocales($attribute, $value);
1640 }elseif(is_string($inspect_for_callback_child_method)){
1641 $this->setAttributeByLocale($attribute, $value, $inspect_for_callback_child_method);
1643 $this->_groupInternationalizedAttribute($attribute, $value);
1649 function set($attribute, $value = null, $inspect_for_callback_child_method = true, $compose_after_set = true)
1651 if(is_array($attribute)){
1652 return $this->setAttributes($attribute);
1654 return $this->setAttribute($attribute, $value, $inspect_for_callback_child_method, $compose_after_set);
1658 * Allows you to set all the attributes at once by passing in an array with
1659 * keys matching the attribute names (which again matches the column names).
1660 * Sensitive attributes can be protected from this form of mass-assignment by
1661 * using the $this->setProtectedAttributes method. Or you can alternatively
1662 * specify which attributes can be accessed in with the $this->setAccessibleAttributes method.
1663 * Then all the attributes not included in that won?t be allowed to be mass-assigned.
1665 function setAttributes($attributes, $override_attribute_protection = false)
1667 $this->parseAkelosArgs($attributes);
1668 if(!$override_attribute_protection){
1669 $attributes = $this->removeAttributesProtectedFromMassAssignment($attributes);
1671 if(!empty($attributes) && is_array($attributes)){
1672 foreach ($attributes as $k=>$v){
1673 $this->setAttribute($k, $v);
1679 function setId($value)
1681 if($this->isFrozen()){
1684 $pk = $this->getPrimaryKey();
1685 $this->$pk = $value;
1690 /*/Setting Attributes*/
1694 ====================================================================
1695 See also: Setting Attributes, Model Attributes, Toggling Attributes, Counting Attributes.
1698 function getAttribute($attribute, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS
)
1700 if($attribute[0] == '_'){
1704 if($inspect_for_callback_child_method === true && method_exists($this,'get'.AkInflector
::camelize($attribute))){
1706 $watchdog[@$attribute] = @$watchdog[$attribute]+
1;
1707 if($watchdog[$attribute] == 5000){
1708 if((!defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION')) ||
defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_GET_RECURSION
){
1709 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
);
1713 $value = $this->{'get'.AkInflector
::camelize($attribute)}();
1714 return $this->getInheritanceColumn() === $attribute ? AkInflector
::humanize(AkInflector
::underscore($value)) : $value;
1716 if(isset($this->$attribute) ||
(!isset($this->$attribute) && $this->isCombinedAttribute($attribute))){
1717 if($this->hasAttribute($attribute)){
1718 if (!empty($this->_combinedAttributes
) && $this->isCombinedAttribute($attribute)){
1719 $this->composeCombinedAttribute($attribute);
1721 return isset($this->$attribute) ?
$this->$attribute : null;
1722 }elseif($this->_internationalize
&& $this->_isInternationalizeCandidate($attribute)){
1723 if(!empty($this->$attribute) && is_string($this->$attribute)){
1724 return $this->$attribute;
1726 $current_locale = $this->getCurrentLocale();
1727 if(!empty($this->$attribute[$current_locale]) && is_array($this->$attribute)){
1728 return $this->$attribute[$current_locale];
1730 return $this->getAttribute($current_locale.'_'.$attribute);
1734 if($this->_internationalize
){
1735 return $this->getAttributeByLocale($attribute, is_bool($inspect_for_callback_child_method) ?
$this->getCurrentLocale() : $inspect_for_callback_child_method);
1740 function get($attribute = null, $inspect_for_callback_child_method = true)
1742 return !isset($attribute) ?
$this->getAttributes($inspect_for_callback_child_method) : $this->getAttribute($attribute, $inspect_for_callback_child_method);
1746 * Returns an array of all the attributes with their names as keys and clones of their objects as values in case they are objects.
1748 function getAttributes()
1750 $attributes = array();
1751 $available_attributes = $this->getAvailableAttributes();
1752 foreach ($available_attributes as $available_attribute){
1753 $attribute = $this->getAttribute($available_attribute['name']);
1754 $attributes[$available_attribute['name']] = AK_PHP5
&& is_object($attribute) ?
clone($attribute) : $attribute;
1757 if($this->_internationalize
){
1758 $current_locale = $this->getCurrentLocale();
1759 foreach ($this->getInternationalizedColumns() as $column=>$languages){
1760 if(empty($attributes[$column]) && isset($attributes[$current_locale.'_'.$column]) && in_array($current_locale,$languages)){
1761 $attributes[$column] = $attributes[$current_locale.'_'.$column];
1771 * 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.
1775 return $this->{$this->getPrimaryKey()};
1778 /*/Getting Attributes*/
1784 ====================================================================
1785 See also: Setting Attributes, Getting Attributes.
1788 * Turns an attribute that's currently true into false and vice versa. Returns attribute value.
1790 function toggleAttribute($attribute)
1792 $value = $this->getAttribute($attribute);
1793 $new_value = $value ?
false : true;
1794 $this->setAttribute($attribute, $new_value);
1800 * Toggles the attribute and saves the record.
1802 function toggleAttributeAndSave($attribute)
1804 $value = $this->toggleAttribute($attribute);
1805 if($this->updateAttribute($attribute, $value)){
1811 /*/Toggling Attributes*/
1816 ====================================================================
1817 See also: Counting Records, Setting Attributes, Getting Attributes.
1821 * Increments the specified counter by one. So $DiscussionBoard->incrementCounter("post_count",
1822 * $discussion_board_id); would increment the "post_count" counter on the board responding to
1823 * $discussion_board_id. This is used for caching aggregate values, so that they doesn't need to
1824 * be computed every time. Especially important for looping over a collection where each element
1825 * require a number of aggregate values. Like the $DiscussionBoard that needs to list both the number of posts and comments.
1827 function incrementCounter($counter_name, $id, $difference = 1)
1829 return $this->updateAll("$counter_name = $counter_name + $difference", $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 1;
1833 * Works like AkActiveRecord::incrementCounter, but decrements instead.
1835 function decrementCounter($counter_name, $id, $difference = 1)
1837 return $this->updateAll("$counter_name = $counter_name - $difference", $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 1;
1841 * Initializes the attribute to zero if null and subtracts one. Only makes sense for number-based attributes. Returns attribute value.
1843 function decrementAttribute($attribute)
1845 if(!isset($this->$attribute)){
1846 $this->$attribute = 0;
1848 return $this->$attribute -= 1;
1852 * Decrements the attribute and saves the record.
1854 function decrementAndSaveAttribute($attribute)
1856 return $this->updateAttribute($attribute,$this->decrementAttribute($attribute));
1861 * Initializes the attribute to zero if null and adds one. Only makes sense for number-based attributes. Returns attribute value.
1863 function incrementAttribute($attribute)
1865 if(!isset($this->$attribute)){
1866 $this->$attribute = 0;
1868 return $this->$attribute +
= 1;
1872 * Increments the attribute and saves the record.
1874 function incrementAndSaveAttribute($attribute)
1876 return $this->updateAttribute($attribute,$this->incrementAttribute($attribute));
1879 /*/Counting Attributes*/
1882 Protecting attributes
1883 ====================================================================
1887 * If this macro is used, only those attributed named in it will be accessible
1888 * for mass-assignment, such as new ModelName($attributes) and $this->attributes($attributes).
1889 * This is the more conservative choice for mass-assignment protection.
1890 * If you'd rather start from an all-open default and restrict attributes as needed,
1891 * have a look at AkActiveRecord::setProtectedAttributes().
1893 function setAccessibleAttributes()
1895 $args = func_get_args();
1896 $this->_accessibleAttributes
= array_unique(array_merge((array)$this->_accessibleAttributes
, $args));
1900 * Attributes named in this macro are protected from mass-assignment, such as
1901 * new ModelName($attributes) and $this->attributes(attributes). Their assignment
1902 * will simply be ignored. Instead, you can use the direct writer methods to do assignment.
1903 * This is meant to protect sensitive attributes to be overwritten by URL/form hackers.
1907 * class Customer extends ActiveRecord
1909 * function Customer()
1911 * $this->setProtectedAttributes('credit_rating');
1915 * $Customer = new Customer('name' => 'David', 'credit_rating' => 'Excellent');
1916 * $Customer->credit_rating // => null
1917 * $Customer->attributes(array('description' => 'Jolly fellow', 'credit_rating' => 'Superb'));
1918 * $Customer->credit_rating // => null
1920 * $Customer->credit_rating = 'Average'
1921 * $Customer->credit_rating // => 'Average'
1924 function setProtectedAttributes()
1926 $args = func_get_args();
1927 $this->_protectedAttributes
= array_unique(array_merge((array)$this->_protectedAttributes
, $args));
1930 function removeAttributesProtectedFromMassAssignment($attributes)
1932 if(!empty($this->_accessibleAttributes
) && is_array($this->_accessibleAttributes
) && is_array($attributes)){
1933 foreach (array_keys($attributes) as $k){
1934 if(!in_array($k,$this->_accessibleAttributes
)){
1935 unset($attributes[$k]);
1938 }elseif (!empty($this->_protectedAttributes
) && is_array($this->_protectedAttributes
) && is_array($attributes)){
1939 foreach (array_keys($attributes) as $k){
1940 if(in_array($k,$this->_protectedAttributes
)){
1941 unset($attributes[$k]);
1948 /*/Protecting attributes*/
1953 ====================================================================
1954 See also: Getting Attributes, Setting Attributes.
1957 * Returns an array of all the attributes that have been specified for serialization as keys and the objects as values.
1959 function getSerializedAttributes()
1961 return isset($this->_serializedAttributes
) ?
$this->_serializedAttributes
: array();
1964 function getAvailableAttributes()
1966 return array_merge($this->getColumns(), $this->getAvailableCombinedAttributes());
1969 function getAttributeCaption($attribute)
1971 return $this->t(AkInflector
::humanize($attribute));
1975 * This function is useful in case you need to know if attributes have been assigned to an object.
1977 function hasAttributesDefined()
1979 $attributes = join('',$this->getAttributes());
1980 return empty($attributes);
1985 * Returns the primary key field.
1987 function getPrimaryKey()
1989 if(!isset($this->_primaryKey
)){
1990 $this->setPrimaryKey();
1992 return $this->_primaryKey
;
1995 function getColumnNames()
1997 if(empty($this->_columnNames
)){
1998 $columns = $this->getColumns();
1999 foreach ($columns as $column_name=>$details){
2000 $this->_columnNames
[$column_name] = isset($details->columnName
) ?
$this->t($details->columnName
) : $this->getAttributeCaption($column_name);
2003 return $this->_columnNames
;
2008 * Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count",
2009 * and columns used for single table inheritance has been removed.
2011 function getContentColumns()
2013 $inheritance_column = $this->getInheritanceColumn();
2014 $columns = $this->getColumns();
2015 foreach ($columns as $name=>$details){
2016 if((substr($name,-3) == '_id' ||
substr($name,-6) == '_count') ||
2017 !empty($details['primaryKey']) ||
($inheritance_column !== false && $inheritance_column == $name)){
2018 unset($columns[$name]);
2025 * Returns an array of names for the attributes available on this object sorted alphabetically.
2027 function getAttributeNames()
2029 if(!isset($this->_activeRecordHasBeenInstantiated
)){
2030 return Ak
::handleStaticCall();
2032 $attributes = array_keys($this->getAvailableAttributes());
2033 $names = array_combine($attributes,array_map(array(&$this,'getAttributeCaption'), $attributes));
2040 * Returns true if the specified attribute has been set by the user or by a database load and is neither null nor empty?
2042 function isAttributePresent($attribute)
2044 $value = $this->getAttribute($attribute);
2045 return !empty($value);
2049 * Returns true if given attribute exists for this Model.
2051 * @param string $attribute
2054 function hasAttribute ($attribute)
2056 empty($this->_columns
) ?
$this->getColumns() : $this->_columns
; // HINT: only used by HasAndBelongsToMany joinObjects, if the table is not present yet!
2057 return isset($this->_columns
[$attribute]) ||
(!empty($this->_combinedAttributes
) && $this->isCombinedAttribute($attribute));
2060 /*/Model Attributes*/
2065 ====================================================================
2067 * The Akelos Framework has a handy way to represent combined fields.
2068 * You can add a new attribute to your models using a printf patter to glue
2069 * multiple parameters in a single one.
2071 * For example, If we set...
2072 * $this->addCombinedAttributeConfiguration('name', "%s %s", 'first_name', 'last_name');
2073 * $this->addCombinedAttributeConfiguration('date', "%04d-%02d-%02d", 'year', 'month', 'day');
2074 * $this->setAttributes('first_name=>','John','last_name=>','Smith','year=>',2005,'month=>',9,'day=>',27);
2076 * $this->name // will have "John Smith" as value and
2077 * $this->date // will be 2005-09-27
2079 * On the other hand if you do
2081 * $this->setAttribute('date', '2008-11-30');
2083 * All the 'year', 'month' and 'day' getters will be fired (if they exist) the following attributes will be set
2085 * $this->year // will be 2008
2086 * $this->month // will be 11 and
2087 * $this->day // will be 27
2089 * Sometimes you might need a pattern for composing and another for decomposing attributes. In this case you can specify
2090 * an array as the pattern values, where first element will be the composing pattern and second element will be used
2093 * You can also specify a callback method from this object function instead of a pattern. You can also assign a callback
2094 * for composing and another for decomposing by passing their names as an array like on the patterns.
2097 * class User extends ActiveRecord
2101 * // You can use a multiple patterns array where "%s, %s" will be used for combining fields and "%[^,], %s" will be used
2102 * // for decomposing fields. (as you can see you can also use regular expressions on your patterns)
2103 * $User->addCombinedAttributeConfiguration('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name');
2105 * //Here we set email_link so compose_email_link() will be triggered for building up the field and parse_email_link will
2106 * // be used for getting the fields out
2107 * $User->addCombinedAttributeConfiguration('email_link', array("compose_email_link","parse_email_link"), 'email', 'name');
2109 * // We need to tell the ActiveRecord to load it's magic (see the example below for a simpler solution)
2110 * $attributes = (array)func_get_args();
2111 * return $this->init($attributes);
2114 * function compose_email_link()
2116 * $args = func_get_arg(0);
2117 * return "<a href=\'mailto:{$args[\'email\']}\'>{$args[\'name\']}</a>";
2119 * function parse_email_link($email_link)
2121 * $results = sscanf($email_link, "<a href=\'mailto:%[^\']\'>%[^<]</a>");
2122 * return array(\'email\'=>$results[0],\'name\'=>$results[1]);
2128 * You can also simplify your live by declaring the combined attributes as a class variable like:
2130 * class User extends ActiveRecord
2132 * var $combined_attributes array(
2133 * array('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name')
2134 * array('email_link', array("compose_email_link","parse_email_link"), 'email', 'name')
2144 * Returns true if given attribute is a combined attribute for this Model.
2146 * @param string $attribute
2149 function isCombinedAttribute ($attribute)
2151 return !empty($this->_combinedAttributes
) && isset($this->_combinedAttributes
[$attribute]);
2154 function addCombinedAttributeConfiguration($attribute)
2156 $args = is_array($attribute) ?
$attribute : func_get_args();
2157 $columns = array_slice($args,2);
2158 $invalid_columns = array();
2159 foreach ($columns as $colum){
2160 if(!$this->hasAttribute($colum)){
2161 $invalid_columns[] = $colum;
2164 if(!empty($invalid_columns)){
2165 trigger_error(Ak
::t('There was an error while setting the composed field "%field_name", the following mapping column/s "%columns" do not exist',
2166 array('%field_name'=>$args[0],'%columns'=>join(', ',$invalid_columns))), E_USER_ERROR
);
2168 $attribute = array_shift($args);
2169 $this->_combinedAttributes
[$attribute] = $args;
2170 $this->composeCombinedAttribute($attribute);
2174 function composeCombinedAttributes()
2177 if(!empty($this->_combinedAttributes
)){
2178 $attributes = array_keys($this->_combinedAttributes
);
2179 foreach ($attributes as $attribute){
2180 $this->composeCombinedAttribute($attribute);
2185 function composeCombinedAttribute($combined_attribute)
2187 if($this->isCombinedAttribute($combined_attribute)){
2188 $config = $this->_combinedAttributes
[$combined_attribute];
2189 $pattern = array_shift($config);
2191 $pattern = is_array($pattern) ?
$pattern[0] : $pattern;
2194 foreach ($config as $attribute){
2195 if(isset($this->$attribute)){
2196 $got[$attribute] = $this->getAttribute($attribute);
2199 if(count($got) === count($config)){
2200 $this->$combined_attribute = method_exists($this, $pattern) ?
$this->{$pattern}($got) : vsprintf($pattern, $got);
2208 function _getCombinedAttributesWhereThisAttributeIsUsed($attribute)
2211 foreach ($this->_combinedAttributes
as $combined_attribute=>$settings){
2212 if(in_array($attribute,$settings)){
2213 $result[] = $combined_attribute;
2220 function requiredForCombination($attribute)
2222 foreach ($this->_combinedAttributes
as $settings){
2223 if(in_array($attribute,$settings)){
2230 function hasCombinedAttributes()
2232 return count($this->getCombinedSubattributes()) === 0 ?
false :true;
2235 function getCombinedSubattributes($attribute)
2238 if(is_array($this->_combinedAttributes
[$attribute])){
2239 $attributes = $this->_combinedAttributes
[$attribute];
2240 array_shift($attributes);
2241 foreach ($attributes as $attribute_to_check){
2242 if(isset($this->_combinedAttributes
[$attribute_to_check])){
2243 $result[] = $attribute_to_check;
2250 function decomposeCombinedAttributes()
2252 if(!empty($this->_combinedAttributes
)){
2253 $attributes = array_keys($this->_combinedAttributes
);
2254 foreach ($attributes as $attribute){
2255 $this->decomposeCombinedAttribute($attribute);
2260 function decomposeCombinedAttribute($combined_attribute, $used_on_combined_fields = false)
2262 if(isset($this->$combined_attribute) && $this->isCombinedAttribute($combined_attribute)){
2263 $config = $this->_combinedAttributes
[$combined_attribute];
2264 $pattern = array_shift($config);
2265 $pattern = is_array($pattern) ?
$pattern[1] : $pattern;
2267 if(method_exists($this, $pattern)){
2268 $pieces = $this->{$pattern}($this->$combined_attribute);
2269 if(is_array($pieces)){
2270 foreach ($pieces as $k=>$v){
2271 $is_combined = $this->isCombinedAttribute($k);
2273 $this->decomposeCombinedAttribute($k);
2275 $this->setAttribute($k, $v, true, !$is_combined);
2277 if($is_combined && !$used_on_combined_fields){
2278 $combined_attributes_contained_on_this_attribute = $this->getCombinedSubattributes($combined_attribute);
2279 if(count($combined_attributes_contained_on_this_attribute)){
2280 $this->decomposeCombinedAttribute($combined_attribute, true);
2285 $got = sscanf($this->$combined_attribute, $pattern);
2286 for ($x=0; $x<count($got); $x++
){
2287 $attribute = $config[$x];
2288 $is_combined = $this->isCombinedAttribute($attribute);
2290 $this->decomposeCombinedAttribute($attribute);
2292 $this->setAttribute($attribute, $got[$x], true, !$is_combined);
2298 function getAvailableCombinedAttributes()
2300 $combined_attributes = array();
2301 foreach ($this->_combinedAttributes
as $attribute=>$details){
2302 $combined_attributes[$attribute] = array('name'=>$attribute, 'type'=>'string', 'path' => array_shift($details), 'uses'=>$details);
2304 return !empty($this->_combinedAttributes
) && is_array($this->_combinedAttributes
) ?
$combined_attributes : array();
2307 /*/Combined attributes*/
2314 ====================================================================
2317 * Establishes the connection to the database. Accepts either a profile name specified in config/config.php or
2318 * an array as input where the 'type' key must be specified with the name of a database adapter (in lower-case)
2319 * example for regular databases (MySQL, Postgresql, etc):
2321 * $AkActiveRecord->establishConnection('development');
2322 * $AkActiveRecord->establishConnection('super_user');
2324 * $AkActiveRecord->establishConnection(
2326 * 'type' => "mysql",
2327 * 'host' => "localhost",
2328 * 'username' => "myuser",
2329 * 'password' => "mypass",
2330 * 'database' => "somedatabase"
2333 * Example for SQLite database:
2335 * $AkActiveRecord->establishConnection(
2337 * 'type' => "sqlite",
2338 * 'dbfile' => "path/to/dbfile"
2342 function &establishConnection($specification_or_profile = AK_DEFAULT_DATABASE_PROFILE
)
2344 $adapter =& AkDbAdapter
::getInstance($specification_or_profile);
2345 return $this->setConnection(&$adapter);
2350 * Returns true if a connection that's accessible to this class have already been opened.
2352 function isConnected()
2354 return isset($this->_db
);
2358 * Returns the connection currently associated with the class. This can also be used to
2359 * "borrow" the connection to do database work unrelated to any of the specific Active Records.
2361 function &getConnection()
2367 * Sets the connection for the class.
2369 function &setConnection($db_adapter = null)
2371 if (is_null($db_adapter)){
2372 $db_adapter =& AkDbAdapter
::getInstance();
2374 return $this->_db
=& $db_adapter;
2380 function _getDatabaseType()
2382 return $this->_db
->type();
2384 /*/Database connection*/
2389 ====================================================================
2390 See also: Database Reflection.
2394 * Defines the primary key field ? can be overridden in subclasses.
2396 function setPrimaryKey($primary_key = 'id')
2398 if(!$this->hasColumn($primary_key)){
2399 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
);
2401 $this->_primaryKey
= $primary_key;
2406 function getTableName($modify_for_associations = true)
2408 if(!isset($this->_tableName
)){
2409 // We check if we are on a inheritance Table Model
2410 $this->getClassForDatabaseTableMapping();
2411 if(!isset($this->_tableName
)){
2412 $this->setTableName();
2416 if($modify_for_associations && isset($this->_associationTablePrefixes
[$this->_tableName
])){
2417 return $this->_associationTablePrefixes
[$this->_tableName
];
2420 return $this->_tableName
;
2423 function setTableName($table_name = null, $check_for_existence = AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES
, $check_mode = false)
2425 static $available_tables;
2426 if(empty($table_name)){
2427 $table_name = AkInflector
::tableize($this->getModelName());
2429 if($check_for_existence){
2430 if(!isset($available_tables) ||
$check_mode){
2431 if(!isset($this->_db
)){
2432 $this->setConnection();
2434 if(empty($_SESSION['__activeRecordColumnsSettingsCache']['available_tables']) ||
2435 !AK_ACTIVE_RECORD_ENABLE_PERSISTENCE
){
2436 $_SESSION['__activeRecordColumnsSettingsCache']['available_tables'] = $this->_db
->availableTables();
2438 $available_tables = $_SESSION['__activeRecordColumnsSettingsCache']['available_tables'];
2440 if(!in_array($table_name,(array)$available_tables)){
2442 trigger_error(Ak
::t('Unable to set "%table_name" table for the model "%model".'.
2443 ' There is no "%table_name" available into current database layout.'.
2444 ' Set AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES constant to false in order to'.
2445 ' avoid table name validation',array('%table_name'=>$table_name,'%model'=>$this->getModelName())),E_USER_WARNING
);
2450 $this->_tableName
= $table_name;
2455 function getOnlyAvailableAttributes($attributes)
2457 $table_name = $this->getTableName();
2458 $ret_attributes = array();
2459 if(!empty($attributes) && is_array($attributes)){
2460 $available_attributes = $this->getAvailableAttributes();
2462 $keys = array_keys($attributes);
2463 $size = sizeOf($keys);
2464 for ($i=0; $i < $size; $i++
){
2465 $k = str_replace($table_name.'.','',$keys[$i]);
2466 if(isset($available_attributes[$k]['name'][$k])){
2467 $ret_attributes[$k] =& $attributes[$keys[$i]];
2471 return $ret_attributes;
2474 function getColumnsForAttributes($attributes)
2476 $ret_attributes = array();
2477 $table_name = $this->getTableName();
2478 if(!empty($attributes) && is_array($attributes)){
2479 $columns = $this->getColumns();
2480 foreach ($attributes as $k=>$v){
2481 $k = str_replace($table_name.'.','',$k);
2482 if(isset($columns[$k]['name'][$k])){
2483 $ret_attributes[$k] = $v;
2487 return $ret_attributes;
2491 * Returns true if given attribute exists for this Model.
2493 * @param string $name Name of table to look in
2496 function hasColumn($column)
2498 empty($this->_columns
) ?
$this->getColumns() : $this->_columns
;
2499 return isset($this->_columns
[$column]);
2507 ====================================================================
2508 See also: Table Settings, Type Casting.
2513 * Initializes the attributes array with keys matching the columns from the linked table and
2514 * the values matching the corresponding default value of that column, so
2515 * that a new instance, or one populated from a passed-in array, still has all the attributes
2516 * that instances loaded from the database would.
2518 function attributesFromColumnDefinition()
2520 $attributes = array();
2522 foreach ((array)$this->getColumns() as $column_name=>$column_settings){
2523 if (!isset($column_settings['primaryKey']) && isset($column_settings['hasDefault'])) {
2524 $attributes[$column_name] = $this->_extractValueFromDefault($column_settings['defaultValue']);
2526 $attributes[$column_name] = null;
2533 * Gets information from the database engine about a single table
2537 function _databaseTableInternals($table)
2539 if(empty($_SESSION['__activeRecordColumnsSettingsCache']['database_table_'.$table.'_internals']) ||
!AK_ACTIVE_RECORD_ENABLE_PERSISTENCE
){
2540 $_SESSION['__activeRecordColumnsSettingsCache']['database_table_'.$table.'_internals'] = $this->_db
->getColumnDetails($table);
2542 $cache[$table] = $_SESSION['__activeRecordColumnsSettingsCache']['database_table_'.$table.'_internals'];
2544 return $cache[$table];
2547 function getColumnsWithRegexBoundaries()
2549 $columns = array_keys($this->getColumns());
2550 foreach ($columns as $k=>$column){
2551 $columns[$k] = '/([^\.])\b('.$column.')\b/';
2558 * If is the first time we use a model this function will run the installer for the model if it exists
2562 function _runCurrentModelInstallerIfExists(&$column_objects)
2564 static $installed_models = array();
2565 if(!defined('AK_AVOID_AUTOMATIC_ACTIVE_RECORD_INSTALLERS') && !in_array($this->getModelName(), $installed_models)){
2566 $installed_models[] = $this->getModelName();
2567 require_once(AK_LIB_DIR
.DS
.'AkInstaller.php');
2568 $installer_name = $this->getModelName().'Installer';
2569 $installer_file = AK_APP_DIR
.DS
.'installers'.DS
.AkInflector
::underscore($installer_name).'.php';
2570 if(file_exists($installer_file)){
2571 require_once($installer_file);
2572 if(class_exists($installer_name)){
2573 $Installer = new $installer_name();
2574 if(method_exists($Installer,'install')){
2575 $Installer->install();
2576 $column_objects = $this->_databaseTableInternals($this->getTableName());
2577 return !empty($column_objects);
2587 * Returns an array of column objects for the table associated with this class.
2589 function getColumns($force_reload = false)
2591 if(empty($this->_columns
) ||
$force_reload){
2592 $this->_columns
= $this->getColumnSettings($force_reload);
2595 return (array)$this->_columns
;
2598 function getColumnSettings($force_reload = false)
2600 if(empty($this->_columnsSettings
) ||
$force_reload){
2601 $this->loadColumnsSettings($force_reload);
2602 $this->initiateColumnsToNull();
2604 return isset($this->_columnsSettings
) ?
$this->_columnsSettings
: array();
2607 function loadColumnsSettings($force_reload = false)
2609 if(is_null($this->_db
)){
2610 $this->setConnection();
2612 $this->_columnsSettings
= $force_reload ?
null : $this->_getPersistedTableColumnSettings();
2614 if(empty($this->_columnsSettings
) ||
!AK_ACTIVE_RECORD_ENABLE_PERSISTENCE
){
2615 if(empty($this->_dataDictionary
)){
2616 $this->_dataDictionary
=& $this->_db
->getDictionary();
2619 $column_objects = $this->_databaseTableInternals($this->getTableName());
2621 if( !isset($this->_avoidTableNameValidation
) &&
2622 !is_array($column_objects) &&
2623 !$this->_runCurrentModelInstallerIfExists($column_objects)){
2624 trigger_error(Ak
::t('Ooops! Could not fetch details for the table %table_name.', array('%table_name'=>$this->getTableName())), E_USER_ERROR
);
2626 }elseif (empty($column_objects)){
2627 $this->_runCurrentModelInstallerIfExists($column_objects);
2629 if(is_array($column_objects)){
2630 foreach (array_keys($column_objects) as $k){
2631 $this->setColumnSettings($column_objects[$k]->name
, $column_objects[$k]);
2634 if(!empty($this->_columnsSettings
)){
2635 $this->_persistTableColumnSettings();
2638 return isset($this->_columnsSettings
) ?
$this->_columnsSettings
: array();
2643 function setColumnSettings($column_name, $column_object)
2645 $this->_columnsSettings
[$column_name] = array();
2646 $this->_columnsSettings
[$column_name]['name'] = $column_object->name
;
2648 if($this->_internationalize
&& $this->_isInternationalizeCandidate($column_object->name
)){
2649 $this->_addInternationalizedColumn($column_object->name
);
2652 $this->_columnsSettings
[$column_name]['type'] = $this->getAkelosDataType($column_object);
2653 if(!empty($column_object->primary_key
)){
2654 $this->_primaryKey
= empty($this->_primaryKey
) ?
$column_object->name
: $this->_primaryKey
;
2655 $this->_columnsSettings
[$column_name]['primaryKey'] = true;
2657 if(!empty($column_object->auto_increment
)){
2658 $this->_columnsSettings
[$column_name]['autoIncrement'] = true;
2660 if(!empty($column_object->has_default
)){
2661 $this->_columnsSettings
[$column_name]['hasDefault'] = true;
2663 if(!empty($column_object->not_null
)){
2664 $this->_columnsSettings
[$column_name]['notNull'] = true;
2666 if(!empty($column_object->max_length
) && $column_object->max_length
> 0){
2667 $this->_columnsSettings
[$column_name]['maxLength'] = $column_object->max_length
;
2669 if(!empty($column_object->scale
) && $column_object->scale
> 0){
2670 $this->_columnsSettings
[$column_name]['scale'] = $column_object->scale
;
2672 if(isset($column_object->default_value
)){
2673 $this->_columnsSettings
[$column_name]['defaultValue'] = $column_object->default_value
;
2679 * Resets all the cached information about columns, which will cause they to be reloaded on the next request.
2681 function resetColumnInformation()
2683 if(isset($_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName()])){
2684 unset($_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName()]);
2686 $this->_clearPersitedColumnSettings();
2687 $this->_columnNames
= $this->_columns
= $this->_columnsSettings
= $this->_contentColumns
= array();
2693 function _getColumnsSettings()
2695 return $_SESSION['__activeRecordColumnsSettingsCache'];
2701 function _getModelColumnSettings()
2703 return $_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName()];
2709 function _persistTableColumnSettings()
2711 $_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName().'_column_settings'] = $this->_columnsSettings
;
2717 function _getPersistedTableColumnSettings()
2719 $model_name = $this->getModelName();
2720 if(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA
&& !isset($_SESSION['__activeRecordColumnsSettingsCache']) && AK_CACHE_HANDLER
> 0){
2721 $this->_loadPersistedColumnSetings();
2723 return isset($_SESSION['__activeRecordColumnsSettingsCache'][$model_name.'_column_settings']) ?
2724 $_SESSION['__activeRecordColumnsSettingsCache'][$model_name.'_column_settings'] : false;
2730 function _clearPersitedColumnSettings()
2732 if(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA
&& AK_CACHE_HANDLER
> 0){
2733 $Cache =& Ak
::cache();
2734 $Cache->init(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE
);
2735 $Cache->clean('AkActiveRecord');
2742 function _savePersitedColumnSettings()
2744 if(isset($_SESSION['__activeRecordColumnsSettingsCache'])){
2745 $Cache =& Ak
::cache();
2746 $Cache->init(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE
);
2747 $Cache->save(serialize($_SESSION['__activeRecordColumnsSettingsCache']), 'active_record_db_cache', 'AkActiveRecord');
2754 function _loadPersistedColumnSetings()
2756 if(!isset($_SESSION['__activeRecordColumnsSettingsCache'])){
2757 $Cache =& Ak
::cache();
2758 $Cache->init(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE
);
2759 if($serialized_column_settings = $Cache->get('active_record_db_cache', 'AkActiveRecord') && !empty($serialized_column_settings)){
2760 $_SESSION['__activeRecordColumnsSettingsCache'] = @unserialize
($serialized_column_settings);
2762 }elseif(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA
){
2763 register_shutdown_function(array($this,'_savePersitedColumnSettings'));
2766 $_SESSION['__activeRecordColumnsSettingsCache'] = array();
2771 function initiateAttributeToNull($attribute)
2773 if(!isset($this->$attribute)){
2774 $this->$attribute = null;
2778 function initiateColumnsToNull()
2780 if(isset($this->_columnsSettings
) && is_array($this->_columnsSettings
)){
2781 array_map(array(&$this,'initiateAttributeToNull'),array_keys($this->_columnsSettings
));
2787 * Akelos data types are mapped to phpAdodb data types
2789 * Returns the Akelos data type for an Adodb Column Object
2791 * 'C'=>'string', // Varchar, capped to 255 characters.
2792 * 'X' => 'text' // Larger varchar, capped to 4000 characters (to be compatible with Oracle).
2793 * 'XL' => 'text' // For Oracle, returns CLOB, otherwise the largest varchar size.
2795 * 'C2' => 'string', // Multibyte varchar
2796 * 'X2' => 'string', // Multibyte varchar (largest size)
2798 * 'B' => 'binary', // BLOB (binary large object)
2800 * 'D' => array('date', 'datetime'), // Date (some databases do not support this, and we return a datetime type)
2801 * 'T' => array('datetime', 'timestamp'), //Datetime or Timestamp
2802 * 'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1)
2803 * 'I' => // Integer (mapped to I4)
2804 * 'I1' => 'integer', // 1-byte integer
2805 * 'I2' => 'integer', // 2-byte integer
2806 * 'I4' => 'integer', // 4-byte integer
2807 * 'I8' => 'integer', // 8-byte integer
2808 * 'F' => 'float', // Floating point number
2809 * 'N' => 'integer' // Numeric or decimal number
2811 * @return string One of this 'string','text','integer','float','datetime','timestamp',
2812 * 'time', 'name','date', 'binary', 'boolean'
2814 function getAkelosDataType(&$adodb_column_object)
2816 $config_var_name = AkInflector
::variablize($adodb_column_object->name
.'_data_type');
2817 if(!empty($this->{$config_var_name})){
2818 return $this->{$config_var_name};
2820 if(stristr($adodb_column_object->type
, 'BLOB')){
2823 if(!empty($adodb_column_object->auto_increment
)) {
2826 $meta_type = $this->_dataDictionary
->MetaType($adodb_column_object);
2827 $adodb_data_types = array(
2828 'C'=>'string', // Varchar, capped to 255 characters.
2829 'X' => 'text', // Larger varchar, capped to 4000 characters (to be compatible with Oracle).
2830 'XL' => 'text', // For Oracle, returns CLOB, otherwise the largest varchar size.
2832 'C2' => 'string', // Multibyte varchar
2833 'X2' => 'string', // Multibyte varchar (largest size)
2835 'B' => 'binary', // BLOB (binary large object)
2837 'D' => array('date'), // Date
2838 'T' => array('datetime', 'timestamp'), //Datetime or Timestamp
2839 'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1)
2840 'R' => 'serial', // Serial Integer
2841 'I' => 'integer', // Integer (mapped to I4)
2842 'I1' => 'integer', // 1-byte integer
2843 'I2' => 'integer', // 2-byte integer
2844 'I4' => 'integer', // 4-byte integer
2845 'I8' => 'integer', // 8-byte integer
2846 'F' => 'float', // Floating point number
2847 'N' => 'decimal' // Numeric or decimal number
2850 $result = !isset($adodb_data_types[$meta_type]) ?
2852 (is_array($adodb_data_types[$meta_type]) ?
$adodb_data_types[$meta_type][0] : $adodb_data_types[$meta_type]);
2854 if($result == 'text'){
2855 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 )){
2860 if($this->_getDatabaseType() == 'mysql'){
2861 if($result == 'integer' && stristr($adodb_column_object->type
, 'TINYINT')){
2864 }elseif($this->_getDatabaseType() == 'postgre'){
2865 if($adodb_column_object->type
== 'timestamp' ||
$result == 'datetime'){
2866 $adodb_column_object->max_length
= 19;
2868 }elseif($this->_getDatabaseType() == 'sqlite'){
2869 if($result == 'integer' && (int)$adodb_column_object->max_length
=== 1 && stristr($adodb_column_object->type
, 'TINYINT')){
2871 }elseif($result == 'integer' && stristr($adodb_column_object->type
, 'DOUBLE')){
2876 if($result == 'datetime' && substr($adodb_column_object->name
,-3) == '_on'){
2885 * This method retrieves current class name that will be used to map
2886 * your database to this object.
2888 function getClassForDatabaseTableMapping()
2890 $class_name = get_class($this);
2891 if(is_subclass_of($this,'akactiverecord') ||
is_subclass_of($this,'AkActiveRecord')){
2892 $parent_class = get_parent_class($this);
2893 while (substr(strtolower($parent_class),-12) != 'activerecord'){
2894 $class_name = $parent_class;
2895 $parent_class = get_parent_class($parent_class);
2899 $class_name = $this->_getModelName($class_name);
2900 // This is an Active Record Inheritance so we set current table to parent table.
2901 if(!empty($class_name) && strtolower($class_name) != 'activerecord'){
2902 $this->_inheritanceClassName
= $class_name;
2903 @$this->setTableName(AkInflector
::tableize($class_name), false);
2909 function getDisplayField()
2911 return empty($this->displayField
) && $this->hasAttribute('name') ?
'name' : (isset($this->displayField
) && $this->hasAttribute($this->displayField
) ?
$this->displayField
: $this->getPrimaryKey());
2914 function setDisplayField($attribute_name)
2916 if($this->hasAttribute($attribute_name)){
2917 $this->displayField
= $attribute_name;
2927 /*/Database Reflection*/
2931 ====================================================================
2934 function t($string, $array = null)
2936 return Ak
::t($string, $array, AkInflector
::underscore($this->getModelName()));
2939 function getInternationalizedColumns()
2942 $model = $this->getModelName();
2943 $available_locales = $this->getAvailableLocales();
2944 if(empty($cache[$model])){
2945 $cache[$model] = array();
2946 foreach ($this->getColumnSettings() as $column_name=>$details){
2947 if(!empty($details['i18n'])){
2948 $_tmp_pos = strpos($column_name,'_');
2949 $column = substr($column_name,$_tmp_pos+
1);
2950 $lang = substr($column_name,0,$_tmp_pos);
2951 if(in_array($lang, $available_locales)){
2952 $cache[$model][$column] = empty($cache[$model][$column]) ?
array($lang) :
2953 array_merge($cache[$model][$column] ,array($lang));
2959 return $cache[$model];
2962 function getAvailableLocales()
2964 static $available_locales;
2965 if(empty($available_locales)){
2966 if(defined('AK_ACTIVE_RECORD_DEFAULT_LOCALES')){
2967 $available_locales = Ak
::stringToArray(AK_ACTIVE_RECORD_DEFAULT_LOCALES
);
2969 $available_locales = Ak
::langs();
2972 return $available_locales;
2975 function getCurrentLocale()
2977 static $current_locale;
2978 if(empty($current_locale)){
2979 $current_locale = Ak
::lang();
2980 $available_locales = $this->getAvailableLocales();
2981 if(!in_array($current_locale, $available_locales)){
2982 $current_locale = array_shift($available_locales);
2985 return $current_locale;
2989 function getAttributeByLocale($attribute, $locale)
2991 $internationalizable_columns = $this->getInternationalizedColumns();
2992 if(!empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){
2993 return $this->getAttribute($locale.'_'.$attribute);
2997 function getAttributeLocales($attribute)
2999 $attribute_locales = array();
3000 foreach ($this->getAvailableLocales() as $locale){
3001 if($this->hasColumn($locale.'_'.$attribute)){
3002 $attribute_locales[$locale] = $this->getAttributeByLocale($attribute, $locale);
3005 return $attribute_locales;
3008 function setAttributeByLocale($attribute, $value, $locale)
3010 $internationalizable_columns = $this->getInternationalizedColumns();
3012 if($this->_isInternationalizeCandidate($locale.'_'.$attribute) && !empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){
3013 $this->setAttribute($locale.'_'.$attribute, $value);
3018 function setAttributeLocales($attribute, $values = array())
3020 foreach ($values as $locale=>$value){
3021 $this->setAttributeByLocale($attribute, $value, $locale);
3028 function _delocalizeAttribute($attribute)
3030 return $this->_isInternationalizeCandidate($attribute) ?
substr($attribute,3) : $attribute;
3036 function _isInternationalizeCandidate($column_name)
3038 $pos = strpos($column_name,'_');
3039 return $pos === 2 && in_array(substr($column_name,0,$pos),$this->getAvailableLocales());
3045 function _addInternationalizedColumn($column_name)
3047 $this->_columnsSettings
[$column_name]['i18n'] = true;
3052 * Adds an internationalized attribute to an array containing other locales for the same column name
3055 * es_title and en_title will be available user title = array('es'=>'...', 'en' => '...')
3059 function _groupInternationalizedAttribute($attribute, $value)
3061 if($this->_internationalize
&& $this->_isInternationalizeCandidate($attribute)){
3062 if(!empty($this->$attribute)){
3063 $_tmp_pos = strpos($attribute,'_');
3064 $column = substr($attribute,$_tmp_pos+
1);
3065 $lang = substr($attribute,0,$_tmp_pos);
3066 $this->$column = empty($this->$column) ?
array() : $this->$column;
3067 if(empty($this->$column) ||
(!empty($this->$column) && is_array($this->$column))){
3068 $this->$column = empty($this->$column) ?
array($lang=>$value) : array_merge($this->$column,array($lang=>$value));
3081 ====================================================================
3082 See also: Database Reflection.
3085 function getAttributesBeforeTypeCast()
3087 $attributes_array = array();
3088 $available_attributes = $this->getAvailableAttributes();
3089 foreach ($available_attributes as $attribute){
3090 $attribute_value = $this->getAttributeBeforeTypeCast($attribute['name']);
3091 if(!empty($attribute_value)){
3092 $attributes_array[$attribute['name']] = $attribute_value;
3095 return $attributes_array;
3099 function getAttributeBeforeTypeCast($attribute)
3101 if(isset($this->{$attribute.'_before_type_cast'})){
3102 return $this->{$attribute.'_before_type_cast'};
3109 return $this->castAttributeForDatabase($this->getPrimaryKey(), $this->getId());
3113 * 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.
3115 function setSerializeAttribute($attr_name, $class_name = null)
3117 if($this->hasColumn($attr_name)){
3118 $this->_serializedAttributes
[$attr_name] = $class_name;
3122 function getAvailableAttributesQuoted()
3124 return $this->getAttributesQuoted($this->getAttributes());
3128 function getAttributesQuoted($attributes_array)
3131 $attributes_array = $this->getSanitizedConditionsArray($attributes_array);
3132 foreach (array_diff($attributes_array,array('')) as $k=>$v){
3133 $set[$k] = $k.'='.$v;
3139 function getColumnType($column_name)
3141 empty($this->_columns
) ?
$this->getColumns() : null;
3142 return empty($this->_columns
[$column_name]['type']) ?
false : $this->_columns
[$column_name]['type'];
3145 function getColumnScale($column_name)
3147 empty($this->_columns
) ?
$this->getColumns() : null;
3148 return empty($this->_columns
[$column_name]['scale']) ?
false : $this->_columns
[$column_name]['scale'];
3151 function castAttributeForDatabase($column_name, $value, $add_quotes = true)
3154 switch ($this->getColumnType($column_name)) {
3157 $date_time = $this->_db
->quote_datetime(Ak
::getTimestamp($value));
3158 $result = $add_quotes ?
$date_time : trim($date_time ,"'");
3166 $date = $this->_db
->quote_date(Ak
::getTimestamp($value));
3167 $result = $add_quotes ?
$date : trim($date, "'");
3174 $result = is_null($value) ?
'null' : (!empty($value) ?
"'1'" : "'0'");
3178 if($this->_getDatabaseType() == 'postgre'){
3179 $result = is_null($value) ?
'null::bytea ' : " '".$this->_db
->escape_blob($value)."'::bytea ";
3181 $result = is_null($value) ?
'null' : ($add_quotes ?
$this->_db
->quote_string($value) : $value);
3186 if(is_null($value)){
3189 if($scale = $this->getColumnScale($column_name)){
3190 $value = number_format($value, $scale);
3192 $result = $add_quotes ?
$this->_db
->quote_string($value) : $value;
3198 $result = (is_null($value) ||
$value==='') ?
'null' : (integer)$value;
3202 $result = (empty($value) && $value !== 0) ?
'null' : (is_numeric($value) ?
$value : $this->_db
->quote_string($value));
3203 $result = !empty($this->_columns
[$column_name]['notNull']) && $result == 'null' && $this->_getDatabaseType() == 'sqlite' ?
'0' : $result;
3207 $result = is_null($value) ?
'null' : ($add_quotes ?
$this->_db
->quote_string($value) : $value);
3211 // !! nullable vs. not nullable !!
3212 return empty($this->_columns
[$column_name]['notNull']) ?
($result === '' ?
"''" : $result) : ($result === 'null' ?
'' : $result);
3215 function castAttributeFromDatabase($column_name, $value)
3217 if($this->hasColumn($column_name)){
3218 $column_type = $this->getColumnType($column_name);
3221 if('integer' == $column_type){
3222 return is_null($value) ?
null : (integer)$value;
3223 //return is_null($value) ? null : $value; // maybe for bigint we can do this
3224 }elseif('boolean' == $column_type){
3225 if (is_null($value)) {
3228 if ($this->_getDatabaseType()=='postgre'){
3229 return $value=='t' ?
true : false;
3231 return (integer)$value === 1 ?
true : false;
3232 }elseif(!empty($value) && 'date' == $column_type && strstr(trim($value),' ')){
3233 return substr($value,0,10) == '0000-00-00' ?
null : str_replace(substr($value,strpos($value,' ')), '', $value);
3234 }elseif (!empty($value) && 'datetime' == $column_type && substr($value,0,10) == '0000-00-00'){
3236 }elseif ('binary' == $column_type && $this->_getDatabaseType() == 'postgre'){
3237 $value = $this->_db
->unescape_blob($value);
3238 $value = empty($value) ||
trim($value) == 'null' ?
null : $value;
3247 * Joins date arguments into a single attribute. Like the array generated by the date_helper, so
3248 * array('published_on(1i)' => 2002, 'published_on(2i)' => 'January', 'published_on(3i)' => 24)
3249 * Will be converted to array('published_on'=>'2002-01-24')
3253 function _castDateParametersFromDateHelper_(&$params)
3258 $date_attributes = array();
3259 foreach ($params as $k=>$v) {
3260 if(preg_match('/^([A-Za-z0-9_]+)\(([1-5]{1})i\)$/',$k,$match)){
3261 $date_attributes[$match[1]][$match[2]] = $v;
3266 foreach ($date_attributes as $attribute=>$date){
3267 $params[$attribute] = trim(@$date[1].'-'.@$date[2].'-'.@$date[3].' '.@$date[4].':'.@$date[5].':'.@$date[6],' :-');
3274 function _addBlobQueryStack($column_name, $blob_value)
3276 $this->_BlobQueryStack
[$column_name] = $blob_value;
3282 function _updateBlobFields($condition)
3284 if(!empty($this->_BlobQueryStack
) && is_array($this->_BlobQueryStack
)){
3285 foreach ($this->_BlobQueryStack
as $column=>$value){
3286 $this->_db
->UpdateBlob($this->getTableName(), $column, $value, $condition);
3288 $this->_BlobQueryStack
= null;
3296 ====================================================================
3298 * Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
3299 * record increments the lock_version column and the locking facilities ensure that records instantiated twice
3300 * will let the last one saved return false on save() if the first was also updated. Example:
3302 * $p1 = new Person(1);
3303 * $p2 = new Person(1);
3305 * $p1->first_name = "Michael";
3308 * $p2->first_name = "should fail";
3309 * $p2->save(); // Returns false
3311 * You're then responsible for dealing with the conflict by checking the return value of save(); and either rolling back, merging,
3312 * or otherwise apply the business logic needed to resolve the conflict.
3314 * You must ensure that your database schema defaults the lock_version column to 0.
3316 * This behavior can be turned off by setting <tt>AkActiveRecord::lock_optimistically = false</tt>.
3318 function isLockingEnabled()
3320 return (!isset($this->lock_optimistically
) ||
$this->lock_optimistically
!== false) && $this->hasColumn('lock_version');
3322 /*/Optimistic Locking*/
3327 ====================================================================
3328 See also: Observers.
3330 * Callbacks are hooks into the life-cycle of an Active Record object that allows you to trigger logic
3331 * before or after an alteration of the object state. This can be used to make sure that associated and
3332 * dependent objects are deleted when destroy is called (by overwriting beforeDestroy) or to massage attributes
3333 * before they're validated (by overwriting beforeValidation). As an example of the callbacks initiated, consider
3334 * the AkActiveRecord->save() call:
3337 * - (-) needsValidation()
3338 * - (1) beforeValidation()
3339 * - (2) beforeValidationOnCreate() / beforeValidationOnUpdate()
3341 * - (-) validateOnCreate()
3342 * - (4) afterValidation()
3343 * - (5) afterValidationOnCreate() / afterValidationOnUpdate()
3344 * - (6) beforeSave()
3345 * - (7) beforeCreate() / beforeUpdate()
3347 * - (8) afterCreate() / afterUpdate()
3349 * - (10) afterDestroy()
3350 * - (11) beforeDestroy()
3353 * That's a total of 15 callbacks, which gives you immense power to react and prepare for each state in the
3354 * Active Record lifecycle.
3357 * class CreditCard extends ActiveRecord
3359 * // Strip everything but digits, so the user can specify "555 234 34" or
3360 * // "5552-3434" or both will mean "55523434"
3361 * function beforeValidationOnCreate
3363 * if(!empty($this->number)){
3364 * $this->number = ereg_replace('[^0-9]*','',$this->number);
3369 * class Subscription extends ActiveRecord
3371 * // Note: This is not implemented yet
3372 * var $beforeCreate = 'recordSignup';
3374 * function recordSignup()
3376 * $this->signed_up_on = date("Y-m-d");
3380 * class Firm extends ActiveRecord
3382 * //Destroys the associated clients and people when the firm is destroyed
3383 * // Note: This is not implemented yet
3384 * var $beforeDestroy = array('destroyAssociatedPeople', 'destroyAssociatedClients');
3386 * function destroyAssociatedPeople()
3388 * $Person = new Person();
3389 * $Person->destroyAll("firm_id=>", $this->id);
3392 * function destroyAssociatedClients()
3394 * $Client = new Client();
3395 * $Client->destroyAll("client_of=>", $this->id);
3400 * == Canceling callbacks ==
3402 * If a before* callback returns false, all the later callbacks and the associated action are cancelled. If an after* callback returns
3403 * false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
3404 * defined as methods on the model, which are called last.
3406 * Override this methods to hook Active Records
3411 function beforeCreate(){return true;}
3412 function beforeValidation(){return true;}
3413 function beforeValidationOnCreate(){return true;}
3414 function beforeValidationOnUpdate(){return true;}
3415 function beforeSave(){return true;}
3416 function beforeUpdate(){return true;}
3417 function afterUpdate(){return true;}
3418 function afterValidation(){return true;}
3419 function afterValidationOnCreate(){return true;}
3420 function afterValidationOnUpdate(){return true;}
3421 function afterCreate(){return true;}
3422 function afterDestroy(){return true;}
3423 function beforeDestroy(){return true;}
3424 function afterSave(){return true;}
3431 ====================================================================
3433 * Transaction support for database operations
3435 * Transactions are enabled automatically for Active record objects, But you can nest transactions within models.
3436 * This transactions are nested, and only the outermost will be executed
3438 * $User->transactionStart();
3439 * $User->create('username'=>'Bermi');
3440 * $Members->create('username'=>'Bermi');
3442 * if(!checkSomething()){
3443 * $User->transactionFail();
3446 * $User->transactionComplete();
3449 function transactionStart()
3451 return $this->_db
->startTransaction();
3454 function transactionComplete()
3456 return $this->_db
->stopTransaction();
3459 function transactionFail()
3461 $this->_db
->failTransaction();
3465 function transactionHasFailed()
3467 return $this->_db
->hasTransactionFailed();
3477 ====================================================================
3478 See also: Error Handling.
3480 * Active Records implement validation by overwriting AkActiveRecord::validate (or the variations, validateOnCreate and
3481 * validateOnUpdate). Each of these methods can inspect the state of the object, which usually means ensuring
3482 * that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
3486 * class Person extends ActiveRecord
3488 * function validate()
3490 * $this->addErrorOnEmpty(array('first_name', 'last_name'));
3491 * if(!preg_match('/[0-9]{4,12}/', $this->phone_number)){
3492 * $this->addError("phone_number", "has invalid format");
3496 * function validateOnCreate() // is only run the first time a new object is saved
3498 * if(!isValidDiscount($this->membership_discount)){
3499 * $this->addError("membership_discount", "has expired");
3503 * function validateOnUpdate()
3505 * if($this->countChangedAttributes() == 0){
3506 * $this->addErrorToBase("No changes have occurred");
3511 * $Person = new Person(array("first_name" => "David", "phone_number" => "what?"));
3512 * $Person->save(); // => false (and doesn't do the save);
3513 * $Person->hasErrors(); // => false
3514 * $Person->countErrors(); // => 2
3515 * $Person->getErrorsOn("last_name"); // => "can't be empty"
3516 * $Person->getErrorsOn("phone_number"); // => "has invalid format"
3517 * $Person->yieldEachFullError(); // => "Last name can't be empty \n Phone number has invalid format"
3519 * $Person->setAttributes(array("last_name" => "Heinemeier", "phone_number" => "555-555"));
3520 * $Person->save(); // => true (and person is now saved in the database)
3522 * An "_errors" array is available for every Active Record.
3527 * Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
3530 * class Person extends ActiveRecord
3532 * function validate()
3534 * $this->validatesConfirmationOf('password');
3535 * $this->validatesConfirmationOf('email_address', "should match confirmation");
3540 * <?=$form_helper->password_field("person", "password"); ?>
3541 * <?=$form_helper->password_field("person", "password_confirmation"); ?>
3543 * The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual.
3544 * It exists only as an in-memory variable for validating the password. This check is performed only if password_confirmation
3548 function validatesConfirmationOf($attribute_names, $message = 'confirmation')
3550 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3551 $attribute_names = Ak
::toArray($attribute_names);
3552 foreach ($attribute_names as $attribute_name){
3553 $attribute_accessor = $attribute_name.'_confirmation';
3554 if(isset($this->$attribute_accessor) && @$this->$attribute_accessor != @$this->$attribute_name){
3555 $this->addError($attribute_name, $message);
3561 * Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
3563 * class Person extends ActiveRecord
3565 * function validateOnCreate()
3567 * $this->validatesAcceptanceOf('terms_of_service');
3568 * $this->validatesAcceptanceOf('eula', "must be abided");
3572 * The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed only if
3573 * terms_of_service is not null.
3577 * Specifies value that is considered accepted. The default value is a string "1", which makes it easy to relate to an HTML checkbox.
3579 function validatesAcceptanceOf($attribute_names, $message = 'accepted', $accept = 1)
3581 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3583 $attribute_names = Ak
::toArray($attribute_names);
3584 foreach ($attribute_names as $attribute_name){
3585 if(@$this->$attribute_name != $accept){
3586 $this->addError($attribute_name, $message);
3592 * Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
3594 * class Book extends ActiveRecord
3596 * var $has_many = 'pages';
3597 * var $belongs_to = 'library';
3599 * function validate(){
3600 * $this->validatesAssociated(array('pages', 'library'));
3605 * Warning: If, after the above definition, you then wrote:
3607 * class Page extends ActiveRecord
3609 * var $belongs_to = 'book';
3610 * function validate(){
3611 * $this->validatesAssociated('book');
3615 * ...this would specify a circular dependency and cause infinite recursion.
3617 * NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
3618 * is both present and guaranteed to be valid, you also need to use validatesPresenceOf.
3620 function validatesAssociated($attribute_names, $message = 'invalid')
3622 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3623 $attribute_names = Ak
::toArray($attribute_names);
3624 foreach ($attribute_names as $attribute_name){
3625 if(!empty($this->$attribute_name)){
3626 if(is_array($this->$attribute_name)){
3627 foreach(array_keys($this->$attribute_name) as $k){
3628 if(method_exists($this->{$attribute_name}[$k],'isValid') && !$this->{$attribute_name}[$k]->isValid()){
3629 $this->addError($attribute_name, $message);
3632 }elseif (method_exists($this->$attribute_name,'isValid') && !$this->$attribute_name->isValid()){
3633 $this->addError($attribute_name, $message);
3639 function isBlank($value = null)
3641 return trim((string)$value) == '';
3645 * Validates that the specified attributes are not blank (as defined by AkActiveRecord::isBlank()).
3647 function validatesPresenceOf($attribute_names, $message = 'blank')
3649 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3651 $attribute_names = Ak
::toArray($attribute_names);
3652 foreach ($attribute_names as $attribute_name){
3653 $this->addErrorOnBlank($attribute_name, $message);
3658 * Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
3660 * class Person extends ActiveRecord
3662 * function validate()
3664 * $this->validatesLengthOf('first_name', array('maximum'=>30));
3665 * $this->validatesLengthOf('last_name', array('maximum'=>30,'message'=> "less than %d if you don't mind"));
3666 * $this->validatesLengthOf('last_name', array('within'=>array(7, 32)));
3667 * $this->validatesLengthOf('last_name', array('in'=>array(6, 20), 'too_long' => "pick a shorter name", 'too_short' => "pick a longer name"));
3668 * $this->validatesLengthOf('fav_bra_size', array('minimum'=>1, 'too_short'=>"please enter at least %d character"));
3669 * $this->validatesLengthOf('smurf_leader', array('is'=>4, 'message'=>"papa is spelled with %d characters... don't play me."));
3673 * 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.
3675 * Configuration options:
3676 * <tt>minimum</tt> - The minimum size of the attribute
3677 * <tt>maximum</tt> - The maximum size of the attribute
3678 * <tt>is</tt> - The exact size of the attribute
3679 * <tt>within</tt> - A range specifying the minimum and maximum size of the attribute
3680 * <tt>in</tt> - A synonym(or alias) for :within
3681 * <tt>allow_null</tt> - Attribute may be null; skip validation.
3683 * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default "is" "is too long (max is %d characters)")
3684 * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default "is" "is too short (min is %d characters)")
3685 * <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)")
3686 * <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
3688 function validatesLengthOf($attribute_names, $options = array())
3690 // Merge given options with defaults.
3691 $default_options = array(
3692 'too_long' => $this->_defaultErrorMessages
['too_long'],
3693 'too_short' => $this->_defaultErrorMessages
['too_short'],
3694 'wrong_length' => $this->_defaultErrorMessages
['wrong_length'],
3695 'allow_null' => false
3698 $range_options = array();
3699 foreach ($options as $k=>$v){
3700 if(in_array($k,array('minimum','maximum','is','in','within'))){
3701 $range_options[$k] = $v;
3707 // Ensure that one and only one range option is specified.
3708 switch (count($range_options)) {
3710 trigger_error(Ak
::t('Range unspecified. Specify the "within", "maximum", "minimum, or "is" option.'), E_USER_ERROR
);
3714 $options = array_merge($default_options, $options);
3717 trigger_error(Ak
::t('Too many range options specified. Choose only one.'), E_USER_ERROR
);
3726 if(empty($option_value) ||
!is_array($option_value) ||
count($option_value) != 2 ||
!is_numeric($option_value[0]) ||
!is_numeric($option_value[1])){
3727 trigger_error(Ak
::t('%option must be a Range (array(min, max))',array('%option',$option)), E_USER_ERROR
);
3730 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
3731 foreach ($attribute_names as $attribute_name){
3732 if((!empty($option['allow_null']) && !isset($this->$attribute_name)) ||
(Ak
::size($this->$attribute_name)) < $option_value[0]){
3733 $this->addError($attribute_name, sprintf($options['too_short'], $option_value[0]));
3734 }elseif((!empty($option['allow_null']) && !isset($this->$attribute_name)) ||
(Ak
::size($this->$attribute_name)) > $option_value[1]){
3735 $this->addError($attribute_name, sprintf($options['too_long'], $option_value[1]));
3744 if(empty($option_value) ||
!is_numeric($option_value) ||
$option_value <= 0){
3745 trigger_error(Ak
::t('%option must be a nonnegative Integer',array('%option',$option_value)), E_USER_ERROR
);
3749 // Declare different validations per option.
3750 $validity_checks = array('is' => "==", 'minimum' => ">=", 'maximum' => "<=");
3751 $message_options = array('is' => 'wrong_length', 'minimum' => 'too_short', 'maximum' => 'too_long');
3753 $message = sprintf(!empty($options['message']) ?
$options['message'] : $options[$message_options[$option]],$option_value);
3755 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
3756 foreach ($attribute_names as $attribute_name){
3757 if((!$options['allow_null'] && !isset($this->$attribute_name)) ||
3758 eval("return !(".Ak
::size(@$this->$attribute_name)." {$validity_checks[$option]} $option_value);")){
3759 $this->addError($attribute_name, $message);
3770 function validatesSizeOf($attribute_names, $options = array())
3772 return validatesLengthOf($attribute_names, $options);
3776 * Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
3777 * can be named "davidhh".
3779 * class Person extends ActiveRecord
3781 * function validate()
3783 * $this->validatesUniquenessOf('passport_number');
3784 * $this->validatesUniquenessOf('user_name', array('scope' => "account_id"));
3788 * It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
3789 * making sure that a teacher can only be on the schedule once per semester for a particular class.
3791 * class TeacherSchedule extends ActiveRecord
3793 * function validate()
3795 * $this->validatesUniquenessOf('passport_number');
3796 * $this->validatesUniquenessOf('teacher_id', array('scope' => array("semester_id", "class_id"));
3801 * 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
3802 * attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
3804 * Configuration options:
3805 * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
3806 * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope"
3807 * <tt>case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (true by default).
3808 * <tt>if</tt> - Specifies a method to call or a string to evaluate to determine if the validation should
3809 * occur (e.g. 'if' => 'allowValidation', or 'if' => '$this->signup_step > 2'). The
3810 * method, or string should return or evaluate to a true or false value.
3812 function validatesUniquenessOf($attribute_names, $options = array())
3814 $default_options = array('case_sensitive'=>true, 'message'=>'taken');
3815 $options = array_merge($default_options, $options);
3817 if(!empty($options['if'])){
3818 if(method_exists($this,$options['if'])){
3819 if($this->{$options['if']}() === false){
3823 eval('$__eval_result = ('.rtrim($options['if'],';').');');
3824 if(empty($__eval_result)){
3830 $message = isset($this->_defaultErrorMessages
[$options['message']]) ?
$this->t($this->_defaultErrorMessages
[$options['message']]) : $options['message'];
3831 unset($options['message']);
3833 foreach ((array)$attribute_names as $attribute_name){
3834 $value = isset($this->$attribute_name) ?
$this->$attribute_name : null;
3836 if($value === null ||
($options['case_sensitive'] ||
!$this->hasColumn($attribute_name))){
3837 $condition_sql = $this->getTableName().'.'.$attribute_name.' '.$this->getAttributeCondition($value);
3838 $condition_params = array($value);
3840 $condition_sql = 'LOWER('.$this->getTableName().'.'.$attribute_name.') '.$this->getAttributeCondition($value);
3841 $condition_params = array(is_array($value) ?
array_map('utf8_strtolower',$value) : utf8_strtolower($value));
3844 if(!empty($options['scope'])){
3845 foreach ((array)$options['scope'] as $scope_item){
3846 $scope_value = $this->get($scope_item);
3847 $condition_sql .= ' AND '.$this->getTableName().'.'.$scope_item.' '.$this->getAttributeCondition($scope_value);
3848 $condition_params[] = $scope_value;
3852 if(!$this->isNewRecord()){
3853 $condition_sql .= ' AND '.$this->getTableName().'.'.$this->getPrimaryKey().' <> ?';
3854 $condition_params[] = $this->getId();
3856 array_unshift($condition_params,$condition_sql);
3857 if ($this->find('first', array('conditions' => $condition_params))){
3858 $this->addError($attribute_name, $message);
3866 * Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
3870 * class Person extends ActiveRecord
3872 * function validate()
3874 * $this->validatesFormatOf('email', "/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/");
3879 * A regular expression must be provided or else an exception will be raised.
3881 * There are some regular expressions bundled with the Akelos Framework.
3882 * 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.
3883 * This are predefined perl-like regular extensions.
3885 * * AK_NOT_EMPTY_REGULAR_EXPRESSION ---> /.+/
3886 * * 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
3887 * * AK_NUMBER_REGULAR_EXPRESSION ---> /^[0-9]+$/
3888 * * AK_PHONE_REGULAR_EXPRESSION ---> /^([\+]?[(]?[\+]?[ ]?[0-9]{2,3}[)]?[ ]?)?[0-9 ()\-]{4,25}$/
3889 * * 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}$/
3890 * * 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])$/
3891 * * AK_POST_CODE_REGULAR_EXPRESSION ---> /^[0-9A-Za-z -]{2,7}$/
3893 * 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.
3896 * <tt>$message</tt> - A custom error message (default is: "is invalid")
3897 * <tt>$regular_expression</tt> - The regular expression used to validate the format with (note: must be supplied!)
3899 function validatesFormatOf($attribute_names, $regular_expression, $message = 'invalid', $regex_function = 'preg_match')
3901 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3903 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
3904 foreach ($attribute_names as $attribute_name){
3905 if(!isset($this->$attribute_name) ||
!$regex_function($regular_expression, $this->$attribute_name)){
3906 $this->addError($attribute_name, $message);
3912 * Validates whether the value of the specified attribute is available in a particular array of elements.
3914 * class Person extends ActiveRecord
3916 * function validate()
3918 * $this->validatesInclusionOf('gender', array('male', 'female'), "woah! what are you then!??!!");
3919 * $this->validatesInclusionOf('age', range(0, 99));
3923 * <tt>$array_of_ possibilities</tt> - An array of available items
3924 * <tt>$message</tt> - Specifies a customer error message (default is: "is not included in the list")
3925 * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false)
3927 function validatesInclusionOf($attribute_names, $array_of_possibilities, $message = 'inclusion', $allow_null = false)
3929 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3931 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
3932 foreach ($attribute_names as $attribute_name){
3933 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 )){
3934 $this->addError($attribute_name, $message);
3940 * Validates that the value of the specified attribute is not in a particular array of elements.
3942 * class Person extends ActiveRecord
3944 * function validate()
3946 * $this->validatesExclusionOf('username', array('admin', 'superuser'), "You don't belong here");
3947 * $this->validatesExclusionOf('age', range(30,60), "This site is only for under 30 and over 60");
3952 * <tt>$array_of_possibilities</tt> - An array of items that the value shouldn't be part of
3953 * <tt>$message</tt> - Specifies a customer error message (default is: "is reserved")
3954 * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false)
3956 function validatesExclusionOf($attribute_names, $array_of_possibilities, $message = 'exclusion', $allow_null = false)
3958 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3960 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
3961 foreach ($attribute_names as $attribute_name){
3963 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 )){
3964 $this->addError($attribute_name, $message);
3973 * Validates whether the value of the specified attribute is numeric.
3975 * class Person extends ActiveRecord
3977 * function validate()
3979 * $this->validatesNumericalityOf('value');
3984 * <tt>$message</tt> - A custom error message (default is: "is not a number")
3985 * <tt>$only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
3986 * <tt>$allow_null</tt> Skip validation if attribute is null (default is false).
3988 function validatesNumericalityOf($attribute_names, $message = 'not_a_number', $only_integer = false, $allow_null = false)
3990 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3992 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
3993 foreach ($attribute_names as $attribute_name){
3997 isset($this->$attribute_name) ?
4000 !is_integer(@$this->$attribute_name) :
4001 !is_numeric(@$this->$attribute_name)
4006 isset($this->$attribute_name) ?
4007 ($only_integer ?
!is_integer(@$this->$attribute_name) : !is_numeric(@$this->$attribute_name)) :
4012 $this->addError($attribute_name, $message);
4020 * Returns true if no errors were added otherwise false.
4024 $this->clearErrors();
4025 if($this->beforeValidation() && $this->notifyObservers('beforeValidation')){
4028 if($this->_set_default_attribute_values_automatically
){
4029 //$this->_setDefaultAttributeValuesAutomatically();
4034 if($this->_automated_validators_enabled
){
4035 //$this->_runAutomatedValidators();
4038 $this->afterValidation();
4039 $this->notifyObservers('afterValidation');
4041 if ($this->isNewRecord()){
4042 if($this->beforeValidationOnCreate()){
4043 $this->notifyObservers('beforeValidationOnCreate');
4044 $this->validateOnCreate();
4045 $this->afterValidationOnCreate();
4046 $this->notifyObservers('afterValidationOnCreate');
4049 if($this->beforeValidationOnUpdate()){
4050 $this->notifyObservers('beforeValidationOnUpdate');
4051 $this->validateOnUpdate();
4052 $this->afterValidationOnUpdate();
4053 $this->notifyObservers('afterValidationOnUpdate');
4058 return !$this->hasErrors();
4062 * By default the Active Record will validate for the maximum length for database columns. You can
4063 * disable the automated validators by setting $this->_automated_validators_enabled to false.
4064 * Specific validators are (for now):
4065 * $this->_automated_max_length_validator = true; // true by default, but you can set it to false on your model
4066 * $this->_automated_not_null_validator = false; // disabled by default
4070 function _runAutomatedValidators()
4072 foreach ($this->_columns
as $column_name=>$column_settings){
4073 if($this->_automated_max_length_validator
&&
4074 empty($column_settings['primaryKey']) &&
4075 !empty($this->$column_name) &&
4076 !empty($column_settings['maxLength']) && $column_settings['maxLength'] > 0 &&
4077 strlen($this->$column_name) > $column_settings['maxLength']){
4078 $this->addError($column_name, sprintf($this->_defaultErrorMessages
['too_long'], $column_settings['maxLength']));
4079 }elseif($this->_automated_not_null_validator
&& empty($column_settings['primaryKey']) && !empty($column_settings['notNull']) && (!isset($this->$column_name) ||
is_null($this->$column_name))){
4080 $this->addError($column_name,'empty');
4086 * $this->_set_default_attribute_values_automatically = true; // This enables automated attribute setting from database definition
4090 function _setDefaultAttributeValuesAutomatically()
4092 foreach ($this->_columns
as $column_name=>$column_settings){
4093 if(empty($column_settings['primaryKey']) && isset($column_settings['hasDefault']) && $column_settings['hasDefault'] && (!isset($this->$column_name) ||
is_null($this->$column_name))){
4094 if(empty($column_settings['defaultValue'])){
4095 if($column_settings['type'] == 'integer' && empty($column_settings['notNull'])){
4096 $this->$column_name = 0;
4097 }elseif(($column_settings['type'] == 'string' ||
$column_settings['type'] == 'text') && empty($column_settings['notNull'])){
4098 $this->$column_name = '';
4101 $this->$column_name = $column_settings['defaultValue'];
4108 * Overwrite this method for validation checks on all saves and use addError($field, $message); for invalid attributes.
4115 * Overwrite this method for validation checks used only on creation.
4117 function validateOnCreate()
4122 * Overwrite this method for validation checks used only on updates.
4124 function validateOnUpdate()
4133 ====================================================================
4134 See also: Callbacks.
4138 * $state store the state of this observable object
4142 var $_observable_state;
4147 function _instantiateDefaultObserver()
4149 $default_observer_name = ucfirst($this->getModelName().'Observer');
4150 if(class_exists($default_observer_name)){
4151 //$Observer =& new $default_observer_name($this);
4152 Ak
::singleton($default_observer_name, $this);
4157 * Calls the $method using the reference to each
4158 * registered observer.
4159 * @return true (this is used internally for triggering observers on default callbacks)
4161 function notifyObservers ($method = null)
4163 $observers =& $this->getObservers();
4164 $observer_count = count($observers);
4166 if(!empty($method)){
4167 $this->setObservableState($method);
4170 $model_name = $this->getModelName();
4171 for ($i=0; $i<$observer_count; $i++
) {
4172 if(in_array($model_name, $observers[$i]->_observing
)){
4173 if(method_exists($observers[$i], $method)){
4174 $observers[$i]->$method($this);
4176 $observers[$i]->update($this->getObservableState(), &$this);
4179 $observers[$i]->update($this->getObservableState(), &$this);
4182 $this->setObservableState('');
4188 function setObservableState($state_message)
4190 $this->_observable_state
= $state_message;
4193 function getObservableState()
4195 return $this->_observable_state
;
4199 * Register the reference to an object object
4202 function &addObserver(&$observer)
4204 static $observers, $registered_observers;
4205 $observer_class_name = get_class($observer);
4206 if(!isset($registered_observers[$observer_class_name]) && func_num_args() == 1){
4207 $observers[] =& $observer;
4208 $registered_observers[$observer_class_name] = count($observers);
4214 * Register the reference to an object object
4217 function &getObservers()
4219 $observers =& $this->addObserver(&$this, false);
4230 ====================================================================
4231 See also: Validators.
4236 * Returns the Errors array that holds all information about attribute error messages.
4238 function getErrors()
4240 return $this->_errors
;
4244 * Adds an error to the base object instead of any particular attribute. This is used
4245 * to report errors that doesn't tie to any specific attribute, but rather to the object
4246 * as a whole. These error messages doesn't get prepended with any field name when iterating
4247 * with yieldEachFullError, so they should be complete sentences.
4249 function addErrorToBase($message)
4251 $this->addError($this->getModelName(), $message);
4255 * Returns errors assigned to base object through addToBase according to the normal rules of getErrorsOn($attribute).
4257 function getBaseErrors()
4259 $errors = $this->getErrors();
4260 return (array)@$errors[$this->getModelName()];
4265 * Adds an error message ($message) to the ($attribute), which will be returned on a call to <tt>getErrorsOn($attribute)</tt>
4266 * for the same attribute and ensure that this error object returns false when asked if <tt>hasErrors</tt>. More than one
4267 * error can be added to the same $attribute in which case an array will be returned on a call to <tt>getErrorsOn($attribute)</tt>.
4268 * If no $message is supplied, "invalid" is assumed.
4270 function addError($attribute, $message = 'invalid')
4272 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
4273 $this->_errors
[$attribute][] = $message;
4277 * Will add an error message to each of the attributes in $attributes that is empty.
4279 function addErrorOnEmpty($attribute_names, $message = 'empty')
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(empty($this->$attribute)){
4285 $this->addError($attribute, $message);
4291 * Will add an error message to each of the attributes in $attributes that is blank (using $this->isBlank).
4293 function addErrorOnBlank($attribute_names, $message = 'blank')
4295 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
4296 $attribute_names = Ak
::toArray($attribute_names);
4297 foreach ($attribute_names as $attribute){
4298 if($this->isBlank(@$this->$attribute)){
4299 $this->addError($attribute, $message);
4305 * Will add an error message to each of the attributes in $attributes that has a length outside of the passed boundary $range.
4306 * If the length is above the boundary, the too_long_message message will be used. If below, the too_short_message.
4308 function addErrorOnBoundaryBreaking($attribute_names, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short')
4310 $too_long_message = isset($this->_defaultErrorMessages
[$too_long_message]) ?
$this->_defaultErrorMessages
[$too_long_message] : $too_long_message;
4311 $too_short_message = isset($this->_defaultErrorMessages
[$too_short_message]) ?
$this->_defaultErrorMessages
[$too_short_message] : $too_short_message;
4313 $attribute_names = Ak
::toArray($attribute_names);
4314 foreach ($attribute_names as $attribute){
4315 if(@$this->$attribute < $range_begin){
4316 $this->addError($attribute, $too_short_message);
4318 if(@$this->$attribute > $range_end){
4319 $this->addError($attribute, $too_long_message);
4325 function addErrorOnBoundryBreaking ($attributes, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short')
4327 $this->addErrorOnBoundaryBreaking($attributes, $range_begin, $range_end, $too_long_message, $too_short_message);
4331 * Returns true if the specified $attribute has errors associated with it.
4333 function isInvalid($attribute)
4335 return $this->getErrorsOn($attribute);
4339 * Returns false, if no errors are associated with the specified $attribute.
4340 * Returns the error message, if one error is associated with the specified $attribute.
4341 * Returns an array of error messages, if more than one error is associated with the specified $attribute.
4343 function getErrorsOn($attribute)
4345 if (empty($this->_errors
[$attribute])){
4347 }elseif (count($this->_errors
[$attribute]) == 1){
4348 $k = array_keys($this->_errors
[$attribute]);
4349 return $this->_errors
[$attribute][$k[0]];
4351 return $this->_errors
[$attribute];
4357 * Yields each attribute and associated message per error added.
4359 function yieldEachError()
4361 foreach ($this->_errors
as $errors){
4362 foreach ($errors as $error){
4363 $this->yieldError($error);
4368 function yieldError($message)
4370 $messages = is_array($message) ?
$message : array($message);
4371 foreach ($messages as $message){
4372 echo "<div class='error'><p>$message</p></div>\n";
4378 * Yields each full error message added. So Person->addError("first_name", "can't be empty") will be returned
4379 * through iteration as "First name can't be empty".
4381 function yieldEachFullError()
4383 $full_messages = $this->getFullErrorMessages();
4384 foreach ($full_messages as $full_message){
4385 $this->yieldError($full_message);
4391 * Returns all the full error messages in an array.
4393 function getFullErrorMessages()
4395 $full_messages = array();
4397 foreach ($this->_errors
as $attribute=>$errors){
4398 $full_messages[$attribute] = array();
4399 foreach ($errors as $error){
4400 $full_messages[$attribute][] = $this->t('%attribute_name %error', array(
4401 '%attribute_name'=>AkInflector
::humanize($this->_delocalizeAttribute($attribute)),
4406 return $full_messages;
4410 * Returns true if no errors have been added.
4412 function hasErrors()
4414 return !empty($this->_errors
);
4418 * Removes all the errors that have been added.
4420 function clearErrors()
4422 $this->_errors
= array();
4426 * Returns the total number of errors added. Two errors added to the same attribute will be counted as such
4427 * with this as well.
4429 function countErrors()
4432 foreach ($this->_errors
as $errors){
4433 $error_count = count($errors)+
$error_count;
4436 return $error_count;
4440 function errorsToString($print = false)
4442 $result = "\n<div id='errors'>\n<ul class='error'>\n";
4443 foreach ($this->getFullErrorMessages() as $error){
4444 $result .= is_array($error) ?
"<li class='error'>".join('</li><li class=\'error\'>',$error)."</li>\n" : "<li class='error'>$error</li>\n";
4446 $result .= "</ul>\n</div>\n";
4460 ====================================================================
4461 See also: Acts as List, Acts as Tree, Acts as Nested Set.
4465 * actAs provides a method for extending Active Record models.
4468 * $this->actsAs('list', array('scope' => 'todo_list'));
4470 function actsAs($behaviour, $options = array())
4472 $class_name = $this->_getActAsClassName($behaviour);
4473 $underscored_place_holder = AkInflector
::underscore($behaviour);
4474 $camelized_place_holder = AkInflector
::camelize($underscored_place_holder);
4476 if($this->$underscored_place_holder =& $this->_getActAsInstance($class_name, $options)){
4477 $this->$camelized_place_holder =& $this->$underscored_place_holder;
4478 if($this->$underscored_place_holder->init($options)){
4479 $this->__ActsLikeAttributes
[$underscored_place_holder] = $underscored_place_holder;
4487 function _getActAsClassName($behaviour)
4489 $class_name = AkInflector
::camelize($behaviour);
4490 return file_exists(AK_LIB_DIR
.DS
.'AkActiveRecord'.DS
.'AkActsAsBehaviours'.DS
.'AkActsAs'.$class_name.'.php') && !class_exists('ActsAs'.$class_name) ?
4491 'AkActsAs'.$class_name : 'ActsAs'.$class_name;
4497 function &_getActAsInstance($class_name, $options)
4499 if(!class_exists($class_name)){
4500 if(substr($class_name,0,2) == 'Ak'){
4501 include_once(AK_LIB_DIR
.DS
.'AkActiveRecord'.DS
.'AkActsAsBehaviours'.DS
.$class_name.'.php');
4503 include_once(AK_APP_PLUGINS_DIR
.DS
.AkInflector
::underscore($class_name).DS
.'lib'.DS
.$class_name.'.php');
4506 if(!class_exists($class_name)){
4507 trigger_error(Ak
::t('The class %class used for handling an "act_as %class" does not exist',array('%class'=>$class_name)), E_USER_ERROR
);
4511 $ActAsInstance =& new $class_name($this, $options);
4512 return $ActAsInstance;
4519 function _loadActAsBehaviours()
4521 $this->act_as
= !empty($this->acts_as
) ?
$this->acts_as
: (empty($this->act_as
) ?
false : $this->act_as
);
4522 if(!empty($this->act_as
)){
4523 if(is_string($this->act_as
)){
4524 $this->act_as
= array_unique(array_diff(array_map('trim',explode(',',$this->act_as
.',')), array('')));
4525 foreach ($this->act_as
as $type){
4526 $this->actsAs($type);
4528 }elseif (is_array($this->act_as
)){
4529 foreach ($this->act_as
as $type=>$options){
4530 $this->actsAs($type, $options);
4537 * Returns a comma separated list of possible acts like (active record, nested set, list)....
4541 $result = 'active record';
4542 foreach ($this->__ActsLikeAttributes
as $type){
4543 if(!empty($this->$type) && is_object($this->$type) && method_exists($this->{$type}, 'getType')){
4544 $result .= ','.$this->{$type}->getType();
4550 /*/Act as Behaviours*/
4554 ====================================================================
4560 if(!$this->isConnected()){
4561 $this->setConnection();
4563 $this->_db
->connection
->debug
= $this->_db
->connection
->debug ?
false : true;
4564 $this->db_debug
=& $this->_db
->connection
->debug
;
4567 function toString($print = false)
4570 if(!AK_CLI ||
(AK_ENVIRONMENT
== 'testing' && !AK_CLI
)){
4571 $result = "<h2>Details for ".AkInflector
::humanize(AkInflector
::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()."</h2>\n<dl>\n";
4572 foreach ($this->getColumnNames() as $column=>$caption){
4573 $result .= "<dt>$caption</dt>\n<dd>".$this->getAttribute($column)."</dd>\n";
4575 $result .= "</dl>\n<hr />";
4579 }elseif(AK_ENVIRONMENT
== 'development'){
4581 str_replace("\n"," ",var_export($this->getAttributes(),true));
4586 $result = "\n-------\n Details for ".AkInflector
::humanize(AkInflector
::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()." ==\n\n/==\n";
4587 foreach ($this->getColumnNames() as $column=>$caption){
4588 $result .= "\t * $caption: ".$this->getAttribute($column)."\n";
4590 $result .= "\n\n-------\n";
4598 function dbugging($trace_this_on_debug_mode = null)
4600 if(!empty($this->_db
->debug
) && !empty($trace_this_on_debug_mode)){
4601 $message = !is_scalar($trace_this_on_debug_mode) ?
var_export($trace_this_on_debug_mode, true) : (string)$trace_this_on_debug_mode;
4602 Ak
::trace($message);
4604 return !empty($this->_db
->debug
);
4609 function debug ($data = 'active_record_class', $_functions=0)
4611 if(!AK_DEBUG
&& !AK_DEV_MODE
){
4615 $data = $data == 'active_record_class' ?
(AK_PHP5 ?
clone($this) : $this) : $data;
4617 if($_functions!=0) {
4623 if (isset ($data)) {
4624 if (is_array($data) ||
is_object($data)) {
4626 if (count ($data)) {
4627 echo AK_CLI ?
"/--\n" : "<ol>\n";
4628 while (list ($key,$value) = each ($data)) {
4632 $type=gettype($value);
4633 if ($type=="array") {
4634 AK_CLI ?
printf ("\t* (%s) %s:\n",$type, $key) :
4635 printf ("<li>(%s) <b>%s</b>:\n",$type, $key);
4637 Ak
::debug ($value,$sf);
4638 $lines = explode("\n",ob_get_clean()."\n");
4639 foreach ($lines as $line){
4640 echo "\t".$line."\n";
4642 }elseif($type == "object"){
4643 if(method_exists($value,'hasColumn') && $value->hasColumn($key)){
4644 $value->toString(true);
4645 AK_CLI ?
printf ("\t* (%s) %s:\n",$type, $key) :
4646 printf ("<li>(%s) <b>%s</b>:\n",$type, $key);
4648 Ak
::debug ($value,$sf);
4649 $lines = explode("\n",ob_get_clean()."\n");
4650 foreach ($lines as $line){
4651 echo "\t".$line."\n";
4654 }elseif (eregi ("function", $type)) {
4656 AK_CLI ?
printf ("\t* (%s) %s:\n",$type, $key, $value) :
4657 printf ("<li>(%s) <b>%s</b> </li>\n",$type, $key, $value);
4663 AK_CLI ?
printf ("\t* (%s) %s = %s\n",$type, $key, $value) :
4664 printf ("<li>(%s) <b>%s</b> = %s</li>\n",$type, $key, $value);
4667 echo AK_CLI ?
"\n--/\n" : "</ol>fin.\n";
4681 ====================================================================
4684 * Selects and filters a search result to include only specified columns
4686 * $people_for_select = $People->select($People->find(),'name','email');
4688 * Now $people_for_select will hold an array with
4690 * array ('name' => 'Jose','email' => 'jose@example.com'),
4691 * array ('name' => 'Alicia','email' => 'alicia@example.com'),
4692 * array ('name' => 'Hilario','email' => 'hilario@example.com'),
4693 * array ('name' => 'Bermi','email' => 'bermi@example.com')
4696 function select(&$source_array)
4698 $resulting_array = array();
4699 if(!empty($source_array) && is_array($source_array) && func_num_args() > 1) {
4700 (array)$args = array_filter(array_slice(func_get_args(),1),array($this,'hasColumn'));
4701 foreach ($source_array as $source_item){
4702 $item_fields = array();
4703 foreach ($args as $arg){
4704 $item_fields[$arg] =& $source_item->get($arg);
4706 $resulting_array[] =& $item_fields;
4709 return $resulting_array;
4714 * Collect is a function for selecting items from double depth array
4715 * like the ones returned by the AkActiveRecord. This comes useful when you just need some
4716 * fields for generating tables, select lists with only desired fields.
4718 * $people_for_select = Ak::select($People->find(),'id','email');
4720 * Returns something like:
4722 * array ('10' => 'jose@example.com'),
4723 * array ('15' => 'alicia@example.com'),
4724 * array ('16' => 'hilario@example.com'),
4725 * array ('18' => 'bermi@example.com')
4728 function collect(&$source_array, $key_index, $value_index)
4730 $resulting_array = array();
4731 if(!empty($source_array) && is_array($source_array)) {
4732 foreach ($source_array as $source_item){
4733 $resulting_array[$source_item->get($key_index)] = $source_item->get($value_index);
4736 return $resulting_array;
4741 return Ak
::toJson($this->getAttributes());
4745 * converts to yaml-strings
4748 * User::toYaml($users->find('all'));
4751 * @param array of ActiveRecords[optional] $data
4753 function toYaml($data = null)
4755 return Ak
::convert('active_record', 'yaml', empty($data) ?
$this : $data);
4760 * Parses an special formated array as a list of keys and values
4762 * This function generates an array with values and keys from an array with numeric keys.
4764 * This allows to parse an array to a function in the following manner.
4765 * create('first_name->', 'Bermi', 'last_name->', 'Ferrer');
4766 * //Previous code will be the same that
4767 * create(array('first_name'=>'Bermi', 'last_name'=> 'Ferrer'));
4769 * Use this syntax only for quick testings, not for production environments. If the number of arguments varies, the result might be unpredictable.
4771 * This function syntax is disabled by default. You need to define('AK_ENABLE_AKELOS_ARGS', true)
4772 * if you need this functionality.
4776 function parseAkelosArgs(&$args)
4778 if(!AK_ENABLE_AKELOS_ARGS
){
4779 $this->_castDateParametersFromDateHelper_($args);
4782 $k = array_keys($args);
4783 if(isset($k[1]) && substr($args[$k[0]],-1) == '>'){
4786 for($i = 0; $i < $size; $i++
) {
4788 if(!isset($key) && is_string($args[$k[$i]]) && substr($v,-1) == '>'){
4789 $key = rtrim($v, '=-> ');
4790 }elseif(isset($key)) {
4794 $params[$k[$i]] = $v;
4797 if(!empty($params)){
4801 $this->_castDateParametersFromDateHelper_($args);
4804 * Gets an array from a string.
4806 * Acts like Php explode() function but uses any of this as valid separators ' AND ',' and ',' + ',' ',',',';'
4808 function getArrayFromAkString($string)
4810 if(is_array($string)){
4813 $string = str_replace(array(' AND ',' and ',' + ',' ',',',';'),array('|','|','|','','|','|'),trim($string));
4814 return strstr($string,'|') ?
explode('|', $string) : array($string);
4819 function getAttributeCondition($argument)
4821 if(is_array($argument)){
4823 }elseif (is_null($argument)){
4833 ====================================================================
4839 var $_calculation_options = array('conditions', 'joins', 'order', 'select', 'group', 'having', 'distinct', 'limit', 'offset');
4842 * Count operates using three different approaches.
4844 * * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
4845 * * Count by conditions or joins
4846 * * Count using options will find the row count matched by the options used.
4848 * The last approach, count using options, accepts an option hash as the only parameter. The options are:
4850 * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array("user_name = ?", $username ). See conditions in the intro.
4851 * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
4852 * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
4853 * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
4854 * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join.
4855 * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
4857 * Examples for counting all:
4858 * $Person->count(); // returns the total count of all people
4860 * Examples for count by +conditions+ and +joins+ (this has been deprecated):
4861 * $Person->count("age > 26"); // returns the number of people older than 26
4862 * $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(*).
4864 * Examples for count with options:
4865 * $Person->count('conditions' => "age > 26");
4866 * $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.
4867 * $Person->count('id', 'conditions' => "age > 26"); // Performs a COUNT(id)
4868 * $Person->count('all', 'conditions' => "age > 26"); // Performs a COUNT(*) ('all' is an alias for '*')
4870 * Note: $Person->count('all') will not work because it will use 'all' as the condition. Use $Person->count() instead.
4874 $args = func_get_args();
4875 list($column_name, $options) = $this->_constructCountOptionsFromLegacyArgs($args);
4876 return $this->calculate('count', $column_name, $options);
4880 * Calculates average value on a given column. The value is returned as a float. See #calculate for examples with options.
4882 * $Person->average('age');
4884 function average($column_name, $options = array())
4886 return $this->calculate('avg', $column_name, $options);
4890 * 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.
4892 * $Person->minimum('age');
4894 function minimum($column_name, $options = array())
4896 return $this->calculate('min', $column_name, $options);
4900 * 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.
4902 * $Person->maximum('age');
4904 function maximum($column_name, $options = array())
4906 return $this->calculate('max', $column_name, $options);
4910 * 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.
4912 * $Person->sum('age');
4914 function sum($column_name, $options = array())
4916 return $this->calculate('sum', $column_name, $options);
4920 * This calculates aggregate values in the given column: Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
4921 * Options such as 'conditions', 'order', 'group', 'having', and 'joins' can be passed to customize the query.
4923 * There are two basic forms of output:
4924 * * 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.
4925 * * Grouped values: This returns an ordered hash of the values and groups them by the 'group' option. It takes a column name.
4927 * $values = $Person->maximum('age', array('group' => 'last_name'));
4928 * echo $values["Drake"]
4932 * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array( "user_name = ?", username ). See conditions in the intro.
4933 * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
4934 * The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
4935 * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
4936 * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
4937 * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join.
4938 * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
4941 * $Person->calculate('count', 'all'); // The same as $Person->count();
4942 * $Person->average('age'); // SELECT AVG(age) FROM people...
4943 * $Person->minimum('age', array('conditions' => array('last_name != ?', 'Drake'))); // Selects the minimum age for everyone with a last name other than 'Drake'
4944 * $Person->minimum('age', array('having' => 'min(age) > 17', 'group' => 'last'_name)); // Selects the minimum age for any family without any minors
4946 function calculate($operation, $column_name, $options = array())
4948 $this->_validateCalculationOptions($options);
4949 $column_name = empty($options['select']) ?
$column_name : $options['select'];
4950 $column_name = $column_name == 'all' ?
'*' : $column_name;
4951 $column = $this->_getColumnFor($column_name);
4952 if (!empty($options['group'])){
4953 return $this->_executeGroupedCalculation($operation, $column_name, $column, $options);
4955 return $this->_executeSimpleCalculation($operation, $column_name, $column, $options);
4964 function _constructCountOptionsFromLegacyArgs($args)
4967 $column_name = 'all';
4972 count(options=array())
4973 count($column_name='all', $options=array())
4974 count($conditions=null, $joins=null)
4976 if(count($args) > 2){
4977 trigger_error(Ak
::t("Unexpected parameters passed to count(\$options=array())", E_USER_ERROR
));
4978 }elseif(count($args) > 0){
4979 if(!empty($args[0]) && is_array($args[0])){
4980 $options = $args[0];
4981 }elseif(!empty($args[1]) && is_array($args[1])){
4982 $column_name = array_shift($args);
4983 $options = array_shift($args);
4985 $options = array('conditions' => $args[0]);
4986 if(!empty($args[1])){
4987 $options = array_merge($options, array('joins' => $args[1]));
4991 return array($column_name, $options);
4998 function _constructCalculationSql($operation, $column_name, $options)
5000 $operation = strtolower($operation);
5001 $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name);
5002 $use_workaround = $operation == 'count' && !empty($options['distinct']) && $this->_getDatabaseType() == 'sqlite';
5004 $sql = $use_workaround ?
5005 "SELECT COUNT(*) AS $aggregate_alias" : // A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
5006 "SELECT $operation(".(empty($options['distinct'])?
'':'DISTINCT ')."$column_name) AS $aggregate_alias";
5009 $sql .= empty($options['group']) ?
'' : ", {$options['group_field']} AS {$options['group_alias']}";
5010 $sql .= $use_workaround ?
" FROM (SELECT DISTINCT {$column_name}" : '';
5011 $sql .= " FROM ".$this->getTableName()." ";
5013 $sql .= empty($options['joins']) ?
'' : " {$options['joins']} ";
5015 empty($options['conditions']) ?
null : $this->addConditions($sql, $options['conditions']);
5017 if (!empty($options['group'])){
5018 $sql .= " GROUP BY {$options['group_field']} ";
5019 $sql .= empty($options['having']) ?
'' : " HAVING {$options['having']} ";
5022 $sql .= empty($options['order']) ?
'' : " ORDER BY {$options['order']} ";
5023 $this->_db
->addLimitAndOffset($sql, $options);
5024 $sql .= $use_workaround ?
')' : '';
5032 function _executeSimpleCalculation($operation, $column_name, $column, $options)
5034 $value = $this->_db
->selectValue($this->_constructCalculationSql($operation, $column_name, $options));
5035 return $this->_typeCastCalculatedValue($value, $column, $operation);
5041 function _executeGroupedCalculation($operation, $column_name, $column, $options)
5043 $group_field = $options['group'];
5044 $group_alias = $this->_getColumnAliasFor($group_field);
5045 $group_column = $this->_getColumnFor($group_field);
5046 $options = array_merge(array('group_field' => $group_field, 'group_alias' => $group_alias),$options);
5047 $sql = $this->_constructCalculationSql($operation, $column_name, $options);
5048 $calculated_data = $this->_db
->select($sql);
5049 $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name);
5052 foreach ($calculated_data as $row){
5053 $key = $this->_typeCastCalculatedValue($row[$group_alias], $group_column);
5054 $all[$key] = $this->_typeCastCalculatedValue($row[$aggregate_alias], $column, $operation);
5062 function _validateCalculationOptions($options = array())
5064 $invalid_options = array_diff(array_keys($options),$this->_calculation_options
);
5065 if(!empty($invalid_options)){
5066 trigger_error(Ak
::t('%options are not valid calculation options.', array('%options'=>join(', ',$invalid_options))), E_USER_ERROR
);
5071 * Converts a given key to the value that the database adapter returns as
5072 * as a usable column name.
5073 * users.id #=> users_id
5074 * sum(id) #=> sum_id
5075 * count(distinct users.id) #=> count_distinct_users_id
5076 * count(*) #=> count_all
5080 function _getColumnAliasFor()
5082 $args = func_get_args();
5083 $keys = strtolower(join(' ',(!empty($args) ?
(is_array($args[0]) ?
$args[0] : $args) : array())));
5084 return preg_replace(array('/\*/','/\W+/','/^ +/','/ +$/','/ +/'),array('all',' ','','','_'), $keys);
5090 function _getColumnFor($field)
5092 $field_name = ltrim(substr($field,strpos($field,'.')),'.');
5093 if(in_array($field_name,$this->getColumnNames())){
5102 function _typeCastCalculatedValue($value, $column, $operation = null)
5104 $operation = strtolower($operation);
5105 if($operation == 'count'){
5106 return intval($value);
5107 }elseif ($operation == 'avg'){
5108 return floatval($value);
5110 return empty($column) ?
$value : AkActiveRecord
::castAttributeFromDatabase($column, $value);
5116 function hasBeenModified()
5118 return Ak
::objectHasBeenModified($this);
5122 * Just freeze the attributes hash, such that associations are still accessible even on destroyed records.
5124 * @todo implement freeze correctly for its intended use
5128 return $this->_freeze
= true;
5133 return !empty($this->_freeze
);
5137 * Alias for getModelName()
5141 return $this->getModelName();
5144 function &objectCache()
5147 $args =& func_get_args();
5148 if(count($args) == 2){
5149 if(!isset($cache[$args[0]])){
5150 $cache[$args[0]] =& $args[1];
5152 }elseif(!isset($cache[$args[0]])){
5155 return $cache[$args[0]];
5161 ====================================================================
5162 Right now Akelos uses phpAdodb for bd abstraction. This are functionalities not
5163 provided in phpAdodb and that will move to a separated driver for each db
5166 function _extractValueFromDefault($default)
5168 if($this->_getDatabaseType() == 'postgre'){
5169 if(preg_match("/^'(.*)'::/", $default, $match)){
5172 // a postgre HACK; we dont know the column-type here
5173 if ($default=='true') {
5176 if ($default=='false') {