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');
26 // Akelos args is a short way to call functions that is only intended for fast prototyping
27 defined('AK_ENABLE_AKELOS_ARGS') ?
null : define('AK_ENABLE_AKELOS_ARGS', false);
28 // Use setColumnName if available when using set('column_name', $value);
29 defined('AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT') ?
null : define('AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT', true);
30 defined('AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS') ?
null : define('AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS', false);
31 defined('AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS') ?
null : define('AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS', AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS
);
32 defined('AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS') ?
null : define('AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS', AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS
);
33 defined('AK_ACTIVE_RECORD_ENABLE_PERSISTENCE') ?
null : define('AK_ACTIVE_RECORD_ENABLE_PERSISTENCE', AK_ENVIRONMENT
!= 'testing');
34 defined('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA') ?
null : define('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA', AK_ACTIVE_RECORD_ENABLE_PERSISTENCE
&& AK_ENVIRONMENT
!= 'development');
35 defined('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE') ?
null : define('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE', 300);
36 defined('AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES') ?
null : define('AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES', true);
37 defined('AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS') ?
null : define('AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS', false);
38 defined('AK_NOT_EMPTY_REGULAR_EXPRESSION') ?
null : define('AK_NOT_EMPTY_REGULAR_EXPRESSION','/.+/');
39 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");
40 defined('AK_NUMBER_REGULAR_EXPRESSION') ?
null : define('AK_NUMBER_REGULAR_EXPRESSION',"/^[0-9]+$/");
41 defined('AK_PHONE_REGULAR_EXPRESSION') ?
null : define('AK_PHONE_REGULAR_EXPRESSION',"/^([\+]?[(]?[\+]?[ ]?[0-9]{2,3}[)]?[ ]?)?[0-9 ()\-]{4,25}$/");
42 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}$/");
43 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])$/");
44 defined('AK_POST_CODE_REGULAR_EXPRESSION') ?
null : define('AK_POST_CODE_REGULAR_EXPRESSION',"/^[0-9A-Za-z -]{2,9}$/");
47 // Forces loading database schema on every call
48 if(AK_DEV_MODE
&& isset($_SESSION['__activeRecordColumnsSettingsCache'])){
49 unset($_SESSION['__activeRecordColumnsSettingsCache']);
52 ak_compat('array_combine');
55 * Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with
56 * which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
57 * is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain
58 * database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
60 * See the mapping rules in table_name and the full example in README.txt for more insight.
64 * 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
65 * you're receiving the data from somewhere else, like a HTTP request. It works like this:
68 * $user = new User(array('name' => 'David', 'occupation' => 'Code Artist'));
69 * echo $user->name; // Will print "David"
72 * You can also use a parameter list initialization.:
74 * $user = new User('name->', 'David', 'occupation->', 'Code Artist');
76 * And of course you can just create a bare object and specify the attributes after the fact:
80 * $user->name = 'David';
81 * $user->occupation = 'Code Artist';
86 * Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement.
87 * The array form is to be used when the condition input is tainted and requires sanitization. The string form can
88 * be used for statements that doesn't involve tainted data. Examples:
91 * class User extends ActiveRecord
93 * function authenticateUnsafely($user_name, $password)
95 * return findFirst("user_name = '$user_name' AND password = '$password'");
98 * function authenticateSafely($user_name, $password)
100 * return findFirst("user_name = ? AND password = ?", $user_name, $password);
105 * The <tt>authenticateUnsafely</tt> method inserts the parameters directly into the query and is thus susceptible to SQL-injection
106 * attacks if the <tt>$user_name</tt> and <tt>$password</tt> parameters come directly from a HTTP request. The <tt>authenticateSafely</tt> method,
107 * 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
108 * an attacker can't escape the query and fake the login (or worse).
110 * When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth
111 * question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing
112 * the question marks with symbols and supplying a hash with values for the matching symbol keys:
115 * $Company->findFirst(
116 * "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
117 * array(':id' => 3, ':name' => "37signals", ':division' => "First", ':accounting_date' => '2005-01-01')
121 * == Accessing attributes before they have been type casted ==
123 * Some times you want to be able to read the raw attribute data without having the column-determined type cast run its course first.
124 * That can be done by using the <attribute>_before_type_cast accessors that all attributes have. For example, if your Account model
125 * has a balance attribute, you can call $Account->balance_before_type_cast or $Account->id_before_type_cast.
127 * This is especially useful in validation situations where the user might supply a string for an integer field and you want to display
128 * the original string back in an error message. Accessing the attribute normally would type cast the string to 0, which isn't what you
131 * == Saving arrays, hashes, and other non-mappable objects in text columns ==
133 * Active Record can serialize any object in text columns. To do so, you must specify this with by setting the attribute serialize with
134 * a comma separated list of columns or an array.
135 * This makes it possible to store arrays, hashes, and other non-mappeable objects without doing any additional work. Example:
138 * class User extends ActiveRecord
140 * var $serialize = 'preferences';
143 * $User = new User(array('preferences'=>array("background" => "black", "display" => 'large')));
144 * $User->find($user_id);
145 * $User->preferences // array("background" => "black", "display" => 'large')
148 * == Single table inheritance ==
150 * Active Record allows inheritance by storing the name of the class in a column that by default is called "type" (can be changed
151 * by overwriting <tt>AkActiveRecord->_inheritanceColumn</tt>). This means that an inheritance looking like this:
154 * class Company extends ActiveRecord{}
155 * class Firm extends Company{}
156 * class Client extends Company{}
157 * class PriorityClient extends Client{}
160 * When you do $Firm->create('name =>', "akelos"), this record will be saved in the companies table with type = "Firm". You can then
161 * fetch this row again using $Company->find('first', "name = '37signals'") and it will return a Firm object.
163 * 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
164 * like normal subclasses with no special magic for differentiating between them or reloading the right type with find.
166 * Note, all the attributes for all the cases are kept in the same table. Read more:
167 * http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
169 * == Connection to multiple databases in different models ==
171 * Connections are usually created through AkActiveRecord->establishConnection and retrieved by AkActiveRecord->connection.
172 * All classes inheriting from AkActiveRecord will use this connection. But you can also set a class-specific connection.
173 * For example, if $Course is a AkActiveRecord, but resides in a different database you can just say $Course->establishConnection
174 * and $Course and all its subclasses will use this connection instead.
176 * Active Records will automatically record creation and/or update timestamps of database objects
177 * if fields of the names created_at/created_on or updated_at/updated_on are present.
178 * Date only: created_on, updated_on
179 * Date and time: created_at, updated_at
181 * This behavior can be turned off by setting <tt>$this->_recordTimestamps = false</tt>.
183 class AkActiveRecord
extends AkAssociatedActiveRecord
188 //var $disableAutomatedAssociationLoading = true;
193 var $_dataDictionary;
195 var $_inheritanceColumn;
199 var $_internationalize;
201 var $_errors = array();
203 var $_attributes = array();
205 var $_protectedAttributes = array();
206 var $_accessibleAttributes = array();
208 var $_recordTimestamps = true;
210 // Column description
211 var $_columnNames = array();
212 // Array of column objects for the table associated with this class.
213 var $_columns = array();
214 // Columns that can be edited/viewed
215 var $_contentColumns = array();
216 // Methods that will be dinamically loaded for the model (EXPERIMENTAL) This pretends to generate something similar to Ruby on Rails finders.
217 // If you set array('findOneByUsernameAndPassword', 'findByCompany', 'findAllByExipringDate')
218 // You'll get $User->findOneByUsernameAndPassword('admin', 'pass');
219 var $_dynamicMethods = false;
220 var $_combinedAttributes = array();
222 var $_BlobQueryStack = null;
224 var $_automated_max_length_validator = true;
225 var $_automated_validators_enabled = true;
226 var $_automated_not_null_validator = false;
227 var $_set_default_attribute_values_automatically = true;
229 // This is needed for enabling support for static active record instantation under php
230 var $_activeRecordHasBeenInstantiated = true;
232 var $__ActsLikeAttributes = array();
235 * Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations.
237 var $_defaultErrorMessages = array(
238 'inclusion' => "is not included in the list",
239 'exclusion' => "is reserved",
240 'invalid' => "is invalid",
241 'confirmation' => "doesn't match confirmation",
242 'accepted' => "must be accepted",
243 'empty' => "can't be empty",
244 'blank' => "can't be blank",
245 'too_long' => "is too long (max is %d characters)",
246 'too_short' => "is too short (min is %d characters)",
247 'wrong_length' => "is the wrong length (should be %d characters)",
248 'taken' => "has already been taken",
249 'not_a_number' => "is not a number"
252 var $__activeRecordObject = true;
256 function __construct()
258 $attributes = (array)func_get_args();
259 return $this->init($attributes);
262 function init($attributes = array())
264 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->_instatiateDefaultObserver();
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($attributes[1] as $k=>$v){
300 $attributes[1][$k] = $this->castAttributeFromDatabase($k, $v);
302 $avoid_loading_associations = isset($attributes[1]['load_associations']) ?
false : !empty($this->disableAutomatedAssociationLoading
);
303 $this->setAttributes($attributes[1], true);
305 $this->newRecord($attributes);
308 $this->_buildFinders();
309 empty($avoid_loading_associations) ?
$this->loadAssociations() : null;
312 function __destruct()
318 * New objects can be instantiated as either empty (pass no construction parameter) or pre-set with attributes but not yet saved
319 * (pass an array with key names matching the associated table column names).
320 * In both instances, valid attribute keys are determined by the column names of the associated table; hence you can't
321 * have attributes that aren't part of the table columns.
323 function newRecord($attributes)
325 $this->_newRecord
= true;
327 if(AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS
&& empty($attributes)){
331 if(isset($attributes) && !is_array($attributes)){
332 $attributes = func_get_args();
334 $this->setAttributes($this->attributesFromColumnDefinition(),true);
335 $this->setAttributes($attributes);
340 * Returns a clone of the record that hasn't been assigned an id yet and is treated as a new record.
342 function cloneRecord()
344 $model_name = $this->getModelName();
345 $attributes = $this->getAttributesBeforeTypeCast();
346 if(isset($attributes[$this->getPrimaryKey()])){
347 unset($attributes[$this->getPrimaryKey()]);
349 return new $model_name($attributes);
354 * Returns true if this object hasn't been saved yet that is, a record for the object doesn't exist yet.
356 function isNewRecord()
358 if(!isset($this->_newRecord
) && !isset($this->{$this->getPrimaryKey()})){
359 $this->_newRecord
= true;
361 return $this->_newRecord
;
367 * Reloads the attributes of this object from the database.
374 if($object = $this->find($this->getId())){
375 $this->setAttributes($object->getAttributes(), true);
386 ====================================================================
389 * Creates an object, instantly saves it as a record (if the validation permits it), and returns it.
390 * If the save fail under validations, the unsaved object is still returned.
392 function &create($attributes = null)
394 if(!isset($this->_activeRecordHasBeenInstantiated
)){
395 return Ak
::handleStaticCall();
398 if(func_num_args() > 1){
399 $attributes = func_get_args();
401 $model = $this->getModelName();
403 $object =& new $model();
404 $object->setAttributes($attributes);
409 function createOrUpdate($validate = true)
411 if($validate && !$this->isValid() ||
!($this->isNewRecord() ?
$this->afterValidationOnCreate() : $this->afterValidationOnUpdate())){
412 $this->transactionFail();
415 return $this->isNewRecord() ?
$this->_create() : $this->_update();
418 function &findOrCreateBy()
420 $args = func_get_args();
421 $Item =& Ak
::call_user_func_array(array(&$this,'findFirstBy'), $args);
423 $attributes = array();
425 list($sql, $columns) = $this->_getFindBySqlAndColumns(array_shift($args), $args);
427 if(!empty($columns)){
428 foreach ($columns as $column){
429 $attributes[$column] = array_shift($args);
432 $Item =& $this->create($attributes);
433 $Item->has_been_created
= true;
435 $Item->has_been_created
= false;
437 $Item->has_been_found
= !$Item->has_been_created
;
442 * Creates a new record with values matching those of the instance attributes.
443 * Must be called as a result of a call to createOrUpdate.
449 if($this->isFrozen()){
450 $this->transactionFail();
454 if($this->beforeCreate()){
456 $this->notifyObservers('beforeCreate');
458 if($this->_recordTimestamps
){
459 if ($this->hasColumn('created_at')){
460 $this->setAttribute('created_at', Ak
::getDate());
462 if ($this->hasColumn('created_on')){
463 $this->setAttribute('created_on', Ak
::getDate(null, 'Y-m-d'));
466 if(isset($this->expires_on
)){
467 if(isset($this->expires_at
) && $this->hasColumn('expires_at')){
468 $this->setAttribute('expires_at',Ak
::getDate(strtotime($this->expires_at
) +
(defined('AK_TIME_DIFFERENCE') ? AK_TIME_DIFFERENCE
*60 : 0)));
469 }elseif(isset($this->expires_on
) && $this->hasColumn('expires_on')){
470 $this->setAttribute('expires_on',Ak
::getDate(strtotime($this->expires_on
) +
(defined('AK_TIME_DIFFERENCE') ? AK_TIME_DIFFERENCE
*60 : 0), 'Y-m-d'));
475 $attributes = $this->getColumnsForAtrributes($this->getAttributes());
477 if($this->isLockingEnabled()){
478 $attributes['lock_version'] = 1;
479 $this->setAttribute('lock_version',1);
482 $pk = $this->getPrimaryKey();
483 $table = $this->getTableName();
485 foreach ($attributes as $column=>$value){
486 $attributes[$column] = $this->castAttributeForDatabase($column,$value);
490 * @todo sanitize attributes
491 * 'beforeValidationOnCreate', 'afterValidationOnCreate'
493 if(!isset($this->_generateSequence
) ||
(isset($this->_generateSequence
) && $this->_generateSequence
!== false)){
494 if((empty($attributes[$pk]) ||
(!empty($attributes[$pk]) && (integer)$attributes[$pk] > 0 ))){
495 if($this->_getDatabaseType() == 'sqlite'){
496 $table_details = $this->_databaseTableInternals('seq_'.$table);
497 if(!isset($table_details['ID'])){
498 $this->_db
->CreateSequence('seq_'.$table);
500 $attributes[$pk] = $this->_db
->GenID('seq_'.$table);
505 $__attributes = $attributes;
506 $attributes = array_diff($attributes, array('',"''"));
508 $sql = 'INSERT INTO '.$table.' '.
509 '('.join(', ',array_keys($attributes)).') '.
510 'VALUES ('.join(',',array_values($attributes)).')';
512 if(!$this->_executeSql($sql, false)){
513 AK_DEBUG ?
trigger_error($this->_db
->ErrorMsg(), E_USER_NOTICE
) : null;
516 $id = !empty($attributes[$pk]) ?
$attributes[$pk] : $this->_db
->Insert_ID($table, $pk);
519 if(!$this->transactionHasFailed()){
520 $this->_newRecord
= false;
521 if(!$this->afterCreate()){
522 $this->transactionFail();
524 $this->notifyObservers('afterCreate');
528 $this->transactionFail();
533 /*/Creating records*/
538 ====================================================================
541 * - No record exists: Creates a new record with values matching those of the object attributes.
542 * - A record does exist: Updates the record with values matching those of the object attributes.
544 function save($validate = true)
546 if($this->isFrozen()){
550 $this->transactionStart();
551 if($this->beforeSave() && $this->notifyObservers('beforeSave')){
552 $result = $this->createOrUpdate($validate);
553 if(!$this->transactionHasFailed()){
554 if(!$this->afterSave()){
555 $this->transactionFail();
557 if(!$this->notifyObservers('afterSave')){
558 $this->transactionFail();
563 $this->transactionFail();
566 $result = $this->transactionHasFailed() ?
false : $result;
567 $this->transactionComplete();
576 ====================================================================
577 See also: Counting Attributes.
581 * Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
583 * $Product->countBySql("SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id");
585 function countBySql($sql)
587 if(!isset($this->_activeRecordHasBeenInstantiated
)){
588 return Ak
::handleStaticCall();
590 if(!stristr($sql, 'COUNT') && stristr($sql, ' FROM ')){
591 $sql = 'SELECT COUNT(*) '.substr($sql,strpos(str_replace(' from ',' FROM ', $sql),' FROM '));
593 if(!$this->isConnected()){
594 $this->setConnection();
597 AK_LOG_EVENTS ?
($this->Logger
->message($this->getModelName().' executing SQL: '.$sql)) : null;
598 $rs = $this->_db
->Execute($sql);
600 return @(integer)$rs->fields
[0];
602 /*/Counting Records*/
606 ====================================================================
611 * Finds the record from the passed id, instantly saves it with the passed attributes (if the validation permits it),
612 * and returns it. If the save fail under validations, the unsaved object is still returned.
614 function update($id, $attributes)
616 if(!isset($this->_activeRecordHasBeenInstantiated
)){
617 return Ak
::handleStaticCall();
621 foreach ($id as $idx=>$single_id){
622 $results[] = $this->update($single_id, isset($attributes[$idx]) ?
$attributes[$idx] : $attributes);
626 $object =& $this->find($id);
627 $object->updateAttributes($attributes);
633 * Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
634 * Note: Make sure that updates made with this method doesn't get subjected to validation checks.
635 * Hence, attributes can be updated even if the full object isn't valid.
637 function updateAttribute($name, $value)
639 $this->setAttribute($name, $value);
640 return $this->save(false);
645 * Updates all the attributes in from the passed array and saves the record. If the object is
646 * invalid, the saving will fail and false will be returned.
648 function updateAttributes($attributes, $object = null)
650 isset($object) ?
$object->setAttributes($attributes) : $this->setAttributes($attributes);
652 return isset($object) ?
$object->save() : $this->save();
656 * Updates all records with the SET-part of an SQL update statement in updates and returns an
657 * integer with the number of rows updates. A subset of the records can be selected by specifying conditions. Example:
658 * <code>$Billing->updateAll("category = 'authorized', approved = 1", "author = 'David'");</code>
660 * Important note: Conditions are not sanitized yet so beware of accepting
661 * variable conditions when using this function
663 function updateAll($updates, $conditions = null)
665 if(!isset($this->_activeRecordHasBeenInstantiated
)){
666 return Ak
::handleStaticCall();
669 * @todo sanitize sql conditions
671 $sql = 'UPDATE '.$this->getTableName().' SET '.$updates;
672 $sql .= isset($conditions) ?
' WHERE '.$conditions : '';
673 $this->_executeSql($sql);
674 return $this->_db
->Affected_Rows();
679 * Updates the associated record with values matching those of the instance attributes.
680 * Must be called as a result of a call to createOrUpdate.
686 if($this->isFrozen()){
687 $this->transactionFail();
690 if($this->beforeUpdate()){
691 $this->notifyObservers('beforeUpdate');
693 if($this->_recordTimestamps
){
694 if ($this->hasColumn('updated_at')){
695 $this->setAttribute('updated_at', Ak
::getDate());
697 if ($this->hasColumn('updated_on')){
698 $this->setAttribute('updated_on', Ak
::getDate(null, 'Y-m-d'));
704 if ($this->isLockingEnabled()){
705 $previous_value = $this->lock_version
;
706 $this->setAttribute('lock_version', $previous_value +
1);
707 $lock_check = ' AND lock_version = '.$previous_value;
710 $quoted_attributes = $this->getAvailableAttributesQuoted();
712 if(!empty($quoted_attributes)){
713 $sql = 'UPDATE '.$this->getTableName().' '.
714 'SET '.join(', ', $quoted_attributes) .' '.
715 'WHERE '.$this->getPrimaryKey().'='.$this->quotedId().$lock_check;
718 if(!$this->_executeSql($sql, false)){
719 $this->transactionFail();
720 AK_DEBUG ?
trigger_error($this->_db
->ErrorMsg(), E_USER_NOTICE
) : null;
723 if ($this->isLockingEnabled()){
724 if($this->_db
->Affected_Rows() != 1){
725 $this->setAttribute('lock_version', $previous_value);
726 $this->transactionFail();
727 trigger_error(Ak
::t('Attempted to update a stale object'), E_USER_NOTICE
);
732 if(!$this->transactionHasFailed()){
733 if($this->afterUpdate()){
734 $this->notifyObservers('afterUpdate');
736 $this->transactionFail();
741 $this->transactionFail();
746 /*/Updating records*/
752 ====================================================================
757 * Deletes the record with the given id without instantiating an object first. If an array of
758 * ids is provided, all of them are deleted.
762 if(!isset($this->_activeRecordHasBeenInstantiated
)){
763 return Ak
::handleStaticCall();
765 $id = func_num_args() > 1 ?
func_get_args() : $id;
766 return $this->deleteAll($this->getPrimaryKey().' IN ('.(is_array($id) ?
join(', ',$id) : $id).')');
771 * Deletes all the records that matches the condition without instantiating the objects first
772 * (and hence not calling the destroy method). Example:
774 * <code>$Post->destroyAll("person_id = 5 AND (category = 'Something' OR category = 'Else')");</code>
776 * Important note: Conditions are not sanitized yet so beware of accepting
777 * variable conditions when using this function
779 function deleteAll($conditions = null)
781 if(!isset($this->_activeRecordHasBeenInstantiated
)){
782 return Ak
::handleStaticCall();
785 * @todo sanitize sql conditions
787 $sql = 'DELETE FROM '.$this->getTableName();
789 $sql .= isset($conditions) ?
' WHERE '.$conditions : ($this->_getDatabaseType() == 'sqlite' ?
' WHERE 1' : ''); // (HACK) If where clause is not included sqlite_changes will not get the right result
790 $this->_executeSql($sql);
791 return $this->_db
->Affected_Rows() > 0;
796 * Destroys the record with the given id by instantiating the object and calling destroy
797 * (all the callbacks are the triggered). If an array of ids is provided, all of them are destroyed.
798 * Deletes the record in the database and freezes this instance to reflect that no changes should be
799 * made (since they can't be persisted).
801 function destroy($id = null)
803 if(!isset($this->_activeRecordHasBeenInstantiated
)){
804 return Ak
::handleStaticCall();
807 $this->transactionStart();
808 $id = func_num_args() > 1 ?
func_get_args() : $id;
811 $id_arr = is_array($id) ?
$id : array($id);
812 if($objects = $this->find($id_arr)){
813 $results = count($objects);
815 for ($i=0; $results > $i; $i++
){
816 if(!$objects[$i]->destroy()){
817 $no_problems = false;
820 $this->transactionComplete();
823 $this->transactionComplete();
827 if(!$this->isNewRecord()){
828 if($this->beforeDestroy()){
829 $this->notifyObservers('beforeDestroy');
831 $sql = 'DELETE FROM '.$this->getTableName().' WHERE '.$this->getPrimaryKey().' = '.$this->_db
->qstr($this->getId());
832 $this->_executeSql($sql);
833 $had_success = ($this->_db
->Affected_Rows() > 0);
834 if(!$had_success ||
($had_success && !$this->afterDestroy())){
835 $this->transactionFail();
836 $had_success = false;
838 $had_success = $this->notifyObservers('afterDestroy') === false ?
false : true;
840 $this->transactionComplete();
844 $this->transactionFail();
845 $this->transactionComplete();
851 if(!$this->afterDestroy()){
852 $this->transactionFail();
854 $this->notifyObservers('afterDestroy');
857 $this->transactionComplete();
864 * Destroys the objects for all the records that matches the condition by instantiating
865 * each object and calling the destroy method.
869 * $Person->destroyAll("last_login < '2004-04-04'");
871 function destroyAll($conditions)
873 if($objects = $this->find('all',array('conditions'=>$conditions))){
874 $results = count($objects);
876 for ($i=0; $results > $i; $i++
){
877 if(!$objects[$i]->destroy()){
878 $no_problems = false;
887 /*/Deleting records*/
894 ====================================================================
898 * Returns true if the given id represents the primary key of a record in the database, false otherwise. Example:
900 * $Person->exists(5);
904 return $this->find('first',array('conditions' => array($this->getPrimaryKey().' = '.$id))) !== false;
908 * Find operates with three different retrieval approaches:
909 * * Find by id: This can either be a specific id find(1), a list of ids find(1, 5, 6),
910 * or an array of ids find(array(5, 6, 10)). If no record can be found for all of the listed ids,
911 * then RecordNotFound will be raised.
912 * * Find first: This will return the first record matched by the options used. These options
913 * can either be specific conditions or merely an order.
914 * If no record can matched, false is returned.
915 * * Find all: This will return all the records matched by the options used. If no records are found, an empty array is returned.
917 * All approaches accepts an $option array as their last parameter. The options are:
919 * 'conditions' => An SQL fragment like "administrator = 1" or array("user_name = ?" => $username). See conditions in the intro.
920 * 'order' => An SQL fragment like "created_at DESC, name".
921 * 'limit' => An integer determining the limit on the number of rows that should be returned.
922 * 'offset' => An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
923 * 'joins' => An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = $id". (Rarely needed).
924 * 'include' => Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols
925 * named refer to already defined associations. See eager loading under Associations.
927 * Examples for find by id:
929 * $Person->find(1); // returns the object for ID = 1
930 * $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
931 * $Person->find(array(7, 17)); // returns an array for objects with IDs in (7, 17)
932 * $Person->find(array(1)); // returns an array for objects the object with ID = 1
933 * $Person->find(1, array('conditions' => "administrator = 1", 'order' => "created_on DESC"));
936 * Examples for find first:
938 * $Person->find('first'); // returns the first object fetched by SELECT * FROM people
939 * $Person->find('first', array('conditions' => array("user_name = ':user_name'", ':user_name' => $user_name)));
940 * $Person->find('first', array('order' => "created_on DESC", 'offset' => 5));
943 * Examples for find all:
945 * $Person->find('all'); // returns an array of objects for all the rows fetched by SELECT * FROM people
946 * $Person->find(); // Same as $Person->find('all');
947 * $Person->find('all', array('conditions => array("category IN (categories)", 'categories' => join(','$categories)), 'limit' => 50));
948 * $Person->find('all', array('offset' => 10, 'limit' => 10));
949 * $Person->find('all', array('include' => array('account', 'friends'));
954 if(!isset($this->_activeRecordHasBeenInstantiated
)){
955 return Ak
::handleStaticCall();
958 $num_args = func_num_args();
959 if($num_args === 2 && func_get_arg(0) == 'set arguments'){
960 $args = func_get_arg(1);
961 $num_args = count($args);
964 $args = $num_args > 0 ?
(!isset($args) ?
func_get_args() : $args) : array('all');
966 // Handle find('first', 23)
967 if($num_args >= 2 && is_numeric($args[1]) && $args[0] == 'first'){
969 $args[0] = (integer)$args[0];
973 if($num_args === 1 && is_numeric($args[0]) && $args[0] > 0){
974 $args[0] = (integer)$args[0]; //Cast query by Id
977 $options = $num_args > 0 && (is_array($args[$num_args-1]) && isset($args[0][0]) && !is_numeric($args[0][0])) ?
array_pop($args) : array();
979 //$options = func_get_arg(func_num_args()-1);
980 if(!empty($options['conditions']) && is_array($options['conditions'])){
981 if (isset($options['conditions'][0]) && strstr($options['conditions'][0], '?') && count($options['conditions']) > 1){
982 $pattern = array_shift($options['conditions']);
983 $options['bind'] = array_values($options['conditions']);
984 $options['conditions'] = $pattern;
985 }elseif (isset($options['conditions'][0])){
986 $pattern = array_shift($options['conditions']);
987 $options['conditions'] = str_replace(array_keys($options['conditions']), array_values($this->getSanitizedConditionsArray($options['conditions'])),$pattern);
989 $options['conditions'] = join(' AND ',(array)$this->getAttributesQuoted($options['conditions']));
993 if ($num_args === 2 && !empty($args[0]) && !empty($args[1]) && is_string($args[0]) && ($args[0] == 'all' ||
$args[0] == 'first') && is_string($args[1])){
994 if (!is_array($args[1]) && $args[1] > 0 && $args[0] == 'first'){
996 $args = array($args[1]);
999 $options['conditions'] = $args[1];
1000 $args = array($args[0]);
1002 }elseif ($num_args === 1 && isset($args[0]) && is_string($args[0]) && $args[0] != 'all' && $args[0] != 'first'){
1003 $options = array('conditions'=> $args[0]);
1004 $args = array('first');
1007 if(!empty($options['conditions']) && is_numeric($options['conditions']) && $options['conditions'] > 0){
1008 unset($options['conditions']);
1012 if(!empty($args[0]) && is_string($args[0]) && strstr($args[0],'?')){
1013 $options = array_merge(array('conditions' => array_shift($args)), $options);
1014 $options['bind'] = $args;
1015 $args = array('all');
1016 }elseif (!empty($args[1]) && is_string($args[1]) && strstr($args[1],'?')){
1017 $_tmp_mode = array_shift($args);
1018 $options = array_merge(array('conditions' => array_shift($args)),$options);
1019 $options['bind'] = $args;
1020 $args = array($_tmp_mode);
1027 $options = array_merge($options, array((!empty($options['include']) && $this->hasAssociations() ?
'virtual_limit':'limit')=>1));
1028 $result =& $this->find('all', $options);
1030 if(!empty($result) && is_array($result)){
1031 $_result =& $result[0];
1041 $limit = isset($options['limit']) ?
$options['limit'] : null;
1042 $offset = isset($options['offset']) ?
$options['offset'] : null;
1043 if((empty($options['conditions']) && empty($options['order']) && is_null($offset) && $this->_getDatabaseType() == 'postgre' ?
1 : 0)){
1044 $options['order'] = $this->getPrimaryKey();
1046 $sql = $this->constructFinderSql($options);
1047 if(!empty($options['bind']) && is_array($options['bind']) && strstr($sql,'?')){
1048 $sql = array_merge(array($sql),$options['bind']);
1051 if((!empty($options['include']) && $this->hasAssociations())){
1052 $result =& $this->findWithAssociations($options, $limit, $offset);
1054 $result =& $this->findBySql($sql, $limit, $offset);
1057 if(!empty($result) && is_array($result)){
1058 $_result =& $result;
1067 $ids = array_unique(isset($args[0]) ?
(is_array($args[0]) ?
$args[0] : (array)$args) : array());
1069 $num_ids = count($ids);
1070 $num_args = count($args);
1072 if(isset($ids[$num_ids-1]) && is_array($ids[$num_ids-1])){
1073 $options = array_merge($options, array_pop($ids));
1077 if($num_args === 1 && !$args[0] > 0){
1078 $options['conditions'] = $args[0];
1081 $conditions = !empty($options['conditions']) ?
' AND '.$options['conditions'] : '';
1083 if(empty($options) && !empty($args[0]) && !empty($args[1]) && is_array($args[0]) && is_array($args[1])){
1084 $options = array_pop($args);
1090 trigger_error($this->t('Couldn\'t find %object_name without an ID%conditions',array('%object_name'=>$this->getModelName(),'%conditions'=>$conditions)), E_USER_ERROR
);
1094 $table_name = !empty($options['include']) && $this->hasAssociations() ?
'__owner' : $this->getTableName();
1095 $result =& $this->find('first', array_merge($options, array('conditions' => $table_name.'.'.$this->getPrimaryKey().' = '.$ids[0].$conditions)));
1096 if(is_array($args[0]) && $result !== false){
1097 //This is a dirty hack for avoiding PHP4 pass by reference error
1098 $result_for_ref = array(&$result);
1099 $_result =& $result_for_ref;
1101 $_result =& $result;
1109 $ids_condition = $this->getPrimaryKey().' IN ('.join(', ',$ids).')';
1111 if(!empty($options['conditions']) && is_array($options['conditions'])){
1112 $options['conditions'][0] = $ids_condition.' AND '.$options['conditions'][0];
1113 }elseif(!empty($options['conditions'])){
1114 $options['conditions'] = $ids_condition.' AND '.$options['conditions'];
1116 $without_conditions = true;
1117 $options['conditions'] = $ids_condition;
1120 $result =& $this->find('all', $options);
1121 if(is_array($result) && (count($result) == $num_ids ||
empty($without_conditions))){
1122 if($result === false){
1125 $_result =& $result;
1140 function &findFirst()
1142 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1143 return Ak
::handleStaticCall();
1145 $args = func_get_args();
1146 $result =& Ak
::call_user_func_array(array(&$this,'find'), array_merge(array('first'),$args));
1152 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1153 return Ak
::handleStaticCall();
1155 $args = func_get_args();
1156 $result =& Ak
::call_user_func_array(array(&$this,'find'), array_merge(array('all'),$args));
1162 * Works like find_all, but requires a complete SQL string. Examples:
1163 * $Post->findBySql("SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id");
1164 * $Post->findBySql(array("SELECT * FROM posts WHERE author = ? AND created_on > ?", $author_id, $start_date));
1166 function &findBySql($sql, $limit = null, $offset = null, $bindings = null)
1168 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1169 return Ak
::handleStaticCall();
1172 $sql_query = array_shift($sql);
1173 $bindings = is_array($sql) && count($sql) > 0 ?
$sql : array($sql);
1176 $this->setConnection();
1178 AK_LOG_EVENTS ?
$this->_startSqlBlockLog() : null;
1181 if(is_integer($limit)){
1182 if(is_integer($offset)){
1183 $results = !empty($bindings) ?
$this->_db
->SelectLimit($sql, $limit, $offset, $bindings) : $this->_db
->SelectLimit($sql, $limit, $offset);
1185 $results = !empty($bindings) ?
$this->_db
->SelectLimit($sql, $limit, -1, $bindings) : $this->_db
->SelectLimit($sql, $limit);
1188 $results = !empty($bindings) ?
$this->_db
->Execute($sql, $bindings) : $this->_db
->Execute($sql);
1191 AK_LOG_EVENTS ?
$this->_endSqlBlockLog() : null;
1194 AK_DEBUG ?
trigger_error($this->_db
->ErrorMsg(), E_USER_NOTICE
) : null;
1197 while ($record = $results->FetchRow()) {
1198 $objects[] =& $this->instantiate($this->getOnlyAvailableAtrributes($record), false);
1206 * This function pretends to emulate RoR finders until AkActiveRecord::addMethod becomes stable on future PHP versions.
1207 * @todo use PHP5 __call method for handling the magic finder methods like findFirstByUnsenameAndPassword('bermi','pass')
1209 function &findFirstBy()
1211 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1212 return Ak
::handleStaticCall();
1214 $args = func_get_args();
1215 if($args[0] != 'first'){
1216 array_unshift($args,'first');
1218 $result =& Ak
::call_user_func_array(array(&$this,'findBy'), $args);
1222 function &findLastBy()
1224 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1225 return Ak
::handleStaticCall();
1227 $args = func_get_args();
1228 $options = array_pop($args);
1229 if(!is_array($options)){
1230 array_push($args, $options);
1233 $options['order'] = $this->getPrimaryKey().' DESC';
1234 array_push($args, $options);
1235 $result =& Ak
::call_user_func_array(array(&$this,'findFirstBy'), $args);
1239 function &findAllBy()
1241 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1242 return Ak
::handleStaticCall();
1244 $args = func_get_args();
1245 if($args[0] == 'first'){
1248 $result =& Ak
::call_user_func_array(array(&$this,'findBy'), $args);
1253 * This method allows you to use finders in a more flexible way like:
1255 * findBy('username AND password', $username, $password);
1256 * findBy('age > ? AND name:contains', 18, 'Joe');
1257 * findBy('is_active = true AND session_id', session_id());
1262 if(!isset($this->_activeRecordHasBeenInstantiated
)){
1263 return Ak
::handleStaticCall();
1265 $args = func_get_args();
1266 $find_by_sql = array_shift($args);
1267 if($find_by_sql == 'all' ||
$find_by_sql == 'first'){
1268 $fetch = $find_by_sql;
1269 $find_by_sql = array_shift($args);
1274 $options = array_pop($args);
1276 if(!is_array($options)){
1277 array_push($args, $options);
1281 $query_values = $args;
1282 $query_arguments_count = count($query_values);
1284 list($sql, $requested_args) = $this->_getFindBySqlAndColumns($find_by_sql, $query_values);
1286 if($query_arguments_count != count($requested_args)){
1287 trigger_error(Ak
::t('Argument list did not match expected set. Requested arguments are:').join(', ',$requested_args),E_USER_ERROR
);
1292 $true_bool_values = array(true,1,'true','True','TRUE','1','y','Y','yes','Yes','YES','s','Si','SI','V','v','T','t');
1294 foreach ($requested_args as $k=>$v){
1295 switch ($this->getColumnType($v)) {
1297 $query_values[$k] = in_array($query_values[$k],$true_bool_values) ?
1 : 0;
1302 $query_values[$k] = str_replace('/','-', $this->castAttributeForDatabase($k,$query_values[$k],false));
1310 $_find_arguments = array();
1311 $_find_arguments[] = $fetch;
1312 $_find_arguments[] = $sql;
1313 foreach ($query_values as $value){
1314 $_find_arguments[] = $value;
1316 $_find_arguments[] = $options;
1318 $_result =& $this->find('set arguments', $_find_arguments);
1319 $result =& $_result; // Pass by reference hack
1324 function _getFindBySqlAndColumns($find_by_sql, &$query_values)
1326 $sql = str_replace(array('(',')','||','|','&&','&',' '),array(' ( ',' ) ',' OR ',' OR ',' AND ',' AND ',' '), $find_by_sql);
1327 $operators = array('AND','and','(',')','&','&&','NOT','<>','OR','|','||');
1328 $pieces = explode(' ',$sql);
1329 $pieces = array_diff($pieces,array(' ',''));
1330 $params = array_diff($pieces,$operators);
1331 $operators = array_diff($pieces,$params);
1334 $parameter_count = 0;
1335 $requested_args = array();
1336 foreach ($pieces as $piece){
1337 if(in_array($piece,$params) && $this->hasColumn($piece)){
1338 $new_sql .= $piece.' = ? ';
1339 $requested_args[$parameter_count] = $piece;
1341 }elseif (!in_array($piece,$operators)){
1343 if(strstr($piece,':')){
1344 $_tmp_parts = explode(':',$piece);
1345 if($this->hasColumn($_tmp_parts[0])){
1346 $query_values[$parameter_count] = isset($query_values[$parameter_count]) ?
$query_values[$parameter_count] : $this->get($_tmp_parts[0]);
1347 switch (strtolower($_tmp_parts[1])) {
1353 $query_values[$parameter_count] = '%'.$query_values[$parameter_count].'%';
1354 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1362 $query_values[$parameter_count] = $query_values[$parameter_count].'%';
1363 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1370 case 'finishes_with':
1371 $query_values[$parameter_count] = '%'.$query_values[$parameter_count];
1372 $new_sql .= $_tmp_parts[0]." LIKE ? ";
1375 $query_values[$parameter_count] = $query_values[$parameter_count];
1376 $new_sql .= $_tmp_parts[0].' '.$_tmp_parts[1].' ? ';
1379 $requested_args[$parameter_count] = $_tmp_parts[0];
1382 $new_sql .= $_tmp_parts[0];
1385 $new_sql .= $piece.' ';
1388 $new_sql .= $piece.' ';
1392 return array($new_sql, $requested_args);
1397 * Given a condition that uses bindings like "user = ? AND created_at > ?" will return a
1398 * string replacing the "?" bindings with the column values for current Active Record
1402 function _getVariableSqlCondition($variable_condition)
1404 $query_values = array();
1405 list($sql, $requested_columns) = $this->_getFindBySqlAndColumns($variable_condition, $query_values);
1406 $replacements = array();
1407 $sql = preg_replace('/((('.join($requested_columns,'|').') = \?) = \?)/','$2', $sql);
1408 foreach ($requested_columns as $attribute){
1409 $replacements[$attribute] = $this->castAttributeForDatabase($attribute, $this->get($attribute));
1411 return trim(preg_replace('/('.join('|',array_keys($replacements)).')\s+([^\?]+)\s+\?/e', "isset(\$replacements['\\1']) ? '\\1 \\2 '.\$replacements['\\1']:'\\1 \\2 null'", $sql));
1415 function constructFinderSql($options, $select_from_prefix = 'default')
1417 $sql = isset($options['select_prefix']) ?
$options['select_prefix'] : ($select_from_prefix == 'default' ?
'SELECT * FROM '.$this->getTableName() : $select_from_prefix);
1418 $sql .= !empty($options['joins']) ?
' '.$options['joins'] : '';
1421 if(isset($options['conditions'])){
1422 $this->addConditions($sql, $options['conditions']);
1423 }elseif ($this->getInheritanceColumn() !== false){
1424 $this->addConditions($sql, array());
1427 // Create an alias for order
1428 if(empty($options['order']) && !empty($options['sort'])){
1429 $options['order'] = $options['sort'];
1432 $sql .= !empty($options['order']) ?
' ORDER BY '.$options['order'] : '';
1439 * Adds a sanitized version of $conditions to the $sql string. Note that the passed $sql string is changed.
1441 function addConditions(&$sql, $conditions = null)
1443 $concat = empty($sql) ?
'' : ' WHERE ';
1444 if(!empty($conditions)){
1445 $sql .= $concat.$conditions;
1449 if($this->descendsFromActiveRecord($this) && $this->getInheritanceColumn() !== false){
1450 $type_condition = $this->typeCondition();
1451 $sql .= !empty($type_condition) ?
$concat.$type_condition : '';
1458 * Gets a sanitized version of the input array. Each element will be escaped
1460 function getSanitizedConditionsArray($conditions_array)
1463 foreach ($conditions_array as $k=>$v){
1464 $k = str_replace(':','',$k); // Used for Oracle type bindings
1465 if($this->hasColumn($k)){
1466 $v = $this->castAttributeForDatabase($k, $v);
1475 * This functions is used to get the conditions from an AkRequest object
1477 function getConditions($conditions, $prefix = '', $model_name = null)
1479 $model_name = isset($model_name) ?
$model_name : $this->getModelName();
1480 $model_conditions = !empty($conditions[$model_name]) ?
$conditions[$model_name] : $conditions;
1481 if(is_a($this->$model_name)){
1482 $model_instance =& $this->$model_name;
1484 $model_instance =& $this;
1486 $new_conditions = array();
1487 if(is_array($model_conditions)){
1488 foreach ($model_conditions as $col=>$value){
1489 if($model_instance->hasColumn($col)){
1490 $new_conditions[$prefix.$col] = $value;
1494 return $new_conditions;
1501 function _quoteColumnName($column_name)
1503 return $this->_db
->nameQuote
.$column_name.$this->_db
->nameQuote
;
1510 * EXPERIMENTAL: Will allow to create finders when PHP includes aggregate_methods as a stable feature on PHP4, for PHP5 we might use __call
1514 function _buildFinders($finderFunctions = array('find','findFirst'))
1516 if(!$this->_dynamicMethods
){
1519 $columns = !is_array($this->_dynamicMethods
) ?
array_keys($this->getColumns()) : $this->_dynamicMethods
;
1520 $class_name = 'ak_'.md5(serialize($columns));
1521 if(!class_exists($class_name)){
1522 $permutations = Ak
::permute($columns);
1523 $implementations = '';
1524 foreach ($finderFunctions as $finderFunction){
1525 foreach ($permutations as $permutation){
1526 $permutation = array_map(array('AkInflector','camelize'),$permutation);
1527 foreach ($permutation as $k=>$v){
1528 $method_name = $finderFunction.'By'.join($permutation,'And');
1529 $implementation = 'function &'.$method_name.'(';
1533 foreach ($permutation as $column){
1534 $column = AkInflector
::underscore($column);
1535 $params .= "$$column, ";
1536 $first_param .= "$column ";
1539 $implementation .= trim($params,' ,')."){\n";
1540 $implementation .= '$options = func_num_args() == '.$i.' ? func_get_arg('.($i-1).') : array();'."\n";
1541 $implementation .= 'return $this->'.$finderFunction.'By(\''.$first_param.'\', '.trim($params,' ,').", \$options);\n }\n";
1542 $implementations[$method_name] = $implementation;
1543 array_shift($permutation);
1547 eval('class '.$class_name.' { '.join("\n",$implementations).' } ');
1550 aggregate_methods(&$this, $class_name);
1555 * Finder methods must instantiate through this method to work with the single-table inheritance model and
1556 * eager loading associations.
1557 * that makes it possible to create objects of different types from the same table.
1559 function &instantiate($record, $set_as_new = true)
1561 $inheritance_column = $this->getInheritanceColumn();
1562 if(!empty($record[$inheritance_column])){
1563 $inheritance_column = $record[$inheritance_column];
1564 $inheritance_model_name = AkInflector
::camelize($inheritance_column);
1565 @require_once
(AkInflector
::toModelFilename($inheritance_model_name));
1566 if(!class_exists($inheritance_model_name)){
1567 trigger_error($this->t("The single-table inheritance mechanism failed to locate the subclass: '%class_name'. ".
1568 "This error is raised because the column '%column' is reserved for storing the class in case of inheritance. ".
1569 "Please rename this column if you didn't intend it to be used for storing the inheritance class ".
1570 "or overwrite #{self.to_s}.inheritance_column to use another column for that information.",
1571 array('%class_name'=>$inheritance_model_name, '%column'=>$this->getInheritanceColumn())),E_USER_ERROR
);
1575 $model_name = isset($inheritance_model_name) ?
$inheritance_model_name : $this->getModelName();
1576 $object =& new $model_name('attributes', $record);
1578 $object->_newRecord
= $set_as_new;
1580 (AK_CLI
&& AK_ENVIRONMENT
== 'development') ?
$object ->toString() : null;
1585 /*/Finding records*/
1591 ====================================================================
1593 function descendsFromActiveRecord(&$object)
1595 if(substr(strtolower(get_parent_class($object)),-12) == 'activerecord'){
1598 if(!method_exists($object, 'getInheritanceColumn')){
1601 $inheritance_column = $object->getInheritanceColumn();
1602 return !empty($inheritance_column);
1606 * Gets the column name for use with single table inheritance. Can be overridden in subclasses.
1608 function getInheritanceColumn()
1610 return empty($this->_inheritanceColumn
) ?
($this->hasColumn('type') ?
'type' : false ) : $this->_inheritanceColumn
;
1614 * Defines the column name for use with single table inheritance. Can be overridden in subclasses.
1616 function setInheritanceColumn($column_name)
1618 if(!$this->hasColumn($column_name)){
1619 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
);
1621 }elseif($this->getColumnType($column_name) != 'string'){
1622 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
);
1625 $this->_inheritanceColumn
= $column_name;
1631 function getSubclasses()
1633 $current_class = get_class($this);
1634 $subclasses = array();
1635 $classes = get_declared_classes();
1637 while ($class = array_shift($classes)) {
1638 $parent_class = get_parent_class($class);
1639 if($parent_class == $current_class ||
in_array($parent_class,$subclasses)){
1640 $subclasses[] = $class;
1641 }elseif(!empty($parent_class)){
1642 $classes[] = $parent_class;
1645 $subclasses = array_unique(array_map(array(&$this,'_getModelName'),$subclasses));
1650 function typeCondition()
1652 $inheritance_column = $this->getInheritanceColumn();
1653 $type_condition = array();
1654 $table_name = $this->getTableName();
1655 $available_types = array_merge(array($this->getModelName()),$this->getSubclasses());
1656 foreach ($available_types as $subclass){
1657 $type_condition[] = ' '.$table_name.'.'.$inheritance_column.' = \''.AkInflector
::humanize(AkInflector
::underscore($subclass)).'\' ';
1659 return empty($type_condition) ?
'' : '('.join('OR',$type_condition).') ';
1662 /*/Table inheritance*/
1668 ====================================================================
1669 See also: Getting Attributes, Model Attributes, Toggling Attributes, Counting Attributes.
1671 function setAttribute($attribute, $value, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS
, $compose_after_set = true)
1673 if($attribute[0] == '_'){
1677 if($this->isFrozen()){
1680 if($inspect_for_callback_child_method === true && method_exists($this,'set'.AkInflector
::camelize($attribute))){
1682 $watchdog[$attribute] = @$watchdog[$attribute]+
1;
1683 if($watchdog[$attribute] == 5000){
1684 if((!defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION')) ||
defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_SET_RECURSION
){
1685 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
);
1689 $this->{$attribute.'_before_type_cast'} = $value;
1690 return $this->{'set'.AkInflector
::camelize($attribute)}($value);
1692 if($this->hasAttribute($attribute)){
1693 $this->{$attribute.'_before_type_cast'} = $value;
1694 $this->$attribute = $value;
1695 if($compose_after_set && !empty($this->_combinedAttributes
) && !$this->requiredForCombination($attribute)){
1696 $combined_attributes = $this->_getCombinedAttributesWhereThisAttributeIsUsed($attribute);
1697 foreach ($combined_attributes as $combined_attribute){
1698 $this->composeCombinedAttribute($combined_attribute);
1701 if ($compose_after_set && $this->isCombinedAttribute($attribute)){
1702 $this->decomposeCombinedAttribute($attribute);
1704 }elseif(substr($attribute,-12) == 'confirmation' && $this->hasAttribute(substr($attribute,0,-13))){
1705 $this->$attribute = $value;
1708 if($this->_internationalize
){
1709 if(is_array($value)){
1710 $this->setAttributeLocales($attribute, $value);
1711 }elseif(is_string($inspect_for_callback_child_method)){
1712 $this->setAttributeByLocale($attribute, $value, $inspect_for_callback_child_method);
1714 $this->_groupInternationalizedAttribute($attribute, $value);
1720 function set($attribute, $value = null, $inspect_for_callback_child_method = true, $compose_after_set = true)
1722 if(is_array($attribute)){
1723 return $this->setAttributes($attribute);
1725 return $this->setAttribute($attribute, $value, $inspect_for_callback_child_method, $compose_after_set);
1729 * Allows you to set all the attributes at once by passing in an array with
1730 * keys matching the attribute names (which again matches the column names).
1731 * Sensitive attributes can be protected from this form of mass-assignment by
1732 * using the $this->setProtectedAttributes method. Or you can alternatively
1733 * specify which attributes can be accessed in with the $this->setAccessibleAttributes method.
1734 * Then all the attributes not included in that won?t be allowed to be mass-assigned.
1736 function setAttributes($attributes, $override_attribute_protection = false)
1738 $this->parseAkelosArgs($attributes);
1739 if(!$override_attribute_protection){
1740 $attributes = $this->removeAttributesProtectedFromMassAssignment($attributes);
1742 if(!empty($attributes) && is_array($attributes)){
1743 foreach ($attributes as $k=>$v){
1744 $this->setAttribute($k, $v);
1750 function setId($value)
1752 if($this->isFrozen()){
1755 $pk = $this->getPrimaryKey();
1756 $this->$pk = $value;
1761 /*/Setting Attributes*/
1765 ====================================================================
1766 See also: Setting Attributes, Model Attributes, Toggling Attributes, Counting Attributes.
1769 function getAttribute($attribute, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS
)
1771 if($attribute[0] == '_'){
1775 if($inspect_for_callback_child_method === true && method_exists($this,'get'.AkInflector
::camelize($attribute))){
1777 $watchdog[@$attribute] = @$watchdog[$attribute]+
1;
1778 if($watchdog[$attribute] == 66){
1779 if((!defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION')) ||
defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_GET_RECURSION
){
1780 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
);
1784 $value = $this->{'get'.AkInflector
::camelize($attribute)}();
1785 return $this->getInheritanceColumn() === $attribute ? AkInflector
::humanize(AkInflector
::underscore($value)) : $value;
1787 if(isset($this->$attribute) ||
(!isset($this->$attribute) && $this->isCombinedAttribute($attribute))){
1788 if($this->hasAttribute($attribute)){
1789 if (!empty($this->_combinedAttributes
) && $this->isCombinedAttribute($attribute)){
1790 $this->composeCombinedAttribute($attribute);
1792 return isset($this->$attribute) ?
$this->$attribute : null;
1793 }elseif($this->_internationalize
&& $this->_isInternationalizeCandidate($attribute)){
1794 if(!empty($this->$attribute) && is_string($this->$attribute)){
1795 return $this->$attribute;
1797 $current_locale = $this->getCurrentLocale();
1798 if(!empty($this->$attribute[$current_locale]) && is_array($this->$attribute)){
1799 return $this->$attribute[$current_locale];
1801 return $this->getAttribute($current_locale.'_'.$attribute);
1805 if($this->_internationalize
){
1806 return $this->getAttributeByLocale($attribute, is_bool($inspect_for_callback_child_method) ?
$this->getCurrentLocale() : $inspect_for_callback_child_method);
1811 function get($attribute = null, $inspect_for_callback_child_method = true)
1813 return !isset($attribute) ?
$this->getAttributes($inspect_for_callback_child_method) : $this->getAttribute($attribute, $inspect_for_callback_child_method);
1817 * Returns an array of all the attributes with their names as keys and clones of their objects as values in case they are objects.
1819 function getAttributes()
1821 $attributes = array();
1822 $available_attributes = $this->getAvailableAttributes();
1823 foreach ($available_attributes as $available_attribute){
1824 $attribute = $this->getAttribute($available_attribute['name']);
1825 $attributes[$available_attribute['name']] = AK_PHP5
&& is_object($attribute) ?
clone($attribute) : $attribute;
1828 if($this->_internationalize
){
1829 $current_locale = $this->getCurrentLocale();
1830 foreach ($this->getInternationalizedColumns() as $column=>$languages){
1831 if(empty($attributes[$column]) && isset($attributes[$current_locale.'_'.$column]) && in_array($current_locale,$languages)){
1832 $attributes[$column] = $attributes[$current_locale.'_'.$column];
1842 * 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.
1846 return $this->{$this->getPrimaryKey()};
1849 /*/Getting Attributes*/
1855 ====================================================================
1856 See also: Setting Attributes, Getting Attributes.
1859 * Turns an attribute that?s currently true into false and vice versa. Returns attribute value.
1861 function toggleAttribute($attribute)
1863 $value = $this->getAttribute($attribute);
1864 $new_value = $value ?
false : true;
1865 $this->setAttribute($attribute, $new_value);
1871 * Toggles the attribute and saves the record.
1873 function toggleAttributeAndSave($attribute)
1875 $value = $this->toggleAttribute($attribute);
1876 if($this->updateAttribute($attribute, $value)){
1882 /*/Toggling Attributes*/
1887 ====================================================================
1888 See also: Counting Records, Setting Attributes, Getting Attributes.
1892 * Increments the specified counter by one. So $DiscussionBoard->incrementCounter("post_count",
1893 * $discussion_board_id); would increment the "post_count" counter on the board responding to
1894 * $discussion_board_id. This is used for caching aggregate values, so that they doesn't need to
1895 * be computed every time. Especially important for looping over a collection where each element
1896 * require a number of aggregate values. Like the $DiscussionBoard that needs to list both the number of posts and comments.
1898 function incrementCounter($counter_name, $id, $difference = 1)
1900 $new_value = $this->getAttribute($counter_name) +
$difference;
1901 if($this->updateAll($counter_name.' = '.$new_value, $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 0){
1908 * Works like AkActiveRecord::incrementCounter, but decrements instead.
1910 function decrementCounter($counter_name, $id, $difference = 1)
1912 $new_value = $this->getAttribute($counter_name) - $difference;
1914 if(!$this->updateAll($counter_name.' = '.$new_value, $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 0){
1921 * Initializes the attribute to zero if null and subtracts one. Only makes sense for number-based attributes. Returns attribute value.
1923 function decrementAttribute($attribute)
1925 if(!isset($this->$attribute)){
1926 $this->setAttribute($attribute, 0);
1929 $value = $this->getAttribute($attribute) -1;
1930 $this->setAttribute($attribute, $value);
1936 * Decrements the attribute and saves the record.
1938 function decrementAndSaveAttribute($attribute)
1940 $value = $this->decrementAttribute($attribute);
1941 if($this->updateAttribute($attribute, $value)){
1949 * Initializes the attribute to zero if null and adds one. Only makes sense for number-based attributes. Returns attribute value.
1951 function incrementAttribute($attribute)
1953 if(!isset($this->$attribute)){
1954 $this->setAttribute($attribute, 0);
1957 $value = $this->getAttribute($attribute) +
1;
1958 $this->setAttribute($attribute, $value);
1964 * Increments the attribute and saves the record.
1966 function incrementAndSaveAttribute($attribute)
1968 $value = $this->incrementAttribute($attribute);
1969 if($this->updateAttribute($attribute, $value)){
1975 /*/Counting Attributes*/
1978 Protecting attributes
1979 ====================================================================
1983 * If this macro is used, only those attributed named in it will be accessible
1984 * for mass-assignment, such as new ModelName($attributes) and $this->attributes($attributes).
1985 * This is the more conservative choice for mass-assignment protection.
1986 * If you'd rather start from an all-open default and restrict attributes as needed,
1987 * have a look at AkActiveRecord::setProtectedAttributes().
1989 function setAccessibleAttributes()
1991 $args = func_get_args();
1992 $this->_accessibleAttributes
= array_unique(array_merge((array)$this->_accessibleAttributes
, $args));
1996 * Attributes named in this macro are protected from mass-assignment, such as
1997 * new ModelName($attributes) and $this->attributes(attributes). Their assignment
1998 * will simply be ignored. Instead, you can use the direct writer methods to do assignment.
1999 * This is meant to protect sensitive attributes to be overwritten by URL/form hackers.
2003 * class Customer extends ActiveRecord
2005 * function Customer()
2007 * $this->setProtectedAttributes('credit_rating');
2011 * $Customer = new Customer('name' => 'David', 'credit_rating' => 'Excellent');
2012 * $Customer->credit_rating // => null
2013 * $Customer->attributes(array('description' => 'Jolly fellow', 'credit_rating' => 'Superb'));
2014 * $Customer->credit_rating // => null
2016 * $Customer->credit_rating = 'Average'
2017 * $Customer->credit_rating // => 'Average'
2020 function setProtectedAttributes()
2022 $args = func_get_args();
2023 $this->_protectedAttributes
= array_unique(array_merge((array)$this->_protectedAttributes
, $args));
2026 function removeAttributesProtectedFromMassAssignment($attributes)
2028 if(!empty($this->_accessibleAttributes
) && is_array($this->_accessibleAttributes
) && is_array($attributes)){
2029 foreach (array_keys($attributes) as $k){
2030 if(!in_array($k,$this->_accessibleAttributes
)){
2031 unset($attributes[$k]);
2034 }elseif (!empty($this->_protectedAttributes
) && is_array($this->_protectedAttributes
) && is_array($attributes)){
2035 foreach (array_keys($attributes) as $k){
2036 if(in_array($k,$this->_protectedAttributes
)){
2037 unset($attributes[$k]);
2044 /*/Protecting attributes*/
2049 ====================================================================
2050 See also: Getting Attributes, Setting Attributes.
2053 * Returns an array of all the attributes that have been specified for serialization as keys and the objects as values.
2055 function getSerializedAttributes()
2057 return isset($this->_serializedAttributes
) ?
$this->_serializedAttributes
: array();
2060 function getAvailableAttributes()
2062 return array_merge($this->getColumns(), $this->getAvailableCombinedAttributes());
2065 function getAttributeCaption($attribute)
2067 return $this->t(AkInflector
::humanize($attribute));
2071 * This function is useful in case you need to know if attributes have been assigned to an object.
2073 function hasAttributesDefined()
2075 $attributes = join('',$this->getAttributes());
2076 return empty($attributes);
2081 * Returns the primary key field.
2083 function getPrimaryKey()
2085 if(!isset($this->_primaryKey
)){
2086 $this->setPrimaryKey();
2088 return $this->_primaryKey
;
2091 function getColumnNames()
2093 if(empty($this->_columnNames
)){
2094 $columns = $this->getColumns();
2095 foreach ($columns as $column_name=>$details){
2096 $this->_columnNames
[$column_name] = isset($details->columnName
) ?
$this->t($details->columnName
) : $this->getAttributeCaption($column_name);
2099 return $this->_columnNames
;
2104 * Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count",
2105 * and columns used for single table inheritance has been removed.
2107 function getContentColumns()
2109 $inheritance_column = $this->getInheritanceColumn();
2110 $columns = $this->getColumns();
2111 foreach ($columns as $name=>$details){
2112 if((substr($name,-3) == '_id' ||
substr($name,-6) == '_count') ||
2113 !empty($details['primaryKey']) ||
($inheritance_column !== false && $inheritance_column == $name)){
2114 unset($columns[$name]);
2121 * Returns an array of names for the attributes available on this object sorted alphabetically.
2123 function getAttributeNames()
2125 if(!isset($this->_activeRecordHasBeenInstantiated
)){
2126 return Ak
::handleStaticCall();
2128 $attributes = array_keys($this->getAvailableAttributes());
2129 $names = array_combine($attributes,array_map(array(&$this,'getAttributeCaption'), $attributes));
2136 * Returns true if the specified attribute has been set by the user or by a database load and is neither null nor empty?
2138 function isAttributePresent($attribute)
2140 $value = $this->getAttribute($attribute);
2141 return !empty($value);
2145 * Returns true if given attribute exists for this Model.
2147 * @param string $attribute
2150 function hasAttribute ($attribute)
2152 empty($this->_columns
) ?
$this->getColumns() : $this->_columns
;
2153 return isset($this->_columns
[$attribute]) ||
(!empty($this->_combinedAttributes
) && $this->isCombinedAttribute($attribute));
2156 /*/Model Attributes*/
2161 ====================================================================
2163 * The Akelos Framework has a handy way to represent combined fields.
2164 * You can add a new attribute to your models using a printf patter to glue
2165 * multiple parameters in a single one.
2167 * For example, If we set...
2168 * $this->addCombinedAttributeConfiguration('name', "%s %s", 'first_name', 'last_name');
2169 * $this->addCombinedAttributeConfiguration('date', "%04d-%02d-%02d", 'year', 'month', 'day');
2170 * $this->setAttributes('first_name=>','John','last_name=>','Smith','year=>',2005,'month=>',9,'day=>',27);
2172 * $this->name // will have "John Smith" as value and
2173 * $this->date // will be 2005-09-27
2175 * On the other hand if you do
2177 * $this->setAttribute('date', '2008-11-30');
2179 * All the 'year', 'month' and 'day' getters will be fired (if they exist) the following attributes will be set
2181 * $this->year // will be 2008
2182 * $this->month // will be 11 and
2183 * $this->day // will be 27
2185 * Sometimes you might need a pattern for composing and another for decomposing attributes. In this case you can specify
2186 * an array as the pattern values, where first element will be the composing pattern and second element will be used
2189 * You can also specify a callback method from this object function instead of a pattern. You can also assign a callback
2190 * for composing and another for decomposing by passing their names as an array like on the patterns.
2193 * class User extends ActiveRecord
2197 * // You can use a multiple patterns array where "%s, %s" will be used for combining fields and "%[^,], %s" will be used
2198 * // for decomposing fields. (as you can see you can also use regular expressions on your patterns)
2199 * $User->addCombinedAttributeConfiguration('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name');
2201 * //Here we set email_link so compose_email_link() will be triggered for building up the field and parse_email_link will
2202 * // be used for getting the fields out
2203 * $User->addCombinedAttributeConfiguration('email_link', array("compose_email_link","parse_email_link"), 'email', 'name');
2205 * // We need to tell the ActiveRecord to load it's magic (see the example below for a simpler solution)
2206 * $attributes = (array)func_get_args();
2207 * return $this->init($attributes);
2210 * function compose_email_link()
2212 * $args = func_get_arg(0);
2213 * return "<a href=\'mailto:{$args[\'email\']}\'>{$args[\'name\']}</a>";
2215 * function parse_email_link($email_link)
2217 * $results = sscanf($email_link, "<a href=\'mailto:%[^\']\'>%[^<]</a>");
2218 * return array(\'email\'=>$results[0],\'name\'=>$results[1]);
2224 * You can also simplify your live by declaring the combined attributes as a class variable like:
2226 * class User extends ActiveRecord
2228 * var $combined_attributes array(
2229 * array('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name')
2230 * array('email_link', array("compose_email_link","parse_email_link"), 'email', 'name')
2240 * Returns true if given attribute is a combined attribute for this Model.
2242 * @param string $attribute
2245 function isCombinedAttribute ($attribute)
2247 return !empty($this->_combinedAttributes
) && isset($this->_combinedAttributes
[$attribute]);
2250 function addCombinedAttributeConfiguration($attribute)
2252 $args = is_array($attribute) ?
$attribute : func_get_args();
2253 $columns = array_slice($args,2);
2254 $invalid_columns = array();
2255 foreach ($columns as $colum){
2256 if(!$this->hasAttribute($colum)){
2257 $invalid_columns[] = $colum;
2260 if(!empty($invalid_columns)){
2261 trigger_error(Ak
::t('There was an error while setting the composed field "%field_name", the following mapping column/s "%columns" do not exist',
2262 array('%field_name'=>$args[0],'%columns'=>join(', ',$invalid_columns))), E_USER_ERROR
);
2264 $attribute = array_shift($args);
2265 $this->_combinedAttributes
[$attribute] = $args;
2266 $this->composeCombinedAttribute($attribute);
2270 function composeCombinedAttributes()
2273 if(!empty($this->_combinedAttributes
)){
2274 $attributes = array_keys($this->_combinedAttributes
);
2275 foreach ($attributes as $attribute){
2276 $this->composeCombinedAttribute($attribute);
2281 function composeCombinedAttribute($combined_attribute)
2283 if($this->isCombinedAttribute($combined_attribute)){
2284 $config = $this->_combinedAttributes
[$combined_attribute];
2285 $pattern = array_shift($config);
2287 $pattern = is_array($pattern) ?
$pattern[0] : $pattern;
2290 foreach ($config as $attribute){
2291 if(isset($this->$attribute)){
2292 $got[$attribute] = $this->getAttribute($attribute);
2295 if(count($got) === count($config)){
2296 $this->$combined_attribute = method_exists($this, $pattern) ?
$this->{$pattern}($got) : vsprintf($pattern, $got);
2304 function _getCombinedAttributesWhereThisAttributeIsUsed($attribute)
2307 foreach ($this->_combinedAttributes
as $combined_attribute=>$settings){
2308 if(in_array($attribute,$settings)){
2309 $result[] = $combined_attribute;
2316 function requiredForCombination($attribute)
2318 foreach ($this->_combinedAttributes
as $settings){
2319 if(in_array($attribute,$settings)){
2326 function hasCombinedAttributes()
2328 return count($this->getCombinedSubattributes()) === 0 ?
false :true;
2331 function getCombinedSubattributes($attribute)
2334 if(is_array($this->_combinedAttributes
[$attribute])){
2335 $attributes = $this->_combinedAttributes
[$attribute];
2336 array_shift($attributes);
2337 foreach ($attributes as $attribute_to_check){
2338 if(isset($this->_combinedAttributes
[$attribute_to_check])){
2339 $result[] = $attribute_to_check;
2346 function decomposeCombinedAttributes()
2348 if(!empty($this->_combinedAttributes
)){
2349 $attributes = array_keys($this->_combinedAttributes
);
2350 foreach ($attributes as $attribute){
2351 $this->decomposeCombinedAttribute($attribute);
2356 function decomposeCombinedAttribute($combined_attribute, $used_on_combined_fields = false)
2358 if(isset($this->$combined_attribute) && $this->isCombinedAttribute($combined_attribute)){
2359 $config = $this->_combinedAttributes
[$combined_attribute];
2360 $pattern = array_shift($config);
2361 $pattern = is_array($pattern) ?
$pattern[1] : $pattern;
2363 if(method_exists($this, $pattern)){
2364 $pieces = $this->{$pattern}($this->$combined_attribute);
2365 if(is_array($pieces)){
2366 foreach ($pieces as $k=>$v){
2367 $is_combined = $this->isCombinedAttribute($k);
2369 $this->decomposeCombinedAttribute($k);
2371 $this->setAttribute($k, $v, true, !$is_combined);
2373 if($is_combined && !$used_on_combined_fields){
2374 $combined_attributes_contained_on_this_attribute = $this->getCombinedSubattributes($combined_attribute);
2375 if(count($combined_attributes_contained_on_this_attribute)){
2376 $this->decomposeCombinedAttribute($combined_attribute, true);
2381 $got = sscanf($this->$combined_attribute, $pattern);
2382 for ($x=0; $x<count($got); $x++
){
2383 $attribute = $config[$x];
2384 $is_combined = $this->isCombinedAttribute($attribute);
2386 $this->decomposeCombinedAttribute($attribute);
2388 $this->setAttribute($attribute, $got[$x], true, !$is_combined);
2394 function getAvailableCombinedAttributes()
2396 $combined_attributes = array();
2397 foreach ($this->_combinedAttributes
as $attribute=>$details){
2398 $combined_attributes[$attribute] = array('name'=>$attribute, 'type'=>'string', 'path' => array_shift($details), 'uses'=>$details);
2400 return !empty($this->_combinedAttributes
) && is_array($this->_combinedAttributes
) ?
$combined_attributes : array();
2403 /*/Combined attributes*/
2410 ====================================================================
2413 * Establishes the connection to the database. Accepts an array as input where the 'adapter'
2414 * key must be specified with the name of a database adapter (in lower-case) example for regular
2415 * databases (MySQL, Postgresql, etc):
2417 * $AkActiveRecord->establishConnection(
2419 * 'adapter' => "mysql",
2420 * 'host' => "localhost",
2421 * 'username' => "myuser",
2422 * 'password' => "mypass",
2423 * 'database' => "somedatabase"
2426 * Example for SQLite database:
2428 * $AkActiveRecord->establishConnection(
2430 * 'adapter' => "sqlite",
2431 * 'dbfile' => "path/to/dbfile"
2435 function &establishConnection($spec = null)
2438 $dns = is_string($spec) ?
$spec : '';
2439 if(!empty($spec['adapter'])){
2440 $dsn = $spec['adapter'] == 'sqlite' ?
2441 'sqlite://'.urlencode($spec['dbfile']).'/' :
2442 $spec['adapter'].'://'.@$spec['username'].':'.@$spec['password'].'@'.@$spec['host'].'/'.@$spec['database'];
2444 $dsn .= isset($spec['persist']) && $spec['persist'] === false ?
'' : '?persist';
2445 return $this->setConnection($dns);
2453 * Returns true if a connection that?s accessible to this class have already been opened.
2455 function isConnected()
2457 return isset($this->_db
);
2461 * Returns the connection currently associated with the class. This can also be used to
2462 * "borrow" the connection to do database work unrelated to any of the specific Active Records.
2464 function &getConnection()
2470 * Set the connection for the class.
2472 function setConnection($dns = null, $connection_id = null)
2474 $this->_db
=& Ak
::db($dns, $connection_id);
2480 function _getDatabaseType()
2482 if(strstr($this->_db
->databaseType
,'mysql')){
2484 }elseif(strstr($this->_db
->databaseType
,'sqlite')){
2486 }elseif(strstr($this->_db
->databaseType
,'post')){
2492 /*/Database connection*/
2497 ====================================================================
2498 See also: Database Reflection.
2502 * Defines the primary key field ? can be overridden in subclasses.
2504 function setPrimaryKey($primary_key = 'id')
2506 if(!$this->hasColumn($primary_key)){
2507 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
);
2509 $this->_primaryKey
= $primary_key;
2514 function getTableName($modify_for_associations = true)
2516 if(!isset($this->_tableName
)){
2517 // We check if we are on a inheritance Table Model
2518 $this->getClassForDatabaseTableMapping();
2519 if(!isset($this->_tableName
)){
2520 $this->setTableName();
2524 if($modify_for_associations && isset($this->_associationTablePrefixes
[$this->_tableName
])){
2525 return $this->_associationTablePrefixes
[$this->_tableName
];
2528 return $this->_tableName
;
2531 function setTableName($table_name = null, $check_for_existence = AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES
, $check_mode = false)
2533 static $available_tables;
2534 if(empty($table_name)){
2535 $table_name = AkInflector
::tableize($this->getModelName());
2537 if($check_for_existence){
2538 if(!isset($available_tables) ||
$check_mode){
2539 if(!isset($this->_db
)){
2540 $this->setConnection();
2542 if(empty($_SESSION['__activeRecordColumnsSettingsCache']['available_tables']) ||
2543 !AK_ACTIVE_RECORD_ENABLE_PERSISTENCE
){
2544 $_SESSION['__activeRecordColumnsSettingsCache']['available_tables'] = $this->_db
->MetaTables();
2546 $available_tables = $_SESSION['__activeRecordColumnsSettingsCache']['available_tables'];
2548 if(!in_array($table_name,(array)$available_tables)){
2550 trigger_error(Ak
::t('Unable to set "%table_name" table for the model "%model".'.
2551 ' There is no "%table_name" available into current database layout.'.
2552 ' Set AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES constant to false in order to'.
2553 ' avoid table name validation',array('%table_name'=>$table_name,'%model'=>$this->getModelName())),E_USER_WARNING
);
2558 $this->_tableName
= $table_name;
2563 function getOnlyAvailableAtrributes($attributes)
2565 $table_name = $this->getTableName();
2566 $ret_attributes = array();
2567 if(!empty($attributes) && is_array($attributes)){
2568 $available_attributes = $this->getAvailableAttributes();
2570 $keys = array_keys($attributes);
2571 $size = sizeOf($keys);
2572 for ($i=0; $i < $size; $i++
){
2573 $k = str_replace($table_name.'.','',$keys[$i]);
2574 if(isset($available_attributes[$k]['name'][$k])){
2575 $ret_attributes[$k] =& $attributes[$keys[$i]];
2579 return $ret_attributes;
2582 function getColumnsForAtrributes($attributes)
2584 $ret_attributes = array();
2585 $table_name = $this->getTableName();
2586 if(!empty($attributes) && is_array($attributes)){
2587 $columns = $this->getColumns();
2588 foreach ($attributes as $k=>$v){
2589 $k = str_replace($table_name.'.','',$k);
2590 if(isset($columns[$k]['name'][$k])){
2591 $ret_attributes[$k] = $v;
2595 return $ret_attributes;
2599 * Returns true if given attribute exists for this Model.
2601 * @param string $name Name of table to look in
2604 function hasColumn($column)
2606 empty($this->_columns
) ?
$this->getColumns() : $this->_columns
;
2607 return isset($this->_columns
[$column]);
2615 ====================================================================
2616 See also: Table Settings, Type Casting.
2621 * Initializes the attributes array with keys matching the columns from the linked table and
2622 * the values matching the corresponding default value of that column, so
2623 * that a new instance, or one populated from a passed-in array, still has all the attributes
2624 * that instances loaded from the database would.
2626 function attributesFromColumnDefinition()
2628 $attributes = array();
2630 foreach ((array)$this->getColumns() as $column_name=>$column_settings){
2631 if (!isset($column_settings['primaryKey']) && isset($column_settings['hasDefault'])) {
2632 $attributes[$column_name] = $this->_extractValueFromDefault($column_settings['defaultValue']);
2634 $attributes[$column_name] = null;
2641 * Gets information from the database engine about a single table
2645 function _databaseTableInternals($table)
2647 if(empty($_SESSION['__activeRecordColumnsSettingsCache']['database_table_'.$table.'_internals']) ||
!AK_ACTIVE_RECORD_ENABLE_PERSISTENCE
){
2648 $_SESSION['__activeRecordColumnsSettingsCache']['database_table_'.$table.'_internals'] = $this->_db
->MetaColumns($table);
2650 $cache[$table] = $_SESSION['__activeRecordColumnsSettingsCache']['database_table_'.$table.'_internals'];
2652 return $cache[$table];
2655 function getColumnsWithRegexBoundaries()
2657 $columns = array_keys($this->getColumns());
2658 foreach ($columns as $k=>$column){
2659 $columns[$k] = '/([^\.])\b('.$column.')\b/';
2666 * If is the first time we use a model this function will run the installer for the model if it exists
2670 function _runCurrentModelInstallerIfExists(&$column_objects)
2672 static $installed_models = array();
2673 if(!defined('AK_AVOID_AUTOMATIC_ACTIVE_RECORD_INSTALLERS') && !in_array($this->getModelName(), $installed_models)){
2674 $installed_models[] = $this->getModelName();
2675 require_once(AK_LIB_DIR
.DS
.'AkInstaller.php');
2676 $installer_name = $this->getModelName().'Installer';
2677 $installer_file = AK_APP_DIR
.DS
.'installers'.DS
.AkInflector
::underscore($installer_name).'.php';
2678 if(file_exists($installer_file)){
2679 require_once($installer_file);
2680 if(class_exists($installer_name)){
2681 $Installer = new $installer_name();
2682 if(method_exists($Installer,'install')){
2683 $Installer->install();
2684 $column_objects = $this->_databaseTableInternals($this->getTableName());
2685 return !empty($column_objects);
2695 * Returns an array of column objects for the table associated with this class.
2697 function getColumns($force_reload = false)
2699 if(empty($this->_columns
) ||
$force_reload){
2700 $this->_columns
= $this->getColumnSettings($force_reload);
2703 return (array)$this->_columns
;
2706 function getColumnSettings($force_reload = false)
2708 if(empty($this->_columnsSettings
) ||
$force_reload){
2709 $this->loadColumnsSettings($force_reload);
2710 $this->initiateColumnsToNull();
2712 return isset($this->_columnsSettings
) ?
$this->_columnsSettings
: array();
2715 function loadColumnsSettings($force_reload = false)
2717 if(is_null($this->_db
)){
2718 $this->setConnection();
2720 $this->_columnsSettings
= $force_reload ?
null : $this->_getPersistedTableColumnSettings();
2722 if(empty($this->_columnsSettings
) ||
!AK_ACTIVE_RECORD_ENABLE_PERSISTENCE
){
2723 if(empty($this->_dataDictionary
)){
2724 $this->_dataDictionary
=& NewDataDictionary($this->_db
);
2727 $column_objects = $this->_databaseTableInternals($this->getTableName());
2729 if( !isset($this->_avoidTableNameValidation
) &&
2730 !is_array($column_objects) &&
2731 !$this->_runCurrentModelInstallerIfExists($column_objects)){
2732 trigger_error(Ak
::t('Ooops! Could not fetch details for the table %table_name.', array('%table_name'=>$this->getTableName())), E_USER_ERROR
);
2734 }elseif (empty($column_objects)){
2735 $this->_runCurrentModelInstallerIfExists($column_objects);
2737 if(is_array($column_objects)){
2738 foreach (array_keys($column_objects) as $k){
2739 $this->setColumnSettings($column_objects[$k]->name
, $column_objects[$k]);
2742 if(!empty($this->_columnsSettings
)){
2743 $this->_persistTableColumnSettings();
2746 return isset($this->_columnsSettings
) ?
$this->_columnsSettings
: array();
2751 function setColumnSettings($column_name, $column_object)
2753 $this->_columnsSettings
[$column_name] = array();
2754 $this->_columnsSettings
[$column_name]['name'] = $column_object->name
;
2756 if($this->_internationalize
&& $this->_isInternationalizeCandidate($column_object->name
)){
2757 $this->_addInternationalizedColumn($column_object->name
);
2760 $this->_columnsSettings
[$column_name]['type'] = $this->getAkelosDataType($column_object);
2761 if(!empty($column_object->primary_key
)){
2762 $this->_primaryKey
= empty($this->_primaryKey
) ?
$column_object->name
: $this->_primaryKey
;
2763 $this->_columnsSettings
[$column_name]['primaryKey'] = true;
2765 if(!empty($column_object->auto_increment
)){
2766 $this->_columnsSettings
[$column_name]['autoIncrement'] = true;
2768 if(!empty($column_object->has_default
)){
2769 $this->_columnsSettings
[$column_name]['hasDefault'] = true;
2771 if(!empty($column_object->not_null
)){
2772 $this->_columnsSettings
[$column_name]['notNull'] = true;
2774 if(!empty($column_object->max_length
) && $column_object->max_length
> 0){
2775 $this->_columnsSettings
[$column_name]['maxLength'] = $column_object->max_length
;
2777 if(isset($column_object->default_value
)){
2778 $this->_columnsSettings
[$column_name]['defaultValue'] = $column_object->default_value
;
2784 * Resets all the cached information about columns, which will cause they to be reloaded on the next request.
2786 function resetColumnInformation()
2788 if(isset($_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName()])){
2789 unset($_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName()]);
2791 $this->_clearPersitedColumnSettings();
2792 $this->_columnNames
= $this->_columns
= $this->_columnsSettings
= $this->_contentColumns
= array();
2798 function _getColumnsSettings()
2800 return $_SESSION['__activeRecordColumnsSettingsCache'];
2806 function _getModelColumnSettings()
2808 return $_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName()];
2814 function _persistTableColumnSettings()
2816 $_SESSION['__activeRecordColumnsSettingsCache'][$this->getModelName().'_column_settings'] = $this->_columnsSettings
;
2822 function _getPersistedTableColumnSettings()
2824 $model_name = $this->getModelName();
2825 if(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA
&& !isset($_SESSION['__activeRecordColumnsSettingsCache']) && AK_CACHE_HANDLER
> 0){
2826 $this->_loadPersistedColumnSetings();
2828 return isset($_SESSION['__activeRecordColumnsSettingsCache'][$model_name.'_column_settings']) ?
2829 $_SESSION['__activeRecordColumnsSettingsCache'][$model_name.'_column_settings'] : false;
2835 function _clearPersitedColumnSettings()
2837 if(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA
&& AK_CACHE_HANDLER
> 0){
2838 $Cache =& Ak
::cache();
2839 $Cache->init(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE
);
2840 $Cache->clean('AkActiveRecord');
2847 function _savePersitedColumnSettings()
2849 if(isset($_SESSION['__activeRecordColumnsSettingsCache'])){
2850 $Cache =& Ak
::cache();
2851 $Cache->init(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE
);
2852 $Cache->save(serialize($_SESSION['__activeRecordColumnsSettingsCache']), 'active_record_db_cache', 'AkActiveRecord');
2859 function _loadPersistedColumnSetings()
2861 if(!isset($_SESSION['__activeRecordColumnsSettingsCache'])){
2862 $Cache =& Ak
::cache();
2863 $Cache->init(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE
);
2864 if($serialized_column_settings = $Cache->get('active_record_db_cache', 'AkActiveRecord') && !empty($serialized_column_settings)){
2865 $_SESSION['__activeRecordColumnsSettingsCache'] = @unserialize
($serialized_column_settings);
2867 }elseif(AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA
){
2868 register_shutdown_function(array($this,'_savePersitedColumnSettings'));
2871 $_SESSION['__activeRecordColumnsSettingsCache'] = array();
2876 function initiateAttributeToNull($attribute)
2878 if(!isset($this->$attribute)){
2879 $this->$attribute = null;
2883 function initiateColumnsToNull()
2885 if(isset($this->_columnsSettings
) && is_array($this->_columnsSettings
)){
2886 array_map(array(&$this,'initiateAttributeToNull'),array_keys($this->_columnsSettings
));
2892 * Akelos data types are mapped to phpAdodb data types
2894 * Returns the Akelos data type for an Adodb Column Object
2896 * 'C'=>'string', // Varchar, capped to 255 characters.
2897 * 'X' => 'text' // Larger varchar, capped to 4000 characters (to be compatible with Oracle).
2898 * 'XL' => 'text' // For Oracle, returns CLOB, otherwise the largest varchar size.
2900 * 'C2' => 'string', // Multibyte varchar
2901 * 'X2' => 'string', // Multibyte varchar (largest size)
2903 * 'B' => 'binary', // BLOB (binary large object)
2905 * 'D' => array('date', 'datetime'), // Date (some databases do not support this, and we return a datetime type)
2906 * 'T' => array('datetime', 'timestamp'), //Datetime or Timestamp
2907 * 'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1)
2908 * 'I' => // Integer (mapped to I4)
2909 * 'I1' => 'integer', // 1-byte integer
2910 * 'I2' => 'integer', // 2-byte integer
2911 * 'I4' => 'integer', // 4-byte integer
2912 * 'I8' => 'integer', // 8-byte integer
2913 * 'F' => 'float', // Floating point number
2914 * 'N' => 'integer' // Numeric or decimal number
2916 * @return string One of this 'string','text','integer','float','datetime','timestamp',
2917 * 'time', 'name','date', 'binary', 'boolean'
2919 function getAkelosDataType(&$adodb_column_object)
2921 $config_var_name = AkInflector
::variablize($adodb_column_object->name
.'_data_type');
2922 if(!empty($this->{$config_var_name})){
2923 return $this->{$config_var_name};
2925 if(stristr($adodb_column_object->type
, 'BLOB')){
2928 if(!empty($adodb_column_object->auto_increment
)) {
2931 $meta_type = $this->_dataDictionary
->MetaType($adodb_column_object);
2932 $adodb_data_types = array(
2933 'C'=>'string', // Varchar, capped to 255 characters.
2934 'X' => 'text', // Larger varchar, capped to 4000 characters (to be compatible with Oracle).
2935 'XL' => 'text', // For Oracle, returns CLOB, otherwise the largest varchar size.
2937 'C2' => 'string', // Multibyte varchar
2938 'X2' => 'string', // Multibyte varchar (largest size)
2940 'B' => 'binary', // BLOB (binary large object)
2942 'D' => array('datetime', 'date'), // Date (some databases do not support this, so we return a datetime type)
2943 'T' => array('datetime', 'timestamp'), //Datetime or Timestamp
2944 'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1)
2945 'R' => 'serial', // Serial Integer
2946 'I' => 'integer', // Integer (mapped to I4)
2947 'I1' => 'integer', // 1-byte integer
2948 'I2' => 'integer', // 2-byte integer
2949 'I4' => 'integer', // 4-byte integer
2950 'I8' => 'integer', // 8-byte integer
2951 'F' => 'float', // Floating point number
2952 'N' => 'integer' // Numeric or decimal number
2955 $result = !isset($adodb_data_types[$meta_type]) ?
2957 (is_array($adodb_data_types[$meta_type]) ?
$adodb_data_types[$meta_type][0] : $adodb_data_types[$meta_type]);
2959 if($result == 'text'){
2960 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 )){
2965 if($this->_getDatabaseType() == 'mysql'){
2966 if($result == 'integer' && (int)$adodb_column_object->max_length
=== 1 &&
2967 stristr($adodb_column_object->type
, 'TINYINT')){
2970 }elseif($this->_getDatabaseType() == 'postgre'){
2971 if($adodb_column_object->type
== 'timestamp' ||
$result == 'datetime'){
2972 $adodb_column_object->max_length
= 19;
2975 if($result == 'boolean' && (int)$adodb_column_object->max_length
!== 1 && stristr($adodb_column_object->type
, 'NUMERIC')){
2978 }elseif($this->_getDatabaseType() == 'sqlite'){
2979 if($result == 'integer' && (int)$adodb_column_object->max_length
=== 1 && stristr($adodb_column_object->type
, 'TINYINT')){
2981 }elseif($result == 'integer' && stristr($adodb_column_object->type
, 'DOUBLE')){
2986 if($result == 'datetime' && substr($adodb_column_object->name
,-3) == '_on'){
2995 * This method retrieves current class name that will be used to map
2996 * your database to this object.
2998 function getClassForDatabaseTableMapping()
3000 $class_name = get_class($this);
3001 if(is_subclass_of($this,'akactiverecord') ||
is_subclass_of($this,'AkActiveRecord')){
3002 $parent_class = get_parent_class($this);
3003 while (substr(strtolower($parent_class),-12) != 'activerecord'){
3004 $class_name = $parent_class;
3005 $parent_class = get_parent_class($parent_class);
3009 $class_name = $this->_getModelName($class_name);
3010 // This is an Active Record Inheritance so we set current table to parent table.
3011 if(!empty($class_name) && strtolower($class_name) != 'activerecord'){
3012 $this->_inheritanceClassName
= $class_name;
3013 @$this->setTableName(AkInflector
::tableize($class_name), false);
3019 function getDisplayField()
3021 return empty($this->displayField
) && $this->hasAttribute('name') ?
'name' : (isset($this->displayField
) && $this->hasAttribute($this->displayField
) ?
$this->displayField
: $this->getPrimaryKey());
3024 function setDisplayField($attribute_name)
3026 if($this->hasAttribute($attribute_name)){
3027 $this->displayField
= $attribute_name;
3037 /*/Database Reflection*/
3041 ====================================================================
3044 function t($string, $array = null)
3046 return Ak
::t($string, $array, AkInflector
::underscore($this->getModelName()));
3049 function getInternationalizedColumns()
3052 $model = $this->getModelName();
3053 $available_locales = $this->getAvailableLocales();
3054 if(empty($cache[$model])){
3055 $cache[$model] = array();
3056 foreach ($this->getColumnSettings() as $column_name=>$details){
3057 if(!empty($details['i18n'])){
3058 $_tmp_pos = strpos($column_name,'_');
3059 $column = substr($column_name,$_tmp_pos+
1);
3060 $lang = substr($column_name,0,$_tmp_pos);
3061 if(in_array($lang, $available_locales)){
3062 $cache[$model][$column] = empty($cache[$model][$column]) ?
array($lang) :
3063 array_merge($cache[$model][$column] ,array($lang));
3069 return $cache[$model];
3072 function getAvailableLocales()
3074 static $available_locales;
3075 if(empty($available_locales)){
3076 if(defined('AK_ACTIVE_RECORD_DEFAULT_LOCALES')){
3077 $available_locales = Ak
::stringToArray(AK_ACTIVE_RECORD_DEFAULT_LOCALES
);
3079 $available_locales = Ak
::langs();
3082 return $available_locales;
3085 function getCurrentLocale()
3087 static $current_locale;
3088 if(empty($current_locale)){
3089 $current_locale = Ak
::lang();
3090 $available_locales = $this->getAvailableLocales();
3091 if(!in_array($current_locale, $available_locales)){
3092 $current_locale = array_shift($available_locales);
3095 return $current_locale;
3099 function getAttributeByLocale($attribute, $locale)
3101 $internationalizable_columns = $this->getInternationalizedColumns();
3102 if(!empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){
3103 return $this->getAttribute($locale.'_'.$attribute);
3107 function getAttributeLocales($attribute)
3109 $attribute_locales = array();
3110 foreach ($this->getAvailableLocales() as $locale){
3111 if($this->hasColumn($locale.'_'.$attribute)){
3112 $attribute_locales[$locale] = $this->getAttributeByLocale($attribute, $locale);
3115 return $attribute_locales;
3118 function setAttributeByLocale($attribute, $value, $locale)
3120 $internationalizable_columns = $this->getInternationalizedColumns();
3122 if($this->_isInternationalizeCandidate($locale.'_'.$attribute) && !empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){
3123 $this->setAttribute($locale.'_'.$attribute, $value);
3128 function setAttributeLocales($attribute, $values = array())
3130 foreach ($values as $locale=>$value){
3131 $this->setAttributeByLocale($attribute, $value, $locale);
3138 function _delocalizeAttribute($attribute)
3140 return $this->_isInternationalizeCandidate($attribute) ?
substr($attribute,3) : $attribute;
3146 function _isInternationalizeCandidate($column_name)
3148 $pos = strpos($column_name,'_');
3149 return $pos === 2 && in_array(substr($column_name,0,$pos),$this->getAvailableLocales());
3155 function _addInternationalizedColumn($column_name)
3157 $this->_columnsSettings
[$column_name]['i18n'] = true;
3162 * Adds an internationalized attribute to an array containing other locales for the same column name
3165 * es_title and en_title will be available user title = array('es'=>'...', 'en' => '...')
3169 function _groupInternationalizedAttribute($attribute, $value)
3171 if($this->_internationalize
&& $this->_isInternationalizeCandidate($attribute)){
3172 if(!empty($this->$attribute)){
3173 $_tmp_pos = strpos($attribute,'_');
3174 $column = substr($attribute,$_tmp_pos+
1);
3175 $lang = substr($attribute,0,$_tmp_pos);
3176 $this->$column = empty($this->$column) ?
array() : $this->$column;
3177 if(empty($this->$column) ||
(!empty($this->$column) && is_array($this->$column))){
3178 $this->$column = empty($this->$column) ?
array($lang=>$value) : array_merge($this->$column,array($lang=>$value));
3191 ====================================================================
3192 See also: Database Reflection.
3195 function getAttributesBeforeTypeCast()
3197 $attributes_array = array();
3198 $available_attributes = $this->getAvailableAttributes();
3199 foreach ($available_attributes as $attribute){
3200 $attribute_value = $this->getAttributeBeforeTypeCast($attribute['name']);
3201 if(!empty($attribute_value)){
3202 $attributes_array[$attribute['name']] = $attribute_value;
3205 return $attributes_array;
3209 function getAttributeBeforeTypeCast($attribute)
3211 if(isset($this->{$attribute.'_before_type_cast'})){
3212 return $this->{$attribute.'_before_type_cast'};
3219 return $this->castAttributeForDatabase($this->getPrimaryKey(), $this->getId());
3223 * 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.
3225 function setSerializeAttribute($attr_name, $class_name = null)
3227 if($this->hasColumn($attr_name)){
3228 $this->_serializedAttributes
[$attr_name] = $class_name;
3232 function getAvailableAttributesQuoted()
3234 return $this->getAttributesQuoted($this->getAttributes());
3238 function getAttributesQuoted($attributes_array)
3241 $attributes_array = $this->getSanitizedConditionsArray($attributes_array);
3242 foreach (array_diff($attributes_array,array('')) as $k=>$v){
3243 $set[$k] = $k.'='.$v;
3249 function getColumnType($column_name)
3251 empty($this->_columns
) ?
$this->getColumns() : null;
3252 return empty($this->_columns
[$column_name]['type']) ?
false : $this->_columns
[$column_name]['type'];
3256 function castAttributeForDatabase($column_name, $value, $add_quotes = true)
3258 $result = '';//$add_quotes ? "''" : ''; // $result = $add_quotes ? $this->_db->qstr('') : '';
3259 switch ($this->getColumnType($column_name)) {
3262 $date_time = $this->_db
->DBTimeStamp($this->getValueForDateColumn($column_name, $value));
3263 $result = $add_quotes ?
$date_time : trim($date_time ,"'");
3271 $date = $this->_db
->DBDate($this->getValueForDateColumn($column_name, $value));
3272 $result = $add_quotes ?
$date : trim($date, "'");
3279 $result = !empty($value) ?
'1' : '0';
3284 if($this->_getDatabaseType() == 'postgre'){
3285 $result = " '".$this->_db
->BlobEncode($value)."'::bytea ";
3287 $result = $add_quotes ?
$this->_db
->qstr($value) : $value;
3294 $value = is_null($value) ?
null : (integer)$value;
3296 $result = (empty($value) && $value !== 0) ?
'null' : (is_numeric($value) ?
$value : $this->_db
->qstr($value));
3297 $result = !empty($this->_columns
[$column_name]['notNull']) && $result == 'null' && $this->_getDatabaseType() == 'sqlite' ?
'0' : $result;
3301 $result = $add_quotes ?
$this->_db
->qstr($value) : $value;
3305 return empty($this->_columns
[$column_name]['notNull']) ?
($result === '' ?
"''" : $result) : ($result == 'null' ?
'' : $result);
3308 function castAttributeFromDatabase($column_name, $value)
3310 if($this->hasColumn($column_name)){
3311 $column_type = $this->getColumnType($column_name);
3313 if('integer' == $column_type){
3314 return is_null($value) ?
null : (integer)$value;
3315 }elseif('boolean' == $column_type){
3316 return (integer)$value === 1 ?
true : false;
3317 }elseif(!empty($value) && 'date' == $column_type && strstr(trim($value),' ')){
3318 return substr($value,0,10) == '0000-00-00' ?
null : str_replace(substr($value,strpos($value,' ')), '', $value);
3319 }elseif (!empty($value) && 'datetime' == $column_type && substr($value,0,10) == '0000-00-00'){
3321 }elseif ('binary' == $column_type && $this->_getDatabaseType() == 'postgre'){
3322 return $this->_db
->BlobDecode($value);
3331 * Joins date arguments into a single attribute. Like the array generated by the date_helper, so
3332 * array('published_on(1i)' => 2002, 'published_on(2i)' => 'January', 'published_on(3i)' => 24)
3333 * Will be converted to array('published_on'=>'2002-01-24')
3337 function _castDateParametersFromDateHelper_(&$params)
3342 $date_attributes = array();
3343 foreach ($params as $k=>$v) {
3344 if(preg_match('/^([A-Za-z0-9_]+)\(([1-5]{1})i\)$/',$k,$match)){
3345 $date_attributes[$match[1]][$match[2]] = $v;
3349 foreach ($date_attributes as $attribute=>$date){
3350 $params[$attribute] = Ak
::getDate(Ak
::getTimestamp(trim(@$date[1].'-'.@$date[2].'-'.@$date[3].' '.@$date[4].':'.@$date[5].':'.@$date[6],' :-')));
3359 function _addBlobQueryStack($column_name, $blob_value)
3361 $this->_BlobQueryStack
[$column_name] = $blob_value;
3367 function _updateBlobFields($condition)
3369 if(!empty($this->_BlobQueryStack
) && is_array($this->_BlobQueryStack
)){
3370 foreach ($this->_BlobQueryStack
as $column=>$value){
3371 $this->_db
->UpdateBlob($this->getTableName(), $column, $value, $condition);
3373 $this->_BlobQueryStack
= null;
3379 function getValueForDateColumn($column_name, $value)
3381 if(!$this->isNewRecord()){
3382 if(($column_name == 'updated_on' ||
$column_name == 'updated_at') && empty($value)){
3385 }elseif(($column_name == 'created_on' ||
$column_name == 'created_at') && empty($value)){
3387 }elseif($column_name == 'expires_on' ||
$column_name == 'expires_at'){
3388 return empty($value) ? Ak
::getTimestamp('9999-12-31 23:59:59') : Ak
::getTimestamp($value);
3391 return Ak
::getTimestamp($value);
3398 ====================================================================
3400 * Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
3401 * record increments the lock_version column and the locking facilities ensure that records instantiated twice
3402 * will let the last one saved return false on save() if the first was also updated. Example:
3404 * $p1 = new Person(1);
3405 * $p2 = new Person(1);
3407 * $p1->first_name = "Michael";
3410 * $p2->first_name = "should fail";
3411 * $p2->save(); // Returns false
3413 * You're then responsible for dealing with the conflict by checking the return value of save(); and either rolling back, merging,
3414 * or otherwise apply the business logic needed to resolve the conflict.
3416 * You must ensure that your database schema defaults the lock_version column to 0.
3418 * This behavior can be turned off by setting <tt>AkActiveRecord::lock_optimistically = false</tt>.
3420 function isLockingEnabled()
3422 return ((isset($this->lock_optimistically
) && $this->lock_optimistically
!== false) ||
!isset($this->lock_optimistically
)) && $this->hasColumn('lock_version');
3424 /*/Optimistic Locking*/
3429 ====================================================================
3430 See also: Observers.
3432 * Callbacks are hooks into the life-cycle of an Active Record object that allows you to trigger logic
3433 * before or after an alteration of the object state. This can be used to make sure that associated and
3434 * dependent objects are deleted when destroy is called (by overwriting beforeDestroy) or to massage attributes
3435 * before they're validated (by overwriting beforeValidation). As an example of the callbacks initiated, consider
3436 * the AkActiveRecord->save() call:
3439 * - (-) needsValidation()
3440 * - (1) beforeValidation()
3441 * - (2) beforeValidationOnCreate() / beforeValidationOnUpdate()
3443 * - (-) validateOnCreate()
3444 * - (4) afterValidation()
3445 * - (5) afterValidationOnCreate() / afterValidationOnUpdate()
3446 * - (6) beforeSave()
3447 * - (7) beforeCreate() / beforeUpdate()
3449 * - (8) afterCreate() / afterUpdate()
3451 * - (10) afterDestroy()
3452 * - (11) beforeDestroy()
3455 * That's a total of 15 callbacks, which gives you immense power to react and prepare for each state in the
3456 * Active Record lifecycle.
3459 * class CreditCard extends ActiveRecord
3461 * // Strip everything but digits, so the user can specify "555 234 34" or
3462 * // "5552-3434" or both will mean "55523434"
3463 * function beforeValidationOnCreate
3465 * if(!empty($this->number)){
3466 * $this->number = ereg_replace('[^0-9]*','',$this->number);
3471 * class Subscription extends ActiveRecord
3473 * // Note: This is not implemented yet
3474 * var $beforeCreate = 'recordSignup';
3476 * function recordSignup()
3478 * $this->signed_up_on = date("Y-m-d");
3482 * class Firm extends ActiveRecord
3484 * //Destroys the associated clients and people when the firm is destroyed
3485 * // Note: This is not implemented yet
3486 * var $beforeDestroy = array('destroyAssociatedPeople', 'destroyAssociatedClients');
3488 * function destroyAssociatedPeople()
3490 * $Person = new Person();
3491 * $Person->destroyAll("firm_id=>", $this->id);
3494 * function destroyAssociatedClients()
3496 * $Client = new Client();
3497 * $Client->destroyAll("client_of=>", $this->id);
3502 * == Canceling callbacks ==
3504 * If a before* callback returns false, all the later callbacks and the associated action are cancelled. If an after* callback returns
3505 * false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
3506 * defined as methods on the model, which are called last.
3508 * Override this methods to hook Active Records
3513 function beforeCreate(){return true;}
3514 function beforeValidation(){return true;}
3515 function beforeValidationOnCreate(){return true;}
3516 function beforeValidationOnUpdate(){return true;}
3517 function beforeSave(){return true;}
3518 function beforeUpdate(){return true;}
3519 function afterUpdate(){return true;}
3520 function afterValidation(){return true;}
3521 function afterValidationOnCreate(){return true;}
3522 function afterValidationOnUpdate(){return true;}
3523 function afterCreate(){return true;}
3524 function afterDestroy(){return true;}
3525 function beforeDestroy(){return true;}
3526 function afterSave(){return true;}
3533 ====================================================================
3535 * Transaction support for database operations
3537 * Transactions are enabled automatically for Active record objects, But you can nest transactions within models.
3538 * This transactions are nested, and only the outermost will be executed
3540 * $User->transactionStart();
3541 * $User->create('username'=>'Bermi');
3542 * $Members->create('username'=>'Bermi');
3544 * if(!checkSomething()){
3545 * $User->transactionFail();
3548 * $User->transactionComplete();
3551 function transactionStart()
3553 if(!$this->isConnected()){
3554 $this->setConnection();
3556 return $this->_db
->StartTrans();
3559 function transactionComplete()
3561 return $this->_db
->CompleteTrans();
3564 function transactionFail()
3566 return $this->_db
->FailTrans();
3569 function transactionHasFailed()
3571 return $this->_db
->HasFailedTrans();
3581 ====================================================================
3582 See also: Error Handling.
3584 * Active Records implement validation by overwriting AkActiveRecord::validate (or the variations, validateOnCreate and
3585 * validateOnUpdate). Each of these methods can inspect the state of the object, which usually means ensuring
3586 * that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
3590 * class Person extends ActiveRecord
3592 * function validate()
3594 * $this->addErrorOnEmpty(array('first_name', 'last_name'));
3595 * if(!preg_match('/[0-9]{4,12}/', $this->phone_number)){
3596 * $this->addError("phone_number", "has invalid format");
3600 * function validateOnCreate() // is only run the first time a new object is saved
3602 * if(!isValidDiscount($this->membership_discount)){
3603 * $this->addError("membership_discount", "has expired");
3607 * function validateOnUpdate()
3609 * if($this->countChangedAttributes() == 0){
3610 * $this->addErrorToBase("No changes have occurred");
3615 * $Person = new Person(array("first_name" => "David", "phone_number" => "what?"));
3616 * $Person->save(); // => false (and doesn't do the save);
3617 * $Person->hasErrors(); // => false
3618 * $Person->countErrors(); // => 2
3619 * $Person->getErrorsOn("last_name"); // => "can't be empty"
3620 * $Person->getErrorsOn("phone_number"); // => "has invalid format"
3621 * $Person->yieldEachFullError(); // => "Last name can't be empty \n Phone number has invalid format"
3623 * $Person->setAttributes(array("last_name" => "Heinemeier", "phone_number" => "555-555"));
3624 * $Person->save(); // => true (and person is now saved in the database)
3626 * An "_errors" array is available for every Active Record.
3631 * Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
3634 * class Person extends ActiveRecord
3636 * function validate()
3638 * $this->validatesConfirmationOf('password');
3639 * $this->validatesConfirmationOf('email_address', "should match confirmation");
3644 * <?=$form_helper->password_field("person", "password"); ?>
3645 * <?=$form_helper->password_field("person", "password_confirmation"); ?>
3647 * The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual.
3648 * It exists only as an in-memory variable for validating the password. This check is performed only if password_confirmation
3652 function validatesConfirmationOf($attribute_names, $message = 'confirmation')
3654 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3655 $attribute_names = Ak
::toArray($attribute_names);
3656 foreach ($attribute_names as $attribute_name){
3657 $attribute_accessor = $attribute_name.'_confirmation';
3658 if(isset($this->$attribute_accessor) && @$this->$attribute_accessor != @$this->$attribute_name){
3659 $this->addError($attribute_name, $message);
3665 * Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
3667 * class Person extends ActiveRecord
3669 * function validateOnCreate()
3671 * $this->validatesAcceptanceOf('terms_of_service');
3672 * $this->validatesAcceptanceOf('eula', "must be abided");
3676 * The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed only if
3677 * terms_of_service is not null.
3681 * Specifies value that is considered accepted. The default value is a string "1", which makes it easy to relate to an HTML checkbox.
3683 function validatesAcceptanceOf($attribute_names, $message = 'accepted', $accept = 1)
3685 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3687 $attribute_names = Ak
::toArray($attribute_names);
3688 foreach ($attribute_names as $attribute_name){
3689 if(@$this->$attribute_name != $accept){
3690 $this->addError($attribute_name, $message);
3696 * Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
3698 * class Book extends ActiveRecord
3700 * var $has_many = 'pages';
3701 * var $belongs_to = 'library';
3703 * function validate(){
3704 * $this->validatesAssociated(array('pages', 'library'));
3709 * Warning: If, after the above definition, you then wrote:
3711 * class Page extends ActiveRecord
3713 * var $belongs_to = 'book';
3714 * function validate(){
3715 * $this->validatesAssociated('book');
3719 * ...this would specify a circular dependency and cause infinite recursion.
3721 * NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
3722 * is both present and guaranteed to be valid, you also need to use validatesPresenceOf.
3724 function validatesAssociated($attribute_names, $message = 'invalid')
3726 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $messa;
3727 $attribute_names = Ak
::toArray($attribute_names);
3728 foreach ($attribute_names as $attribute_name){
3729 if(!empty($this->$attribute_name)){
3730 if(is_array($this->$attribute_name)){
3731 foreach(array_keys($this->$attribute_name) as $k){
3732 if(method_exists($this->{$attribute_name}[$k],'isValid') && !$this->{$attribute_name}[$k]->isValid()){
3733 $this->addError($attribute_name, $message);
3736 }elseif (method_exists($this->$attribute_name,'isValid') && !$this->$attribute_name->isValid()){
3737 $this->addError($attribute_name, $message);
3743 function isBlank($value = null)
3745 return trim((string)$value) == '';
3749 * Validates that the specified attributes are not blank (as defined by AkActiveRecord::isBlank()).
3751 function validatesPresenceOf($attribute_names, $message = 'blank')
3753 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
3755 $attribute_names = Ak
::toArray($attribute_names);
3756 foreach ($attribute_names as $attribute_name){
3757 $this->addErrorOnBlank($attribute_name, $message);
3762 * Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
3764 * class Person extends ActiveRecord
3766 * function validate()
3768 * $this->validatesLengthOf('first_name', array('maximum'=>30));
3769 * $this->validatesLengthOf('last_name', array('maximum'=>30,'message'=> "less than %d if you don't mind"));
3770 * $this->validatesLengthOf('last_name', array('within'=>array(7, 32)));
3771 * $this->validatesLengthOf('last_name', array('in'=>array(6, 20), 'too_long' => "pick a shorter name", 'too_short' => "pick a longer name"));
3772 * $this->validatesLengthOf('fav_bra_size', array('minimum'=>1, 'too_short'=>"please enter at least %d character"));
3773 * $this->validatesLengthOf('smurf_leader', array('is'=>4, 'message'=>"papa is spelled with %d characters... don't play me."));
3777 * 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.
3779 * Configuration options:
3780 * <tt>minimum</tt> - The minimum size of the attribute
3781 * <tt>maximum</tt> - The maximum size of the attribute
3782 * <tt>is</tt> - The exact size of the attribute
3783 * <tt>within</tt> - A range specifying the minimum and maximum size of the attribute
3784 * <tt>in</tt> - A synonym(or alias) for :within
3785 * <tt>allow_null</tt> - Attribute may be null; skip validation.
3787 * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default "is" "is too long (max is %d characters)")
3788 * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default "is" "is too short (min is %d characters)")
3789 * <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)")
3790 * <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
3792 function validatesLengthOf($attribute_names, $options = array())
3794 // Merge given options with defaults.
3795 $default_options = array(
3796 'too_long' => $this->_defaultErrorMessages
['too_long'],
3797 'too_short' => $this->_defaultErrorMessages
['too_short'],
3798 'wrong_length' => $this->_defaultErrorMessages
['wrong_length'],
3799 'allow_null' => false
3802 $range_options = array();
3803 foreach ($options as $k=>$v){
3804 if(in_array($k,array('minimum','maximum','is','in','within'))){
3805 $range_options[$k] = $v;
3811 // Ensure that one and only one range option is specified.
3812 switch (count($range_options)) {
3814 trigger_error(Ak
::t('Range unspecified. Specify the "within", "maximum", "minimum, or "is" option.'), E_USER_ERROR
);
3818 $options = array_merge($default_options, $options);
3821 trigger_error(Ak
::t('Too many range options specified. Choose only one.'), E_USER_ERROR
);
3830 if(empty($option_value) ||
!is_array($option_value) ||
count($option_value) != 2 ||
!is_numeric($option_value[0]) ||
!is_numeric($option_value[1])){
3831 trigger_error(Ak
::t('%option must be a Range (array(min, max))',array('%option',$option)), E_USER_ERROR
);
3834 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
3835 foreach ($attribute_names as $attribute_name){
3836 if((!empty($option['allow_null']) && !isset($this->$attribute_name)) ||
(Ak
::size($this->$attribute_name)) < $option_value[0]){
3837 $this->addError($attribute_name, sprintf($options['too_short'], $option_value[0]));
3838 }elseif((!empty($option['allow_null']) && !isset($this->$attribute_name)) ||
(Ak
::size($this->$attribute_name)) > $option_value[1]){
3839 $this->addError($attribute_name, sprintf($options['too_long'], $option_value[1]));
3848 if(empty($option_value) ||
!is_numeric($option_value) ||
$option_value <= 0){
3849 trigger_error(Ak
::t('%option must be a nonnegative Integer',array('%option',$option_value)), E_USER_ERROR
);
3853 // Declare different validations per option.
3854 $validity_checks = array('is' => "==", 'minimum' => ">=", 'maximum' => "<=");
3855 $message_options = array('is' => 'wrong_length', 'minimum' => 'too_short', 'maximum' => 'too_long');
3857 $message = sprintf(!empty($options['message']) ?
$options['message'] : $options[$message_options[$option]],$option_value);
3859 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
3860 foreach ($attribute_names as $attribute_name){
3861 if((!$options['allow_null'] && !isset($this->$attribute_name)) ||
3862 eval("return !(".Ak
::size(@$this->$attribute_name)." {$validity_checks[$option]} $option_value);")){
3863 $this->addError($attribute_name, $message);
3874 function validatesSizeOf($attribute_names, $options = array())
3876 return validatesLengthOf($attribute_names, $options);
3880 * Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
3881 * can be named "davidhh".
3883 * class Person extends ActiveRecord
3885 * function validate()
3887 * $this->validatesUniquenessOf('passport_number');
3888 * $this->validatesUniquenessOf('user_name', array('scope' => "account_id"));
3892 * It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
3893 * making sure that a teacher can only be on the schedule once per semester for a particular class.
3895 * class TeacherSchedule extends ActiveRecord
3897 * function validate()
3899 * $this->validatesUniquenessOf('passport_number');
3900 * $this->validatesUniquenessOf('teacher_id', array('scope' => array("semester_id", "class_id"));
3905 * 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
3906 * attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
3908 * Configuration options:
3909 * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
3910 * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope"
3911 * <tt>case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (true by default).
3912 * <tt>if</tt> - Specifies a method to call or a string to evaluate to determine if the validation should
3913 * occur (e.g. 'if' => 'allowValidation', or 'if' => '$this->signup_step > 2'). The
3914 * method, or string should return or evaluate to a true or false value.
3916 function validatesUniquenessOf($attribute_names, $options = array())
3918 $default_options = array('case_sensitive'=>true, 'message'=>'taken');
3919 $options = array_merge($default_options, $options);
3921 if(!empty($options['if'])){
3922 if(method_exists($this,$options['if'])){
3923 if($this->{$options['if']}() === false){
3927 eval('$__eval_result = ('.rtrim($options['if'],';').');');
3928 if(empty($__eval_result)){
3934 $message = isset($this->_defaultErrorMessages
[$options['message']]) ?
$this->t($this->_defaultErrorMessages
[$options['message']]) : $options['message'];
3935 unset($options['message']);
3937 foreach ((array)$attribute_names as $attribute_name){
3938 $value = isset($this->$attribute_name) ?
$this->$attribute_name : null;
3940 if($value === null ||
($options['case_sensitive'] ||
!$this->hasColumn($attribute_name))){
3941 $condition_sql = $this->getTableName().'.'.$attribute_name.' '.$this->getAttributeCondition($value);
3942 $condition_params = array($value);
3944 $condition_sql = 'LOWER('.$this->getTableName().'.'.$attribute_name.') '.$this->getAttributeCondition($value);
3945 $condition_params = array(is_array($value) ?
array_map('utf8_strtolower',$value) : utf8_strtolower($value));
3948 if(!empty($options['scope'])){
3949 foreach ((array)$options['scope'] as $scope_item){
3950 $scope_value = $this->get($scope_item);
3951 $condition_sql .= ' AND '.$this->getTableName().'.'.$scope_item.' '.$this->getAttributeCondition($scope_value);
3952 $condition_params[] = $scope_value;
3956 if(!$this->isNewRecord()){
3957 $condition_sql .= ' AND '.$this->getTableName().'.'.$this->getPrimaryKey().' <> ?';
3958 $condition_params[] = $this->getId();
3960 array_unshift($condition_params,$condition_sql);
3961 if ($this->find('first', array('conditions' => $condition_params))){
3962 $this->addError($attribute_name, $message);
3970 * Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
3974 * class Person extends ActiveRecord
3976 * function validate()
3978 * $this->validatesFormatOf('email', "/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/");
3983 * A regular expression must be provided or else an exception will be raised.
3985 * There are some regular expressions bundled with the Akelos Framework.
3986 * 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.
3987 * This are predefined perl-like regular extensions.
3989 * * AK_NOT_EMPTY_REGULAR_EXPRESSION ---> /.+/
3990 * * 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
3991 * * AK_NUMBER_REGULAR_EXPRESSION ---> /^[0-9]+$/
3992 * * AK_PHONE_REGULAR_EXPRESSION ---> /^([\+]?[(]?[\+]?[ ]?[0-9]{2,3}[)]?[ ]?)?[0-9 ()\-]{4,25}$/
3993 * * 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}$/
3994 * * 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])$/
3995 * * AK_POST_CODE_REGULAR_EXPRESSION ---> /^[0-9A-Za-z -]{2,7}$/
3997 * 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.
4000 * <tt>$message</tt> - A custom error message (default is: "is invalid")
4001 * <tt>$regular_expression</tt> - The regular expression used to validate the format with (note: must be supplied!)
4003 function validatesFormatOf($attribute_names, $regular_expression, $message = 'invalid', $regex_function = 'preg_match')
4005 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
4007 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
4008 foreach ($attribute_names as $attribute_name){
4009 if(!isset($this->$attribute_name) ||
!$regex_function($regular_expression, $this->$attribute_name)){
4010 $this->addError($attribute_name, $message);
4016 * Validates whether the value of the specified attribute is available in a particular array of elements.
4018 * class Person extends ActiveRecord
4020 * function validate()
4022 * $this->validatesInclusionOf('gender', array('male', 'female'), "woah! what are you then!??!!");
4023 * $this->validatesInclusionOf('age', range(0, 99));
4027 * <tt>$array_of_ possibilities</tt> - An array of available items
4028 * <tt>$message</tt> - Specifies a customer error message (default is: "is not included in the list")
4029 * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false)
4031 function validatesInclusionOf($attribute_names, $array_of_possibilities, $message = 'inclusion', $allow_null = false)
4033 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
4035 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
4036 foreach ($attribute_names as $attribute_name){
4037 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 )){
4038 $this->addError($attribute_name, $message);
4044 * Validates that the value of the specified attribute is not in a particular array of elements.
4046 * class Person extends ActiveRecord
4048 * function validate()
4050 * $this->validatesExclusionOf('username', array('admin', 'superuser'), "You don't belong here");
4051 * $this->validatesExclusionOf('age', range(30,60), "This site is only for under 30 and over 60");
4056 * <tt>$array_of_possibilities</tt> - An array of items that the value shouldn't be part of
4057 * <tt>$message</tt> - Specifies a customer error message (default is: "is reserved")
4058 * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false)
4060 function validatesExclusionOf($attribute_names, $array_of_possibilities, $message = 'exclusion', $allow_null = false)
4062 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
4064 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
4065 foreach ($attribute_names as $attribute_name){
4067 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 )){
4068 $this->addError($attribute_name, $message);
4077 * Validates whether the value of the specified attribute is numeric.
4079 * class Person extends ActiveRecord
4081 * function validate()
4083 * $this->validatesNumericalityOf('value');
4088 * <tt>$message</tt> - A custom error message (default is: "is not a number")
4089 * <tt>$only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
4090 * <tt>$allow_null</tt> Skip validation if attribute is null (default is false).
4092 function validatesNumericalityOf($attribute_names, $message = 'not_a_number', $only_integer = false, $allow_null = false)
4094 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
4096 $attribute_names = is_array($attribute_names) ?
$attribute_names : array($attribute_names);
4097 foreach ($attribute_names as $attribute_name){
4101 isset($this->$attribute_name) ?
4104 !is_integer(@$this->$attribute_name) :
4105 !is_numeric(@$this->$attribute_name)
4110 isset($this->$attribute_name) ?
4111 ($only_integer ?
!is_integer(@$this->$attribute_name) : !is_numeric(@$this->$attribute_name)) :
4116 $this->addError($attribute_name, $message);
4124 * Returns true if no errors were added otherwise false.
4128 if($this->beforeValidation() && $this->notifyObservers('beforeValidation')){
4130 $this->clearErrors();
4132 if($this->_set_default_attribute_values_automatically
){
4133 $this->_setDefaultAttributeValuesAutomatically();
4138 if($this->_automated_validators_enabled
){
4139 $this->_runAutomatedValidators();
4142 if ($this->isNewRecord()){
4143 if($this->beforeValidationOnCreate()){
4144 $this->notifyObservers('beforeValidationOnCreate');
4145 $this->validateOnCreate();
4146 $this->notifyObservers('afterValidationOnCreate');
4149 if($this->beforeValidationOnUpdate()){
4150 $this->notifyObservers('beforeValidationOnUpdate');
4151 $this->validateOnUpdate();
4152 $this->notifyObservers('afterValidationOnUpdate');
4155 $this->notifyObservers('afterValidation');
4158 return !$this->hasErrors();
4162 * By default the Active Record will validate for the maximum length for database columns. You can
4163 * disable the automated validators by setting $this->_automated_validators_enabled to false.
4164 * Specific validators are (for now):
4165 * $this->_automated_max_length_validator = true; // true by default, but you can set it to false on your model
4166 * $this->_automated_not_null_validator = false; // disabled by default
4170 function _runAutomatedValidators()
4172 foreach ($this->_columns
as $column_name=>$column_settings){
4173 if($this->_automated_max_length_validator
&&
4174 empty($column_settings['primaryKey']) &&
4175 !empty($this->$column_name) &&
4176 !empty($column_settings['maxLength']) &&
4177 $column_settings['maxLength'] > 0 &&
4178 strlen($this->$column_name) > $column_settings['maxLength']){
4179 $this->addError($column_name, sprintf($this->_defaultErrorMessages
['too_long'], $column_settings['maxLength']));
4180 }elseif($this->_automated_not_null_validator
&& empty($column_settings['primaryKey']) && !empty($column_settings['notNull']) && (!isset($this->$column_name) ||
is_null($this->$column_name))){
4181 $this->addError($column_name,'empty');
4187 * $this->_set_default_attribute_values_automatically = true; // This enables automated attribute setting from database definition
4191 function _setDefaultAttributeValuesAutomatically()
4193 foreach ($this->_columns
as $column_name=>$column_settings){
4194 if(empty($column_settings['primaryKey']) && isset($column_settings['hasDefault']) && $column_settings['hasDefault'] && (!isset($this->$column_name) ||
is_null($this->$column_name))){
4195 if(empty($column_settings['defaultValue'])){
4196 if($column_settings['type'] == 'integer' && empty($column_settings['notNull'])){
4197 $this->$column_name = 0;
4198 }elseif(($column_settings['type'] == 'string' ||
$column_settings['type'] == 'text') && empty($column_settings['notNull'])){
4199 $this->$column_name = '';
4202 $this->$column_name = $column_settings['defaultValue'];
4209 * Overwrite this method for validation checks on all saves and use addError($field, $message); for invalid attributes.
4216 * Overwrite this method for validation checks used only on creation.
4218 function validateOnCreate()
4223 * Overwrite this method for validation checks used only on updates.
4225 function validateOnUpdate()
4234 ====================================================================
4235 See also: Callbacks.
4239 * $state store the state of this observable object
4243 var $_observable_state;
4248 function _instatiateDefaultObserver()
4250 $default_observer_name = ucfirst($this->getModelName().'Observer');
4251 if(class_exists($default_observer_name)){
4252 //$Observer =& new $default_observer_name($this);
4253 Ak
::singleton($default_observer_name, $this);
4258 * Calls the $method using the reference to each
4259 * registered observer.
4260 * @return true (this is used internally for triggering observers on default callbacks)
4262 function notifyObservers ($method = null)
4264 $observers =& $this->getObservers();
4265 $observer_count = count($observers);
4267 if(!empty($method)){
4268 $this->setObservableState($method);
4271 $model_name = $this->getModelName();
4272 for ($i=0; $i<$observer_count; $i++
) {
4273 if(in_array($model_name, $observers[$i]->_observing
)){
4274 if(method_exists($observers[$i], $method)){
4275 $observers[$i]->$method($this);
4277 $observers[$i]->update($this->getObservableState(), &$this);
4280 $observers[$i]->update($this->getObservableState(), &$this);
4283 $this->setObservableState('');
4289 function setObservableState($state_message)
4291 $this->_observable_state
= $state_message;
4294 function getObservableState()
4296 return $this->_observable_state
;
4300 * Register the reference to an object object
4303 function &addObserver(&$observer)
4305 static $observers, $registered_observers;
4306 $observer_class_name = get_class($observer);
4307 if(!isset($registered_observers[$observer_class_name]) && func_num_args() == 1){
4308 $observers[] =& $observer;
4309 $registered_observers[$observer_class_name] = count($observers);
4315 * Register the reference to an object object
4318 function &getObservers()
4320 $observers =& $this->addObserver(&$this, false);
4331 ====================================================================
4332 See also: Validators.
4337 * Returns the Errors array that holds all information about attribute error messages.
4339 function getErrors()
4341 return $this->_errors
;
4345 * Adds an error to the base object instead of any particular attribute. This is used
4346 * to report errors that doesn't tie to any specific attribute, but rather to the object
4347 * as a whole. These error messages doesn't get prepended with any field name when iterating
4348 * with yieldEachFullError, so they should be complete sentences.
4350 function addErrorToBase($message)
4352 $this->addError($this->getModelName(), $message);
4356 * Returns errors assigned to base object through addToBase according to the normal rules of getErrorsOn($attribute).
4358 function getBaseErrors()
4360 $errors = $this->getErrors();
4361 return (array)@$errors[$this->getModelName()];
4366 * Adds an error message ($message) to the ($attribute), which will be returned on a call to <tt>getErrorsOn($attribute)</tt>
4367 * for the same attribute and ensure that this error object returns false when asked if <tt>hasErrors</tt>. More than one
4368 * error can be added to the same $attribute in which case an array will be returned on a call to <tt>getErrorsOn($attribute)</tt>.
4369 * If no $message is supplied, "invalid" is assumed.
4371 function addError($attribute, $message = 'invalid')
4373 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
4374 $this->_errors
[$attribute][] = $message;
4378 * Will add an error message to each of the attributes in $attributes that is empty.
4380 function addErrorOnEmpty($attribute_names, $message = 'empty')
4382 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
4383 $attribute_names = Ak
::toArray($attribute_names);
4384 foreach ($attribute_names as $attribute){
4385 if(empty($this->$attribute)){
4386 $this->addError($attribute, $message);
4392 * Will add an error message to each of the attributes in $attributes that is blank (using $this->isBlank).
4394 function addErrorOnBlank($attribute_names, $message = 'blank')
4396 $message = isset($this->_defaultErrorMessages
[$message]) ?
$this->t($this->_defaultErrorMessages
[$message]) : $message;
4397 $attribute_names = Ak
::toArray($attribute_names);
4398 foreach ($attribute_names as $attribute){
4399 if($this->isBlank(@$this->$attribute)){
4400 $this->addError($attribute, $message);
4406 * Will add an error message to each of the attributes in $attributes that has a length outside of the passed boundary $range.
4407 * If the length is above the boundary, the too_long_message message will be used. If below, the too_short_message.
4409 function addErrorOnBoundaryBreaking($attribute_names, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short')
4411 $too_long_message = isset($this->_defaultErrorMessages
[$too_long_message]) ?
$this->_defaultErrorMessages
[$too_long_message] : $too_long_message;
4412 $too_short_message = isset($this->_defaultErrorMessages
[$too_short_message]) ?
$this->_defaultErrorMessages
[$too_short_message] : $too_short_message;
4414 $attribute_names = Ak
::toArray($attribute_names);
4415 foreach ($attribute_names as $attribute){
4416 if(@$this->$attribute < $range_begin){
4417 $this->addError($attribute, $too_short_message);
4419 if(@$this->$attribute > $range_end){
4420 $this->addError($attribute, $too_long_message);
4426 function addErrorOnBoundryBreaking ($attributes, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short')
4428 $this->addErrorOnBoundaryBreaking($attributes, $range_begin, $range_end, $too_long_message, $too_short_message);
4432 * Returns true if the specified $attribute has errors associated with it.
4434 function isInvalid($attribute)
4436 return $this->getErrorsOn($attribute);
4440 * Returns false, if no errors are associated with the specified $attribute.
4441 * Returns the error message, if one error is associated with the specified $attribute.
4442 * Returns an array of error messages, if more than one error is associated with the specified $attribute.
4444 function getErrorsOn($attribute)
4446 if (empty($this->_errors
[$attribute])){
4448 }elseif (count($this->_errors
[$attribute]) == 1){
4449 $k = array_keys($this->_errors
[$attribute]);
4450 return $this->_errors
[$attribute][$k[0]];
4452 return $this->_errors
[$attribute];
4458 * Yields each attribute and associated message per error added.
4460 function yieldEachError()
4462 foreach ($this->_errors
as $errors){
4463 foreach ($errors as $error){
4464 $this->yieldError($error);
4469 function yieldError($message)
4471 $messages = is_array($message) ?
$message : array($message);
4472 foreach ($messages as $message){
4473 echo "<div class='error'><p>$message</p></div>\n";
4479 * Yields each full error message added. So Person->addError("first_name", "can't be empty") will be returned
4480 * through iteration as "First name can't be empty".
4482 function yieldEachFullError()
4484 $full_messages = $this->getFullErrorMessages();
4485 foreach ($full_messages as $full_message){
4486 $this->yieldError($full_message);
4492 * Returns all the full error messages in an array.
4494 function getFullErrorMessages()
4496 $full_messages = array();
4498 foreach ($this->_errors
as $attribute=>$errors){
4499 $full_messages[$attribute] = array();
4500 foreach ($errors as $error){
4501 $full_messages[$attribute][] = $this->t('%attribute_name %error', array(
4502 '%attribute_name'=>AkInflector
::humanize($this->_delocalizeAttribute($attribute)),
4507 return $full_messages;
4511 * Returns true if no errors have been added.
4513 function hasErrors()
4515 return !empty($this->_errors
);
4519 * Removes all the errors that have been added.
4521 function clearErrors()
4523 $this->_errors
= array();
4527 * Returns the total number of errors added. Two errors added to the same attribute will be counted as such
4528 * with this as well.
4530 function countErrors()
4533 foreach ($this->_errors
as $errors){
4534 $error_count = count($errors)+
$error_count;
4537 return $error_count;
4541 function errorsToString($print = false)
4543 $result = "\n<div id='errors'>\n<ul class='error'>\n";
4544 foreach ($this->getFullErrorMessages() as $error){
4545 $result .= is_array($error) ?
"<li class='error'>".join('</li><li class=\'error\'>',$error)."</li>\n" : "<li class='error'>$error</li>\n";
4547 $result .= "</ul>\n</div>\n";
4561 ====================================================================
4562 See also: Acts as List, Acts as Tree, Acts as Nested Set.
4566 * actAs provides a method for extending Active Record models.
4569 * $this->actsAs('list', array('scope' => 'todo_list'));
4571 function actsAs($behaviour, $options = array())
4573 $class_name = $this->_getActAsClassName($behaviour);
4574 $underscored_place_holder = AkInflector
::underscore($behaviour);
4575 $camelized_place_holder = AkInflector
::camelize($underscored_place_holder);
4577 if($this->$underscored_place_holder =& $this->_getActAsInstance($class_name, $options)){
4578 $this->$camelized_place_holder =& $this->$underscored_place_holder;
4579 if($this->$underscored_place_holder->init($options)){
4580 $this->__ActsLikeAttributes
[$underscored_place_holder] = $underscored_place_holder;
4588 function _getActAsClassName($behaviour)
4590 $class_name = AkInflector
::camelize($behaviour);
4591 return file_exists(AK_LIB_DIR
.DS
.'AkActiveRecord'.DS
.'AkActsAs'.$class_name.'.php') && !class_exists('ActsAs'.$class_name) ?
4592 'AkActsAs'.$class_name : 'ActsAs'.$class_name;
4598 function &_getActAsInstance($class_name, $options)
4600 if(!class_exists($class_name)){
4601 if(substr($class_name,0,2) == 'Ak'){
4602 include_once(AK_LIB_DIR
.DS
.'AkActiveRecord'.DS
.$class_name.'.php');
4604 include_once(AK_APP_PLUGINS_DIR
.DS
.AkInflector
::underscore($class_name).DS
.'lib'.DS
.$class_name.'.php');
4607 if(!class_exists($class_name)){
4608 trigger_error(Ak
::t('The class %class used for handling an "act_as %class" does not exist',array('%class'=>$class_name)), E_USER_ERROR
);
4612 $ActAsInstance =& new $class_name($this, $options);
4613 return $ActAsInstance;
4620 function _loadActAsBehaviours()
4622 $this->act_as
= !empty($this->acts_as
) ?
$this->acts_as
: (empty($this->act_as
) ?
false : $this->act_as
);
4623 if(!empty($this->act_as
)){
4624 if(is_string($this->act_as
)){
4625 $this->act_as
= array_unique(array_diff(array_map('trim',explode(',',$this->act_as
.',')), array('')));
4626 foreach ($this->act_as
as $type){
4627 $this->actsAs($type);
4629 }elseif (is_array($this->act_as
)){
4630 foreach ($this->act_as
as $type=>$options){
4631 $this->actsAs($type, $options);
4638 * Returns a comma separated list of possible acts like (active record, nested set, list)....
4642 $result = 'active record';
4643 foreach ($this->__ActsLikeAttributes
as $type){
4644 if(!empty($this->$type) && is_object($this->$type) && method_exists($this->{$type}, 'getType')){
4645 $result .= ','.$this->{$type}->getType();
4651 /*/Act as Behaviours*/
4655 ====================================================================
4660 if(!$this->isConnected()){
4661 $this->setConnection();
4663 $this->_db
->debug
= $this->_db
->debug ?
false : true;
4664 $this->db_debug
=& $this->_db
->debug
;
4667 function toString($print = false)
4670 if(!AK_CLI ||
(AK_ENVIRONMENT
== 'testing' && !AK_CLI
)){
4671 $result = "<h2>Details for ".AkInflector
::humanize(AkInflector
::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()."</h2>\n<dl>\n";
4672 foreach ($this->getColumnNames() as $column=>$caption){
4673 $result .= "<dt>$caption</dt>\n<dd>".$this->getAttribute($column)."</dd>\n";
4675 $result .= "</dl>\n<hr />";
4679 }elseif(AK_ENVIRONMENT
== 'development'){
4681 str_replace("\n"," ",var_export($this->getAttributes(),true));
4686 $result = "\n-------\n Details for ".AkInflector
::humanize(AkInflector
::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()." ==\n\n/==\n";
4687 foreach ($this->getColumnNames() as $column=>$caption){
4688 $result .= "\t * $caption: ".$this->getAttribute($column)."\n";
4690 $result .= "\n\n-------\n";
4698 function dbugging($trace_this_on_debug_mode = null)
4700 if(!empty($this->_db
->debug
) && !empty($trace_this_on_debug_mode)){
4701 $message = !is_scalar($trace_this_on_debug_mode) ?
var_export($trace_this_on_debug_mode, true) : (string)$trace_this_on_debug_mode;
4702 Ak
::trace($message);
4704 return !empty($this->_db
->debug
);
4709 function debug ($data = 'active_record_class', $_functions=0)
4711 if(!AK_DEBUG
&& !AK_DEV_MODE
){
4715 $data = $data == 'active_record_class' ?
(AK_PHP5 ?
clone($this) : $this) : $data;
4717 if($_functions!=0) {
4723 if (isset ($data)) {
4724 if (is_array($data) ||
is_object($data)) {
4726 if (count ($data)) {
4727 echo AK_CLI ?
"/--\n" : "<ol>\n";
4728 while (list ($key,$value) = each ($data)) {
4732 $type=gettype($value);
4733 if ($type=="array") {
4734 AK_CLI ?
printf ("\t* (%s) %s:\n",$type, $key) :
4735 printf ("<li>(%s) <b>%s</b>:\n",$type, $key);
4737 Ak
::debug ($value,$sf);
4738 $lines = explode("\n",ob_get_clean()."\n");
4739 foreach ($lines as $line){
4740 echo "\t".$line."\n";
4742 }elseif($type == "object"){
4743 if(method_exists($value,'hasColumn') && $value->hasColumn($key)){
4744 $value->toString(true);
4745 AK_CLI ?
printf ("\t* (%s) %s:\n",$type, $key) :
4746 printf ("<li>(%s) <b>%s</b>:\n",$type, $key);
4748 Ak
::debug ($value,$sf);
4749 $lines = explode("\n",ob_get_clean()."\n");
4750 foreach ($lines as $line){
4751 echo "\t".$line."\n";
4754 }elseif (eregi ("function", $type)) {
4756 AK_CLI ?
printf ("\t* (%s) %s:\n",$type, $key, $value) :
4757 printf ("<li>(%s) <b>%s</b> </li>\n",$type, $key, $value);
4763 AK_CLI ?
printf ("\t* (%s) %s = %s\n",$type, $key, $value) :
4764 printf ("<li>(%s) <b>%s</b> = %s</li>\n",$type, $key, $value);
4767 echo AK_CLI ?
"\n--/\n" : "</ol>fin.\n";
4781 ====================================================================
4784 * Selects and filters a search result to include only specified columns
4786 * $people_for_select = $People->select($People->find(),'name','email');
4788 * Now $people_for_select will hold an array with
4790 * array ('name' => 'Jose','email' => 'jose@example.com'),
4791 * array ('name' => 'Alicia','email' => 'alicia@example.com'),
4792 * array ('name' => 'Hilario','email' => 'hilario@example.com'),
4793 * array ('name' => 'Bermi','email' => 'bermi@example.com')
4796 function select(&$source_array)
4798 $resulting_array = array();
4799 if(!empty($source_array) && is_array($source_array) && func_num_args() > 1) {
4800 (array)$args = array_filter(array_slice(func_get_args(),1),array($this,'hasColumn'));
4801 foreach ($source_array as $source_item){
4802 $item_fields = array();
4803 foreach ($args as $arg){
4804 $item_fields[$arg] =& $source_item->get($arg);
4806 $resulting_array[] =& $item_fields;
4809 return $resulting_array;
4814 * Collect is a function for selecting items from double depth array
4815 * like the ones returned by the AkActiveRecord. This comes useful when you just need some
4816 * fields for generating tables, select lists with only desired fields.
4818 * $people_for_select = Ak::select($People->find(),'id','email');
4820 * Returns something like:
4822 * array ('10' => 'jose@example.com'),
4823 * array ('15' => 'alicia@example.com'),
4824 * array ('16' => 'hilario@example.com'),
4825 * array ('18' => 'bermi@example.com')
4828 function collect(&$source_array, $key_index, $value_index)
4830 $resulting_array = array();
4831 if(!empty($source_array) && is_array($source_array)) {
4832 foreach ($source_array as $source_item){
4833 $resulting_array[$source_item->get($key_index)] = $source_item->get($value_index);
4836 return $resulting_array;
4841 return Ak
::toJson($this->getAttributes());
4846 return Ak
::convert('active_record', 'yaml', $this);
4851 * Parses an special formated array as a list of keys and values
4853 * This function generates an array with values and keys from an array with numeric keys.
4855 * This allows to parse an array to a function in the following manner.
4856 * create('first_name->', 'Bermi', 'last_name->', 'Ferrer');
4857 * //Previous code will be the same that
4858 * create(array('first_name'=>'Bermi', 'last_name'=> 'Ferrer'));
4860 * Use this syntax only for quick testings, not for production environments. If the number of arguments varies, the result might be unpredictable.
4862 * This function syntax is disabled by default. You need to define('AK_ENABLE_AKELOS_ARGS', true)
4863 * if you need this functionality.
4867 function parseAkelosArgs(&$args)
4869 if(!AK_ENABLE_AKELOS_ARGS
){
4870 $this->_castDateParametersFromDateHelper_($args);
4873 $k = array_keys($args);
4874 if(isset($k[1]) && substr($args[$k[0]],-1) == '>'){
4877 for($i = 0; $i < $size; $i++
) {
4879 if(!isset($key) && is_string($args[$k[$i]]) && substr($v,-1) == '>'){
4880 $key = rtrim($v, '=-> ');
4881 }elseif(isset($key)) {
4885 $params[$k[$i]] = $v;
4888 if(!empty($params)){
4892 $this->_castDateParametersFromDateHelper_($args);
4895 * Gets an array from a string.
4897 * Acts like Php explode() function but uses any of this as valid separators ' AND ',' and ',' + ',' ',',',';'
4899 function getArrayFromAkString($string)
4901 if(is_array($string)){
4904 $string = str_replace(array(' AND ',' and ',' + ',' ',',',';'),array('|','|','|','','|','|'),trim($string));
4905 return strstr($string,'|') ?
explode('|', $string) : array($string);
4912 ====================================================================
4916 * Returns a record array with the column names as keys and column values
4919 function sqlSelectOne($sql, $limit = false, $offset = false)
4921 $result = $this->sqlSelect($sql, $limit, $offset);
4922 return !is_null($result) ?
array_shift($result) : null;
4926 * Returns a single value from a record
4928 function sqlSelectValue($sql, $limit = false, $offset = false)
4930 $result = $this->sqlSelectOne($sql, $limit, $offset);
4931 return !is_null($result) ?
array_shift($result) : null;
4935 * Returns an array of the values of the first column in a select:
4936 * sqlSelectValues("SELECT id FROM companies LIMIT 3") => array(1,2,3)
4938 function sqlSelectValues($sql, $limit = false, $offset = false)
4941 if($results = $this->sqlSelectAll($sql, $limit, $offset)){
4942 foreach ($results as $result){
4943 $values[] = array_slice(array_values($result),0,1);
4949 * Returns an array of record hashes with the column names as keys and
4950 * column values as values.
4952 function sqlSelectAll($sql, $limit = false, $offset = false)
4954 return $this->sqlSelect($sql, $limit, $offset);
4957 function sqlSelect($sql, $limit = false, $offset = false)
4960 $previous_fetch_mode = $GLOBALS['ADODB_FETCH_MODE'];
4961 $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_ASSOC
;
4962 $ResultSet = $limit === false ?
$this->_db
->Execute($sql) : $this->_db
->SelectLimit($sql, $limit, empty($offset) ?
0 : $offset);
4964 while ($record = $ResultSet->FetchRow()) {
4965 $results[] = $record;
4968 AK_DEBUG ?
trigger_error($this->_db
->ErrorMsg(), E_USER_NOTICE
) : null;
4970 $GLOBALS['ADODB_FETCH_MODE'] = $previous_fetch_mode;
4971 return empty($results) ?
null : $results;
4975 function getAttributeCondition($argument)
4977 if(is_array($argument)){
4979 }elseif (is_null($argument)){
4985 /*/Database statements*/
4990 ====================================================================
4996 var $_calculation_options = array('conditions', 'joins', 'order', 'select', 'group', 'having', 'distinct', 'limit', 'offset');
4999 * Count operates using three different approaches.
5001 * * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
5002 * * Count by conditions or joins
5003 * * Count using options will find the row count matched by the options used.
5005 * The last approach, count using options, accepts an option hash as the only parameter. The options are:
5007 * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array("user_name = ?", $username ). See conditions in the intro.
5008 * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
5009 * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
5010 * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
5011 * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join.
5012 * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
5014 * Examples for counting all:
5015 * $Person->count(); // returns the total count of all people
5017 * Examples for count by +conditions+ and +joins+ (this has been deprecated):
5018 * $Person->count("age > 26"); // returns the number of people older than 26
5019 * $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(*).
5021 * Examples for count with options:
5022 * $Person->count('conditions' => "age > 26");
5023 * $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.
5024 * $Person->count('id', 'conditions' => "age > 26"); // Performs a COUNT(id)
5025 * $Person->count('all', 'conditions' => "age > 26"); // Performs a COUNT(*) ('all' is an alias for '*')
5027 * Note: $Person->count('all') will not work because it will use 'all' as the condition. Use $Person->count() instead.
5031 $args = func_get_args();
5032 list($column_name, $options) = $this->_constructCountOptionsFromLegacyArgs($args);
5033 return $this->calculate('count', $column_name, $options);
5037 * Calculates average value on a given column. The value is returned as a float. See #calculate for examples with options.
5039 * $Person->average('age');
5041 function average($column_name, $options = array())
5043 return $this->calculate('avg', $column_name, $options);
5047 * 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.
5049 * $Person->minimum('age');
5051 function minimum($column_name, $options = array())
5053 return $this->calculate('min', $column_name, $options);
5057 * 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.
5059 * $Person->maximum('age');
5061 function maximum($column_name, $options = array())
5063 return $this->calculate('max', $column_name, $options);
5067 * 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.
5069 * $Person->sum('age');
5071 function sum($column_name, $options = array())
5073 return $this->calculate('sum', $column_name, $options);
5077 * This calculates aggregate values in the given column: Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
5078 * Options such as 'conditions', 'order', 'group', 'having', and 'joins' can be passed to customize the query.
5080 * There are two basic forms of output:
5081 * * 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.
5082 * * Grouped values: This returns an ordered hash of the values and groups them by the 'group' option. It takes a column name.
5084 * $values = $Person->maximum('age', array('group' => 'last_name'));
5085 * echo $values["Drake"]
5089 * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array( "user_name = ?", username ). See conditions in the intro.
5090 * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
5091 * The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
5092 * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
5093 * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
5094 * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join.
5095 * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
5098 * $Person->calculate('count', 'all'); // The same as $Person->count();
5099 * $Person->average('age'); // SELECT AVG(age) FROM people...
5100 * $Person->minimum('age', array('conditions' => array('last_name != ?', 'Drake'))); // Selects the minimum age for everyone with a last name other than 'Drake'
5101 * $Person->minimum('age', array('having' => 'min(age) > 17', 'group' => 'last'_name)); // Selects the minimum age for any family without any minors
5103 function calculate($operation, $column_name, $options = array())
5105 $this->_validateCalculationOptions($options);
5106 $column_name = empty($options['select']) ?
$column_name : $options['select'];
5107 $column_name = $column_name == 'all' ?
'*' : $column_name;
5108 $column = $this->_getColumnFor($column_name);
5109 if (!empty($options['group'])){
5110 return $this->_executeGroupedCalculation($operation, $column_name, $column, $options);
5112 return $this->_executeSimpleCalculation($operation, $column_name, $column, $options);
5121 function _constructCountOptionsFromLegacyArgs($args)
5124 $column_name = 'all';
5129 count(options=array())
5130 count($column_name='all', $options=array())
5131 count($conditions=null, $joins=null)
5133 if(count($args) > 2){
5134 trigger_error(Ak
::t("Unexpected parameters passed to count(\$options=array())", E_USER_ERROR
));
5135 }elseif(count($args) > 0){
5136 if(!empty($args[0]) && is_array($args[0])){
5137 $options = $args[0];
5138 }elseif(!empty($args[1]) && is_array($args[1])){
5139 $column_name = array_shift($args);
5140 $options = array_shift($args);
5142 $options = array('conditions' => $args[0]);
5143 if(!empty($args[1])){
5144 $options = array_merge($options, array('joins' => $args[1]));
5148 return array($column_name, $options);
5155 function _constructCalculationSql($operation, $column_name, $options)
5157 $operation = strtolower($operation);
5158 $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name);
5159 $use_workaround = $operation == 'count' && !empty($options['distinct']) && $this->_getDatabaseType() == 'sqlite';
5161 $sql = $use_workaround ?
5162 "SELECT COUNT(*) AS $aggregate_alias" : // A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
5163 "SELECT $operation(".(empty($options['distinct'])?
'':'DISTINCT ')."$column_name) AS $aggregate_alias";
5166 $sql .= empty($options['group']) ?
'' : ", {$options['group_field']} AS {$options['group_alias']}";
5167 $sql .= $use_workaround ?
" FROM (SELECT DISTINCT {$column_name}" : '';
5168 $sql .= " FROM ".$this->getTableName()." ";
5170 $sql .= empty($options['joins']) ?
'' : " {$options['joins']} ";
5172 empty($options['conditions']) ?
null : $this->addConditions($sql, $options['conditions']);
5174 if (!empty($options['group'])){
5175 $sql .= " GROUP BY {$options['group_field']} ";
5176 $sql .= empty($options['having']) ?
'' : " HAVING {$options['having']} ";
5179 $sql .= empty($options['order']) ?
'' : " ORDER BY {$options['order']} ";
5180 $sql .= $use_workaround ?
')' : '';
5188 function _executeSimpleCalculation($operation, $column_name, $column, $options)
5190 $value = $this->sqlSelectValue($this->_constructCalculationSql($operation, $column_name, $options), empty($options['limit']) ?
false : $options['limit'], !isset($options['offset']) ?
false : $options['offset']);
5191 return $this->_typeCastCalculatedValue($value, $column, $operation);
5197 function _executeGroupedCalculation($operation, $column_name, $column, $options)
5199 $group_attr = $options['group'];
5200 $group_field = $options['group'];
5201 $group_alias = $this->_getColumnAliasFor($group_field);
5202 $group_column = $this->_getColumnFor($group_field);
5203 $sql = $this->_constructCalculationSql($operation, $column_name, array_merge(array('group_field' => $group_field, 'group_alias' => $group_alias),$options));
5204 $calculated_data = $this->sqlSelectAll($sql, empty($options['limit']) ?
false : $options['limit'], !isset($options['offset']) ?
false : $options['offset']);
5205 $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name);
5208 foreach ($calculated_data as $row){
5209 $key = $this->_typeCastCalculatedValue($row[$group_alias], $group_column);
5210 $all[$key] = $this->_typeCastCalculatedValue($row[$aggregate_alias], $column, $operation);
5218 function _validateCalculationOptions($options = array())
5220 $invalid_options = array_diff(array_keys($options),$this->_calculation_options
);
5221 if(!empty($invalid_options)){
5222 trigger_error(Ak
::t('%options are not valid calculation options.', array('%options'=>join(', ',$invalid_options))), E_USER_ERROR
);
5227 * Converts a given key to the value that the database adapter returns as
5228 * as a usable column name.
5229 * users.id #=> users_id
5230 * sum(id) #=> sum_id
5231 * count(distinct users.id) #=> count_distinct_users_id
5232 * count(*) #=> count_all
5236 function _getColumnAliasFor()
5238 $args = func_get_args();
5239 $keys = strtolower(join(' ',(!empty($args) ?
(is_array($args[0]) ?
$args[0] : $args) : array())));
5240 return preg_replace(array('/\*/','/\W+/','/^ +/','/ +$/','/ +/'),array('all',' ','','','_'), $keys);
5246 function _getColumnFor($field)
5248 $field_name = ltrim(substr($field,strpos($field,'.')),'.');
5249 if(in_array($field_name,$this->getColumnNames())){
5258 function _typeCastCalculatedValue($value, $column, $operation = null)
5260 $operation = strtolower($operation);
5261 if($operation == 'count'){
5262 return intval($value);
5263 }elseif ($operation == 'avg'){
5264 return floatval($value);
5266 return empty($column) ?
$value : AkActiveRecord
::castAttributeFromDatabase($column, $value);
5272 function extractOptionsFromArgs(&$args)
5274 $_tmp_options = !empty($args) && is_array($args) && is_array($args[count($args)]) ?
array_pop($args) : array();
5276 foreach (array('conditions', 'include', 'joins', 'limit', 'offset', 'order', 'bind', 'select', 'readonly') as $k){
5277 if(isset($_tmp_options[$k])){
5278 $options[$k] = $_tmp_options[$k];
5285 function hasBeenModified()
5287 return Ak
::objectHasBeenModified($this);
5291 * Just freeze the attributes hash, such that associations are still accessible even on destroyed records.
5293 * @todo implement freeze correctly for its intended use
5297 $this->_freeze
= true;
5302 return !empty($this->_freeze
);
5306 * Alias for getModelName()
5310 return $this->getModelName();
5313 function &objectCache()
5316 $args =& func_get_args();
5317 if(count($args) == 2){
5318 if(!isset($cache[$args[0]])){
5319 $cache[$args[0]] =& $args[1];
5321 }elseif(!isset($cache[$args[0]])){
5324 return $cache[$args[0]];
5330 ====================================================================
5331 Right now Akelos uses phpAdodb for bd abstraction. This are functionalities not
5332 provided in phpAdodb and that will move to a separated driver for each db
5335 function _extractValueFromDefault($default)
5337 if($this->_getDatabaseType() == 'postgre'){
5338 if(preg_match("/^'(.*)'::/", $default, $match)){