| [ Index ] |
PHP Cross Reference of Akelos Framework |
[Summary view] [Print] [Text view]
1 <?php 2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ 3 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 // +----------------------------------------------------------------------+ 10 11 /** 12 * @package ActiveRecord 13 * @subpackage Base 14 * @component Active Record 15 * @author Bermi Ferrer <bermi a.t akelos c.om> 2004 - 2007 16 * @author Kaste 2007 17 * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org 18 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html> 19 */ 20 21 require_once (AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkAssociatedActiveRecord.php'); 22 require_once (AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkDbAdapter.php'); 23 require_once (AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkDbSchemaCache.php'); 24 /**#@+ 25 * Constants 26 */ 27 // Akelos args is a short way to call functions that is only intended for fast prototyping 28 defined('AK_ENABLE_AKELOS_ARGS') ? null : define('AK_ENABLE_AKELOS_ARGS', false); 29 // Use setColumnName if available when using set('column_name', $value); 30 defined('AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT') ? null : define('AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT', true); 31 defined('AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS') ? null : define('AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS', false); 32 defined('AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS') ? null : define('AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS', AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS); 33 defined('AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS') ? null : define('AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS', AK_ACTIVE_RECORD_ENABLE_AUTOMATIC_SETTERS_AND_GETTERS); 34 35 // Forces loading database schema on every call 36 if(AK_DEV_MODE) { 37 AkDbSchemaCache::doRefresh(true); 38 } else if (AK_ENVIRONMENT == 'testing') { 39 AkDbSchemaCache::doRefresh(true); 40 define('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA',true); 41 } 42 43 defined('AK_ACTIVE_RECORD_ENABLE_PERSISTENCE') ? null : define('AK_ACTIVE_RECORD_ENABLE_PERSISTENCE', AK_ENVIRONMENT != 'testing'); 44 defined('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA') ? null : define('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA', AK_ACTIVE_RECORD_ENABLE_PERSISTENCE && AK_ENVIRONMENT != 'development'); 45 defined('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE') ? null : define('AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA_LIFE', 300); 46 defined('AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES') ? null : define('AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES', true); 47 defined('AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS') ? null : define('AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS', false); 48 defined('AK_NOT_EMPTY_REGULAR_EXPRESSION') ? null : define('AK_NOT_EMPTY_REGULAR_EXPRESSION','/.+/'); 49 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"); 50 defined('AK_NUMBER_REGULAR_EXPRESSION') ? null : define('AK_NUMBER_REGULAR_EXPRESSION',"/^[0-9]+$/"); 51 defined('AK_PHONE_REGULAR_EXPRESSION') ? null : define('AK_PHONE_REGULAR_EXPRESSION',"/^([\+]?[(]?[\+]?[ ]?[0-9]{2,3}[)]?[ ]?)?[0-9 ()\-]{4,25}$/"); 52 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}$/"); 53 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])$/"); 54 defined('AK_POST_CODE_REGULAR_EXPRESSION') ? null : define('AK_POST_CODE_REGULAR_EXPRESSION',"/^[0-9A-Za-z -]{2,9}$/"); 55 /**#@-*/ 56 57 58 59 ak_compat('array_combine'); 60 61 /** 62 * Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with 63 * which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change 64 * is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain 65 * database table will happen automatically in most common cases, but can be overwritten for the uncommon ones. 66 * 67 * See the mapping rules in table_name and the full example in README.txt for more insight. 68 * 69 * == Creation == 70 * 71 * 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 72 * you're receiving the data from somewhere else, like a HTTP request. It works like this: 73 * 74 * <code> 75 * $user = new User(array('name' => 'David', 'occupation' => 'Code Artist')); 76 * echo $user->name; // Will print "David" 77 * </code> 78 * 79 * You can also use a parameter list initialization.: 80 * 81 * $user = new User('name->', 'David', 'occupation->', 'Code Artist'); 82 * 83 * And of course you can just create a bare object and specify the attributes after the fact: 84 * 85 * <code> 86 * $user = new User(); 87 * $user->name = 'David'; 88 * $user->occupation = 'Code Artist'; 89 * </code> 90 * 91 * == Conditions == 92 * 93 * Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement. 94 * The array form is to be used when the condition input is tainted and requires sanitization. The string form can 95 * be used for statements that doesn't involve tainted data. Examples: 96 * 97 * <code> 98 * class User extends ActiveRecord 99 * { 100 * function authenticateUnsafely($user_name, $password) 101 * { 102 * return findFirst("user_name = '$user_name' AND password = '$password'"); 103 * } 104 * 105 * function authenticateSafely($user_name, $password) 106 * { 107 * return findFirst("user_name = ? AND password = ?", $user_name, $password); 108 * } 109 * } 110 * </code> 111 * 112 * The <tt>authenticateUnsafely</tt> method inserts the parameters directly into the query and is thus susceptible to SQL-injection 113 * attacks if the <tt>$user_name</tt> and <tt>$password</tt> parameters come directly from a HTTP request. The <tt>authenticateSafely</tt> method, 114 * 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 115 * an attacker can't escape the query and fake the login (or worse). 116 * 117 * When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth 118 * question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing 119 * the question marks with symbols and supplying a hash with values for the matching symbol keys: 120 * 121 * <code> 122 * $Company->findFirst( 123 * "id = :id AND name = :name AND division = :division AND created_at > :accounting_date", 124 * array(':id' => 3, ':name' => "37signals", ':division' => "First", ':accounting_date' => '2005-01-01') 125 * ); 126 * </code> 127 * 128 * == Accessing attributes before they have been type casted == 129 * 130 * Some times you want to be able to read the raw attribute data without having the column-determined type cast run its course first. 131 * That can be done by using the <attribute>_before_type_cast accessors that all attributes have. For example, if your Account model 132 * has a balance attribute, you can call $Account->balance_before_type_cast or $Account->id_before_type_cast. 133 * 134 * This is especially useful in validation situations where the user might supply a string for an integer field and you want to display 135 * the original string back in an error message. Accessing the attribute normally would type cast the string to 0, which isn't what you 136 * want. 137 * 138 * == Saving arrays, hashes, and other non-mappable objects in text columns == 139 * 140 * Active Record can serialize any object in text columns. To do so, you must specify this with by setting the attribute serialize with 141 * a comma separated list of columns or an array. 142 * This makes it possible to store arrays, hashes, and other non-mappeable objects without doing any additional work. Example: 143 * 144 * <code> 145 * class User extends ActiveRecord 146 * { 147 * var $serialize = 'preferences'; 148 * } 149 * 150 * $User = new User(array('preferences'=>array("background" => "black", "display" => 'large'))); 151 * $User->find($user_id); 152 * $User->preferences // array("background" => "black", "display" => 'large') 153 * </code> 154 * 155 * == Single table inheritance == 156 * 157 * Active Record allows inheritance by storing the name of the class in a column that by default is called "type" (can be changed 158 * by overwriting <tt>AkActiveRecord->_inheritanceColumn</tt>). This means that an inheritance looking like this: 159 * 160 * <code> 161 * class Company extends ActiveRecord{} 162 * class Firm extends Company{} 163 * class Client extends Company{} 164 * class PriorityClient extends Client{} 165 * </code> 166 * 167 * When you do $Firm->create('name =>', "akelos"), this record will be saved in the companies table with type = "Firm". You can then 168 * fetch this row again using $Company->find('first', "name = '37signals'") and it will return a Firm object. 169 * 170 * 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 171 * like normal subclasses with no special magic for differentiating between them or reloading the right type with find. 172 * 173 * Note, all the attributes for all the cases are kept in the same table. Read more: 174 * http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html 175 * 176 * == Connection to multiple databases in different models == 177 * 178 * Connections are usually created through AkActiveRecord->establishConnection and retrieved by AkActiveRecord->connection. 179 * All classes inheriting from AkActiveRecord will use this connection. But you can also set a class-specific connection. 180 * For example, if $Course is a AkActiveRecord, but resides in a different database you can just say $Course->establishConnection 181 * and $Course and all its subclasses will use this connection instead. 182 * 183 * Active Records will automatically record creation and/or update timestamps of database objects 184 * if fields of the names created_at/created_on or updated_at/updated_on are present. 185 * Date only: created_on, updated_on 186 * Date and time: created_at, updated_at 187 * 188 * This behavior can be turned off by setting <tt>$this->_recordTimestamps = false</tt>. 189 */ 190 class AkActiveRecord extends AkAssociatedActiveRecord 191 { 192 /**#@+ 193 * @access private 194 */ 195 //var $disableAutomatedAssociationLoading = true; 196 var $_tableName; 197 var $_db; 198 var $_newRecord; 199 var $_freeze; 200 var $_dataDictionary; 201 var $_primaryKey; 202 var $_inheritanceColumn; 203 204 var $_associations; 205 206 var $_internationalize; 207 208 var $_errors = array(); 209 210 var $_attributes = array(); 211 212 var $_protectedAttributes = array(); 213 var $_accessibleAttributes = array(); 214 215 var $_recordTimestamps = true; 216 217 // Column description 218 var $_columnNames = array(); 219 // Array of column objects for the table associated with this class. 220 var $_columns = array(); 221 // Columns that can be edited/viewed 222 var $_contentColumns = array(); 223 // Methods that will be dinamically loaded for the model (EXPERIMENTAL) This pretends to generate something similar to Ruby on Rails finders. 224 // If you set array('findOneByUsernameAndPassword', 'findByCompany', 'findAllByExipringDate') 225 // You'll get $User->findOneByUsernameAndPassword('admin', 'pass'); 226 var $_dynamicMethods = false; 227 var $_combinedAttributes = array(); 228 229 var $_BlobQueryStack = null; 230 231 var $_automated_max_length_validator = false; 232 var $_automated_validators_enabled = false; 233 var $_automated_not_null_validator = false; 234 var $_set_default_attribute_values_automatically = true; 235 236 // This is needed for enabling support for static active record instantation under php 237 var $_activeRecordHasBeenInstantiated = true; 238 239 var $__ActsLikeAttributes = array(); 240 241 /** 242 * Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations. 243 */ 244 var $_defaultErrorMessages = array( 245 'inclusion' => "is not included in the list", 246 'exclusion' => "is reserved", 247 'invalid' => "is invalid", 248 'confirmation' => "doesn't match confirmation", 249 'accepted' => "must be accepted", 250 'empty' => "can't be empty", 251 'blank' => "can't be blank", 252 'too_long' => "is too long (max is %d characters)", 253 'too_short' => "is too short (min is %d characters)", 254 'wrong_length' => "is the wrong length (should be %d characters)", 255 'taken' => "has already been taken", 256 'not_a_number' => "is not a number" 257 ); 258 259 var $__activeRecordObject = true; 260 261 /**#@-*/ 262 263 function __construct() 264 { 265 $attributes = (array)func_get_args(); 266 return $this->init($attributes); 267 } 268 269 function init($attributes = array()) 270 { 271 AK_LOG_EVENTS ? ($this->Logger =& Ak::getLogger()) : null; 272 $this->_internationalize = is_null($this->_internationalize) && AK_ACTIVE_RECORD_INTERNATIONALIZE_MODELS_BY_DEFAULT ? count($this->getAvailableLocales()) > 1 : $this->_internationalize; 273 274 @$this->_instantiateDefaultObserver(); 275 276 $this->setConnection(); 277 278 if(!empty($this->table_name)){ 279 $this->setTableName($this->table_name); 280 } 281 $this->act_as = !empty($this->acts_as) ? $this->acts_as : (empty($this->act_as) ? false : $this->act_as); 282 if (!empty($this->act_as)) { 283 $this->_loadActAsBehaviours(); 284 } 285 286 if(!empty($this->combined_attributes)){ 287 foreach ($this->combined_attributes as $combined_attribute){ 288 $this->addCombinedAttributeConfiguration($combined_attribute); 289 } 290 } 291 292 if(isset($attributes[0]) && is_array($attributes[0]) && count($attributes) === 1){ 293 $attributes = $attributes[0]; 294 $this->_newRecord = true; 295 } 296 297 // new AkActiveRecord(23); //Returns object with primary key 23 298 if(isset($attributes[0]) && count($attributes) === 1 && $attributes[0] > 0){ 299 $record = $this->find($attributes[0]); 300 if(!$record){ 301 return false; 302 }else { 303 $this->setAttributes($record->getAttributes(), true); 304 } 305 // This option is only used internally for loading found objects 306 }elseif(isset($attributes[0]) && isset($attributes[1]) && $attributes[0] == 'attributes' && is_array($attributes[1])){ 307 foreach(array_keys($attributes[1]) as $k){ 308 $attributes[1][$k] = $this->castAttributeFromDatabase($k, $attributes[1][$k]); 309 } 310 311 $avoid_loading_associations = isset($attributes[1]['load_associations']) ? false : !empty($this->disableAutomatedAssociationLoading); 312 $this->setAttributes($attributes[1], true); 313 }else{ 314 $this->newRecord($attributes); 315 } 316 317 if($this->_dynamicMethods){ 318 $this->_buildFinders(); 319 } 320 empty($avoid_loading_associations) ? $this->loadAssociations() : null; 321 } 322 323 function __destruct() 324 { 325 326 } 327 328 /** 329 * New objects can be instantiated as either empty (pass no construction parameter) or pre-set with attributes but not yet saved 330 * (pass an array with key names matching the associated table column names). 331 * In both instances, valid attribute keys are determined by the column names of the associated table; hence you can't 332 * have attributes that aren't part of the table columns. 333 */ 334 function newRecord($attributes) 335 { 336 $this->_newRecord = true; 337 338 if(AK_ACTIVE_RECORD_SKIP_SETTING_ACTIVE_RECORD_DEFAULTS && empty($attributes)){ 339 return; 340 } 341 342 if(isset($attributes) && !is_array($attributes)){ 343 $attributes = func_get_args(); 344 } 345 $this->setAttributes($this->attributesFromColumnDefinition(),true); 346 $this->setAttributes($attributes); 347 } 348 349 350 /** 351 * Returns a clone of the record that hasn't been assigned an id yet and is treated as a new record. 352 */ 353 function cloneRecord() 354 { 355 $model_name = $this->getModelName(); 356 $attributes = $this->getAttributesBeforeTypeCast(); 357 if(isset($attributes[$this->getPrimaryKey()])){ 358 unset($attributes[$this->getPrimaryKey()]); 359 } 360 return new $model_name($attributes); 361 } 362 363 364 /** 365 * Returns true if this object hasn't been saved yet that is, a record for the object doesn't exist yet. 366 */ 367 function isNewRecord() 368 { 369 if(!isset($this->_newRecord) && !isset($this->{$this->getPrimaryKey()})){ 370 $this->_newRecord = true; 371 } 372 return $this->_newRecord; 373 } 374 375 376 377 /** 378 * Reloads the attributes of this object from the database. 379 */ 380 function reload() 381 { 382 /** 383 * @todo clear cache 384 */ 385 if($object = $this->find($this->getId())){ 386 $this->setAttributes($object->getAttributes(), true); 387 return true; 388 }else { 389 return false; 390 } 391 } 392 393 394 395 /** 396 Creating records 397 ==================================================================== 398 */ 399 /** 400 * Creates an object, instantly saves it as a record (if the validation permits it), and returns it. 401 * If the save fail under validations, the unsaved object is still returned. 402 */ 403 function &create($attributes = null) 404 { 405 if(!isset($this->_activeRecordHasBeenInstantiated)){ 406 return Ak::handleStaticCall(); 407 } 408 409 if(func_num_args() > 1){ 410 $attributes = func_get_args(); 411 } 412 $model = $this->getModelName(); 413 414 $object =& new $model(); 415 $object->setAttributes($attributes); 416 $object->save(); 417 return $object; 418 } 419 420 function createOrUpdate($validate = true) 421 { 422 if($validate && !$this->isValid()){ 423 $this->transactionFail(); 424 return false; 425 } 426 return $this->isNewRecord() ? $this->_create() : $this->_update(); 427 } 428 429 function &findOrCreateBy() 430 { 431 $args = func_get_args(); 432 $Item =& Ak::call_user_func_array(array(&$this,'findFirstBy'), $args); 433 if(!$Item){ 434 $attributes = array(); 435 436 list($sql, $columns) = $this->_getFindBySqlAndColumns(array_shift($args), $args); 437 438 if(!empty($columns)){ 439 foreach ($columns as $column){ 440 $attributes[$column] = array_shift($args); 441 } 442 } 443 $Item =& $this->create($attributes); 444 $Item->has_been_created = true; 445 }else{ 446 $Item->has_been_created = false; 447 } 448 $Item->has_been_found = !$Item->has_been_created; 449 return $Item; 450 } 451 452 /** 453 * Creates a new record with values matching those of the instance attributes. 454 * Must be called as a result of a call to createOrUpdate. 455 * 456 * @access private 457 */ 458 function _create() 459 { 460 if (!$this->beforeCreate() || !$this->notifyObservers('beforeCreate')){ 461 return $this->transactionFail(); 462 } 463 464 $this->_setRecordTimestamps(); 465 466 // deprecated section 467 if($this->isLockingEnabled() && is_null($this->get('lock_version'))){ 468 Ak::deprecateWarning(array("Column %lock_version_column should have a default setting. Assumed '1'.",'%lock_version_column'=>'lock_version')); 469 $this->setAttribute('lock_version',1); 470 } // end 471 472 $attributes = $this->getColumnsForAttributes($this->getAttributes()); 473 foreach ($attributes as $column=>$value){ 474 $attributes[$column] = $this->castAttributeForDatabase($column,$value); 475 } 476 477 $pk = $this->getPrimaryKey(); 478 $table = $this->getTableName(); 479 480 $id = $this->_db->incrementsPrimaryKeyAutomatically() ? null : $this->_db->getNextSequenceValueFor($table); 481 $attributes[$pk] = $id; 482 483 $attributes = array_diff($attributes, array('')); 484 485 486 $sql = 'INSERT INTO '.$table.' '. 487 '('.join(', ',array_keys($attributes)).') '. 488 'VALUES ('.join(',',array_values($attributes)).')'; 489 490 $inserted_id = $this->_db->insert($sql, $id, $pk, $table, 'Create '.$this->getModelName()); 491 if ($this->transactionHasFailed()){ 492 return false; 493 } 494 $this->setId($inserted_id); 495 496 $this->_newRecord = false; 497 498 if (!$this->afterCreate() || !$this->notifyObservers('afterCreate')){ 499 return $this->transactionFail(); 500 } 501 502 return true; 503 } 504 505 function _setRecordTimestamps() 506 { 507 if (!$this->_recordTimestamps){ 508 return; 509 } 510 if ($this->_newRecord){ 511 if ($this->hasColumn('created_at')){ 512 $this->setAttribute('created_at', Ak::getDate()); 513 } 514 if ($this->hasColumn('created_on')){ 515 $this->setAttribute('created_on', Ak::getDate(null, 'Y-m-d')); 516 } 517 }else{ 518 if ($this->hasColumn('updated_at')){ 519 $this->setAttribute('updated_at', Ak::getDate()); 520 } 521 if ($this->hasColumn('updated_on')){ 522 $this->setAttribute('updated_on', Ak::getDate(null, 'Y-m-d')); 523 } 524 } 525 526 if($this->_newRecord && isset($this->expires_on)){ 527 if(isset($this->expires_at) && $this->hasColumn('expires_at')){ 528 $this->setAttribute('expires_at',Ak::getDate(strtotime($this->expires_at) + (defined('AK_TIME_DIFFERENCE') ? AK_TIME_DIFFERENCE*60 : 0))); 529 }elseif(isset($this->expires_on) && $this->hasColumn('expires_on')){ 530 $this->setAttribute('expires_on',Ak::getDate(strtotime($this->expires_on) + (defined('AK_TIME_DIFFERENCE') ? AK_TIME_DIFFERENCE*60 : 0), 'Y-m-d')); 531 } 532 } 533 534 } 535 536 /*/Creating records*/ 537 538 539 /** 540 Saving records 541 ==================================================================== 542 */ 543 /** 544 * - No record exists: Creates a new record with values matching those of the object attributes. 545 * - A record does exist: Updates the record with values matching those of the object attributes. 546 */ 547 function save($validate = true) 548 { 549 if($this->isFrozen()){ 550 return false; 551 } 552 $result = false; 553 $this->transactionStart(); 554 if($this->beforeSave() && $this->notifyObservers('beforeSave')){ 555 $result = $this->createOrUpdate($validate); 556 if(!$this->transactionHasFailed()){ 557 if(!$this->afterSave()){ 558 $this->transactionFail(); 559 }else{ 560 if(!$this->notifyObservers('afterSave')){ 561 $this->transactionFail(); 562 } 563 } 564 } 565 }else{ 566 $this->transactionFail(); 567 } 568 569 $result = $this->transactionHasFailed() ? false : $result; 570 $this->transactionComplete(); 571 572 return $result; 573 } 574 575 /*/Saving records*/ 576 577 /** 578 Counting Records 579 ==================================================================== 580 See also: Counting Attributes. 581 */ 582 583 /** 584 * Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. 585 * 586 * $Product->countBySql("SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"); 587 */ 588 function countBySql($sql) 589 { 590 if(!isset($this->_activeRecordHasBeenInstantiated)){ 591 return Ak::handleStaticCall(); 592 } 593 if(!stristr($sql, 'COUNT') && stristr($sql, ' FROM ')){ 594 $sql = 'SELECT COUNT(*) '.substr($sql,strpos(str_replace(' from ',' FROM ', $sql),' FROM ')); 595 } 596 if(!$this->isConnected()){ 597 $this->setConnection(); 598 } 599 600 return (integer)$this->_db->selectValue($sql); 601 } 602 /*/Counting Records*/ 603 604 /** 605 Updating records 606 ==================================================================== 607 See also: Callbacks. 608 */ 609 610 /** 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. 613 */ 614 function update($id, $attributes) 615 { 616 if(!isset($this->_activeRecordHasBeenInstantiated)){ 617 return Ak::handleStaticCall(); 618 } 619 if(is_array($id)){ 620 $results = array(); 621 foreach ($id as $idx=>$single_id){ 622 $results[] = $this->update($single_id, isset($attributes[$idx]) ? $attributes[$idx] : $attributes); 623 } 624 return $results; 625 }else{ 626 $object =& $this->find($id); 627 $object->updateAttributes($attributes); 628 return $object; 629 } 630 } 631 632 /** 633 * Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records. 634 */ 635 function updateAttribute($name, $value, $should_validate=true) 636 { 637 $this->setAttribute($name, $value); 638 return $this->save($should_validate); 639 } 640 641 642 /** 643 * Updates all the attributes in from the passed array and saves the record. If the object is 644 * invalid, the saving will fail and false will be returned. 645 */ 646 function updateAttributes($attributes, $object = null) 647 { 648 isset($object) ? $object->setAttributes($attributes) : $this->setAttributes($attributes); 649 650 return isset($object) ? $object->save() : $this->save(); 651 } 652 653 /** 654 * Updates all records with the SET-part of an SQL update statement in updates and returns an 655 * integer with the number of rows updates. A subset of the records can be selected by specifying conditions. Example: 656 * <code>$Billing->updateAll("category = 'authorized', approved = 1", "author = 'David'");</code> 657 * 658 * Important note: Conditions are not sanitized yet so beware of accepting 659 * variable conditions when using this function 660 */ 661 function updateAll($updates, $conditions = null) 662 { 663 if(!isset($this->_activeRecordHasBeenInstantiated)){ 664 return Ak::handleStaticCall(); 665 } 666 /** 667 * @todo sanitize sql conditions 668 */ 669 $sql = 'UPDATE '.$this->getTableName().' SET '.$updates; 670 $this->addConditions($sql, $conditions); 671 return $this->_db->update($sql, $this->getModelName().' Update All'); 672 } 673 674 675 /** 676 * Updates the associated record with values matching those of the instance attributes. 677 * Must be called as a result of a call to createOrUpdate. 678 * 679 * @access private 680 */ 681 function _update() 682 { 683 if(!$this->beforeUpdate() || !$this->notifyObservers('beforeUpdate')){ 684 return $this->transactionFail(); 685 } 686 687 $this->_setRecordTimestamps(); 688 689 $lock_check_sql = ''; 690 if ($this->isLockingEnabled()){ 691 $previous_value = $this->lock_version; 692 $this->setAttribute('lock_version', $previous_value + 1); 693 $lock_check_sql = ' AND lock_version = '.$previous_value; 694 } 695 696 $quoted_attributes = $this->getAvailableAttributesQuoted(); 697 $sql = 'UPDATE '.$this->getTableName().' '. 698 'SET '.join(', ', $quoted_attributes) .' '. 699 'WHERE '.$this->getPrimaryKey().'='.$this->quotedId().$lock_check_sql; 700 701 $affected_rows = $this->_db->update($sql,'Updating '.$this->getModelName()); 702 if($this->transactionHasFailed()){ 703 return false; 704 } 705 706 if ($this->isLockingEnabled() && $affected_rows != 1){ 707 $this->setAttribute('lock_version', $previous_value); 708 trigger_error(Ak::t('Attempted to update a stale object'), E_USER_NOTICE); 709 return $this->transactionFail(); 710 } 711 712 if(!$this->afterUpdate() || !$this->notifyObservers('afterUpdate')){ 713 return $this->transactionFail(); 714 } 715 716 return true; 717 } 718 719 /*/Updating records*/ 720 721 722 723 /** 724 Deleting records 725 ==================================================================== 726 See also: Callbacks. 727 */ 728 729 /** 730 * Deletes the record with the given id without instantiating an object first. If an array of 731 * ids is provided, all of them are deleted. 732 */ 733 function delete($id) 734 { 735 if(!isset($this->_activeRecordHasBeenInstantiated)){ 736 return Ak::handleStaticCall(); 737 } 738 $id = func_num_args() > 1 ? func_get_args() : $id; 739 return $this->deleteAll($this->getPrimaryKey().' IN ('.(is_array($id) ? join(', ',$id) : $id).')'); 740 } 741 742 743 /** 744 * Deletes all the records that matches the condition without instantiating the objects first 745 * (and hence not calling the destroy method). Example: 746 * 747 * <code>$Post->destroyAll("person_id = 5 AND (category = 'Something' OR category = 'Else')");</code> 748 * 749 * Important note: Conditions are not sanitized yet so beware of accepting 750 * variable conditions when using this function 751 */ 752 function deleteAll($conditions = null) 753 { 754 if(!isset($this->_activeRecordHasBeenInstantiated)){ 755 return Ak::handleStaticCall(); 756 } 757 /** 758 * @todo sanitize sql conditions 759 */ 760 $sql = 'DELETE FROM '.$this->getTableName(); 761 $this->addConditions($sql,$conditions); 762 return $this->_db->delete($sql,$this->getModelName().' Delete All'); 763 } 764 765 766 /** 767 * Destroys the record with the given id by instantiating the object and calling destroy 768 * (all the callbacks are the triggered). If an array of ids is provided, all of them are destroyed. 769 * Deletes the record in the database and freezes this instance to reflect that no changes should be 770 * made (since they can't be persisted). 771 */ 772 function destroy($id = null) 773 { 774 if(!isset($this->_activeRecordHasBeenInstantiated)){ 775 return Ak::handleStaticCall(); 776 } 777 778 $id = func_num_args() > 1 ? func_get_args() : $id; 779 780 if(isset($id)){ 781 $this->transactionStart(); 782 $id_arr = is_array($id) ? $id : array($id); 783 if($objects = $this->find($id_arr)){ 784 $results = count($objects); 785 $no_problems = true; 786 for ($i=0; $results > $i; $i++){ 787 if(!$objects[$i]->destroy()){ 788 $no_problems = false; 789 } 790 } 791 $this->transactionComplete(); 792 return $no_problems; 793 }else { 794 $this->transactionComplete(); 795 return false; 796 } 797 }else{ 798 if(!$this->isNewRecord()){ 799 $this->transactionStart(); 800 $return = $this->_destroy() && $this->freeze(); 801 $this->transactionComplete(); 802 return $return; 803 } 804 } 805 } 806 807 function _destroy() 808 { 809 if(!$this->beforeDestroy() || !$this->notifyObservers('beforeDestroy')){ 810 return $this->transactionFail(); 811 } 812 813 $sql = 'DELETE FROM '.$this->getTableName().' WHERE '.$this->getPrimaryKey().' = '.$this->_db->quote_string($this->getId()); 814 if ($this->_db->delete($sql,$this->getModelName().' Destroy') !== 1){ 815 return $this->transactionFail(); 816 } 817 818 if (!$this->afterDestroy() || !$this->notifyObservers('afterDestroy')){ 819 return $this->transactionFail(); 820 } 821 return true; 822 } 823 824 /** 825 * Destroys the objects for all the records that matches the condition by instantiating 826 * each object and calling the destroy method. 827 * 828 * Example: 829 * 830 * $Person->destroyAll("last_login < '2004-04-04'"); 831 */ 832 function destroyAll($conditions) 833 { 834 if($objects = $this->find('all',array('conditions'=>$conditions))){ 835 $results = count($objects); 836 $no_problems = true; 837 for ($i=0; $results > $i; $i++){ 838 if(!$objects[$i]->destroy()){ 839 $no_problems = false; 840 } 841 } 842 return $no_problems; 843 }else { 844 return false; 845 } 846 } 847 848 /*/Deleting records*/ 849 850 851 852 853 /** 854 Finding records 855 ==================================================================== 856 */ 857 858 /** 859 * Returns true if the given id represents the primary key of a record in the database, false otherwise. Example: 860 * 861 * $Person->exists(5); 862 */ 863 function exists($id) 864 { 865 return $this->find('first',array('conditions' => array($this->getPrimaryKey().' = '.$id))) !== false; 866 } 867 868 /** 869 * Find operates with three different retrieval approaches: 870 * * Find by id: This can either be a specific id find(1), a list of ids find(1, 5, 6), 871 * or an array of ids find(array(5, 6, 10)). If no record can be found for all of the listed ids, 872 * then RecordNotFound will be raised. 873 * * Find first: This will return the first record matched by the options used. These options 874 * can either be specific conditions or merely an order. 875 * If no record can matched, false is returned. 876 * * Find all: This will return all the records matched by the options used. If no records are found, an empty array is returned. 877 * 878 * All approaches accepts an $option array as their last parameter. The options are: 879 * 880 * 'conditions' => An SQL fragment like "administrator = 1" or array("user_name = ?" => $username). See conditions in the intro. 881 * 'order' => An SQL fragment like "created_at DESC, name". 882 * 'limit' => An integer determining the limit on the number of rows that should be returned. 883 * 'offset' => An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. 884 * 'joins' => An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = $id". (Rarely needed). 885 * 'include' => Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols 886 * named refer to already defined associations. See eager loading under Associations. 887 * 888 * Examples for find by id: 889 * <code> 890 * $Person->find(1); // returns the object for ID = 1 891 * $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 892 * $Person->find(array(7, 17)); // returns an array for objects with IDs in (7, 17) 893 * $Person->find(array(1)); // returns an array for objects the object with ID = 1 894 * $Person->find(1, array('conditions' => "administrator = 1", 'order' => "created_on DESC")); 895 * </code> 896 * 897 * Examples for find first: 898 * <code> 899 * $Person->find('first'); // returns the first object fetched by SELECT * FROM people 900 * $Person->find('first', array('conditions' => array("user_name = ':user_name'", ':user_name' => $user_name))); 901 * $Person->find('first', array('order' => "created_on DESC", 'offset' => 5)); 902 * </code> 903 * 904 * Examples for find all: 905 * <code> 906 * $Person->find('all'); // returns an array of objects for all the rows fetched by SELECT * FROM people 907 * $Person->find(); // Same as $Person->find('all'); 908 * $Person->find('all', array('conditions' => array("category IN (categories)", 'categories' => join(','$categories)), 'limit' => 50)); 909 * $Person->find('all', array('offset' => 10, 'limit' => 10)); 910 * $Person->find('all', array('include' => array('account', 'friends')); 911 * </code> 912 */ 913 function &find() 914 { 915 if(!isset($this->_activeRecordHasBeenInstantiated)){ 916 return Ak::handleStaticCall(); 917 } 918 919 $args = func_get_args(); 920 921 $options = $this->_extractOptionsFromArgs($args); 922 list($fetch,$options) = $this->_extractConditionsFromArgs($args,$options); 923 924 $this->_sanitizeConditionsVariables($options); 925 926 switch ($fetch) { 927 case 'first': 928 // HACK: php4 pass by ref 929 $result =& $this->_findInitial($options); 930 return $result; 931 break; 932 933 case 'all': 934 // HACK: php4 pass by ref 935 $result =& $this->_findEvery($options); 936 return $result; 937 break; 938 939 default: 940 // HACK: php4 pass by ref 941 $result =& $this->_findFromIds($args, $options); 942 return $result; 943 break; 944 } 945 $result = false; 946 return $result; 947 } 948 949 function &_findInitial($options) 950 { 951 // TODO: virtual_limit is a hack 952 // actually we fetch_all and return only the first row 953 $options = array_merge($options, array((!empty($options['include']) ?'virtual_limit':'limit')=>1)); 954 $result =& $this->_findEvery($options); 955 956 if(!empty($result) && is_array($result)){ 957 $_result =& $result[0]; 958 }else{ 959 $_result = false; 960 // if we return an empty array instead of false we need to change this->exists()! 961 //$_result = array(); 962 } 963 return $_result; 964 965 } 966 967 function &_findEvery($options) 968 { 969 if((!empty($options['include']) && $this->hasAssociations())){ 970 $result =& $this->findWithAssociations($options); 971 }else{ 972 $sql = $this->constructFinderSql($options); 973 if(!empty($options['bind']) && is_array($options['bind']) && strstr($sql,'?')){ 974 $sql = array_merge(array($sql),$options['bind']); 975 } 976 977 $result =& $this->findBySql($sql); 978 } 979 980 if(!empty($result) && is_array($result)){ 981 $_result =& $result; 982 }else{ 983 $_result = false; 984 } 985 return $_result; 986 987 } 988 989 function &_findFromIds($ids, $options) 990 { 991 $expects_array = is_array($ids[0]); 992 $ids = array_unique($expects_array ? (isset($ids[1]) ? array_merge($ids[0],$ids) : $ids[0]) : $ids); 993 994 $num_ids = count($ids); 995 996 //at this point $options['conditions'] can't be an array 997 $conditions = !empty($options['conditions']) ? ' AND '.$options['conditions'] : ''; 998 999 switch ($num_ids){ 1000 case 0 : 1001 trigger_error($this->t('Couldn\'t find %object_name without an ID%conditions',array('%object_name'=>$this->getModelName(),'%conditions'=>$conditions)), E_USER_ERROR); 1002 break; 1003 1004 case 1 : 1005 $table_name = !empty($options['include']) && $this->hasAssociations() ? '__owner' : $this->getTableName(); 1006 $options['conditions'] = $table_name.'.'.$this->getPrimaryKey().' = '.$ids[0].$conditions; 1007 $result =& $this->_findEvery($options); 1008 if (!$expects_array && $result !== false){ 1009 return $result[0]; 1010 } 1011 return $result; 1012 break; 1013 1014 default: 1015 $without_conditions = empty($options['conditions']) ? true : false; 1016 $ids_condition = $this->getPrimaryKey().' IN ('.join(', ',$ids).')'; 1017 $options['conditions'] = $ids_condition.$conditions; 1018 1019 $result =& $this->_findEvery($options); 1020 if(is_array($result) && (count($result) != $num_ids && $without_conditions)){ 1021 $result = false; 1022 } 1023 return $result; 1024 break; 1025 } 1026 1027 } 1028 1029 function _extractOptionsFromArgs(&$args) 1030 { 1031 $last_arg = count($args)-1; 1032 return isset($args[$last_arg]) && is_array($args[$last_arg]) && $this->_isOptionsHash($args[$last_arg]) ? array_pop($args) : array(); 1033 } 1034 1035 function _isOptionsHash($options) 1036 { 1037 if (isset($options[0])){ 1038 return false; 1039 } 1040 $valid_keys = array('conditions', 'include', 'joins', 'limit', 'offset', 'group', 'order', 'sort', 'bind', 'select','select_prefix', 'readonly'); 1041 foreach (array_keys($options) as $key){ 1042 if (!in_array($key,$valid_keys)){ 1043 return false; 1044 } 1045 } 1046 return true; 1047 } 1048 1049 function _extractConditionsFromArgs($args, $options) 1050 { 1051 if(empty($args)){ 1052 $fetch = 'all'; 1053 } else { 1054 $fetch = $args[0]; 1055 } 1056 $num_args = count($args); 1057 1058 // deprecated: acts like findFirstBySQL 1059 if ($num_args === 1 && !is_numeric($args[0]) && is_string($args[0]) && $args[0] != 'all' && $args[0] != 'first'){ 1060 // $Users->find("last_name = 'Williams'"); => find('first',"last_name = 'Williams'"); 1061 Ak::deprecateWarning(array("AR::find('%sql') is ambiguous and therefore deprecated, use AR::find('first',%sql) instead", '%sql'=>$args[0])); 1062 $options = array('conditions'=> $args[0]); 1063 return array('first',$options); 1064 } //end 1065 1066 // set fetch_mode to 'all' if none is given 1067 if (!is_numeric($fetch) && !is_array($fetch) && $fetch != 'all' && $fetch != 'first') { 1068 array_unshift($args, 'all'); 1069 $num_args = count($args); 1070 } 1071 if ($num_args > 1) { 1072 if (is_string($args[1])){ 1073 // $Users->find(:fetch_mode,"first_name = ?",'Tim'); 1074 $fetch = array_shift($args); 1075 $options = array_merge($options, array('conditions'=>$args)); //TODO: merge_conditions 1076 }elseif (is_array($args[1])) { 1077 // $Users->find(:fetch_mode,array('first_name = ?,'Tim')); 1078 $fetch = array_shift($args); 1079 $options = array_merge($options, array('conditions'=>$args[0])); //TODO: merge_conditions 1080 } 1081 } 1082 1083 return array($fetch,$options); 1084 } 1085 1086 function _sanitizeConditionsVariables(&$options) 1087 { 1088 if(!empty($options['conditions']) && is_array($options['conditions'])){ 1089 if (isset($options['conditions'][0]) && strstr($options['conditions'][0], '?') && count($options['conditions']) > 1){ 1090 //array('conditions' => array("name=?",$name)) 1091 $pattern = array_shift($options['conditions']); 1092 $options['bind'] = array_values($options['conditions']); 1093 $options['conditions'] = $pattern; 1094 }elseif (isset($options['conditions'][0])){ 1095 //array('conditions' => array("user_name = :user_name", ':user_name' => 'hilario') 1096 $pattern = array_shift($options['conditions']); 1097 $options['conditions'] = str_replace(array_keys($options['conditions']), array_values($this->getSanitizedConditionsArray($options['conditions'])),$pattern); 1098 }else{ 1099 //array('conditions' => array('user_name'=>'Hilario')) 1100 $options['conditions'] = join(' AND ',(array)$this->getAttributesQuoted($options['conditions'])); 1101 } 1102 } 1103 1104 } 1105 1106 function &findFirst() 1107 { 1108 if(!isset($this->_activeRecordHasBeenInstantiated)){ 1109 return Ak::handleStaticCall(); 1110 } 1111 $args = func_get_args(); 1112 $result =& Ak::call_user_func_array(array(&$this,'find'), array_merge(array('first'),$args)); 1113 return $result; 1114 } 1115 1116 function &findAll() 1117 { 1118 if(!isset($this->_activeRecordHasBeenInstantiated)){ 1119 return Ak::handleStaticCall(); 1120 } 1121 $args = func_get_args(); 1122 $result =& Ak::call_user_func_array(array(&$this,'find'), array_merge(array('all'),$args)); 1123 return $result; 1124 } 1125 1126 1127 /** 1128 * Works like find_all, but requires a complete SQL string. Examples: 1129 * $Post->findBySql("SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"); 1130 * $Post->findBySql(array("SELECT * FROM posts WHERE author = ? AND created_on > ?", $author_id, $start_date)); 1131 */ 1132 function &findBySql($sql, $limit = null, $offset = null, $bindings = null) 1133 { 1134 if ($limit || $offset){ 1135 Ak::deprecateWarning("You're calling AR::findBySql with \$limit or \$offset parameters. This has been deprecated."); 1136 $this->_db->addLimitAndOffset($sql, array('limit'=>$limit,'offset'=>$offset)); 1137 } 1138 if(!isset($this->_activeRecordHasBeenInstantiated)){ 1139 return Ak::handleStaticCall(); 1140 } 1141 $objects = array(); 1142 $records = $this->_db->select ($sql,'selecting'); 1143 foreach ($records as $record){ 1144 $objects[] =& $this->instantiate($this->getOnlyAvailableAttributes($record), false); 1145 } 1146 return $objects; 1147 } 1148 1149 /** 1150 * This function pretends to emulate RoR finders until AkActiveRecord::addMethod becomes stable on future PHP versions. 1151 * @todo use PHP5 __call method for handling the magic finder methods like findFirstByUnsenameAndPassword('bermi','pass') 1152 */ 1153 function &findFirstBy() 1154 { 1155 if(!isset($this->_activeRecordHasBeenInstantiated)){ 1156 return Ak::handleStaticCall(); 1157 } 1158 $args = func_get_args(); 1159 array_unshift($args,'first'); 1160 $result =& Ak::call_user_func_array(array(&$this,'findBy'), $args); 1161 return $result; 1162 } 1163 1164 function &findLastBy() 1165 { 1166 if(!isset($this->_activeRecordHasBeenInstantiated)){ 1167 return Ak::handleStaticCall(); 1168 } 1169 $args = func_get_args(); 1170 $options = $this->_extractOptionsFromArgs($args); 1171 $options['order'] = $this->getPrimaryKey().' DESC'; 1172 array_push($args, $options); 1173 $result =& Ak::call_user_func_array(array(&$this,'findFirstBy'), $args); 1174 return $result; 1175 } 1176 1177 function &findAllBy() 1178 { 1179 if(!isset($this->_activeRecordHasBeenInstantiated)){ 1180 return Ak::handleStaticCall(); 1181 } 1182 $args = func_get_args(); 1183 array_unshift($args,'all'); 1184 $result =& Ak::call_user_func_array(array(&$this,'findBy'), $args); 1185 return $result; 1186 } 1187 1188 /** 1189 * This method allows you to use finders in a more flexible way like: 1190 * 1191 * findBy('username AND password', $username, $password); 1192 * findBy('age > ? AND name:contains', 18, 'Joe'); 1193 * findBy('is_active = true AND session_id', session_id()); 1194 * 1195 */ 1196 function &findBy() 1197 { 1198 if(!isset($this->_activeRecordHasBeenInstantiated)){ 1199 return Ak::handleStaticCall(); 1200 } 1201 $args = func_get_args(); 1202 $find_by_sql = array_shift($args); 1203 if($find_by_sql == 'all' || $find_by_sql == 'first'){ 1204 $fetch = $find_by_sql; 1205 $find_by_sql = array_shift($args); 1206 }else{ 1207 $fetch = 'all'; 1208 } 1209 1210 $options = $this->_extractOptionsFromArgs($args); 1211 1212 $query_values = $args; 1213 $query_arguments_count = count($query_values); 1214 1215 list($sql, $requested_args) = $this->_getFindBySqlAndColumns($find_by_sql, $query_values); 1216 1217 if($query_arguments_count != count($requested_args)){ 1218 trigger_error(Ak::t('Argument list did not match expected set. Requested arguments are:').join(', ',$requested_args),E_USER_ERROR); 1219 $false = false; 1220 return $false; 1221 } 1222 1223 $true_bool_values = array(true,1,'true','True','TRUE','1','y','Y','yes','Yes','YES','s','Si','SI','V','v','T','t'); 1224 1225 foreach ($requested_args as $k=>$v){ 1226 switch ($this->getColumnType($v)) { 1227 case 'boolean': 1228 $query_values[$k] = in_array($query_values[$k],$true_bool_values) ? true : false; 1229 break; 1230 1231 case 'date': 1232 case 'datetime': 1233 $query_values[$k] = str_replace('/','-', $this->castAttributeForDatabase($k,$query_values[$k],false)); 1234 break; 1235 1236 default: 1237 break; 1238 } 1239 } 1240 1241 $conditions = array($sql); 1242 foreach ($query_values as $bind_value){ 1243 $conditions[] = $bind_value; 1244 } 1245 /** 1246 * @todo merge_conditions 1247 */ 1248 $options['conditions'] = $conditions; 1249 1250 $result =& Ak::call_user_func_array(array(&$this,'find'), array($fetch,$options)); 1251 return $result; 1252 } 1253 1254 1255 function _getFindBySqlAndColumns($find_by_sql, &$query_values) 1256 { 1257 $sql = str_replace(array('(',')','||','|','&&','&',' '),array(' ( ',' ) ',' OR ',' OR ',' AND ',' AND ',' '), $find_by_sql); 1258 $operators = array('AND','and','(',')','&','&&','NOT','<>','OR','|','||'); 1259 $pieces = explode(' ',$sql); 1260 $pieces = array_diff($pieces,array(' ','')); 1261 $params = array_diff($pieces,$operators); 1262 $operators = array_diff($pieces,$params); 1263 1264 $new_sql = ''; 1265 $parameter_count = 0; 1266 $requested_args = array(); 1267 foreach ($pieces as $piece){ 1268 if(in_array($piece,$params) && $this->hasColumn($piece)){ 1269 $new_sql .= $piece.' = ? '; 1270 $requested_args[$parameter_count] = $piece; 1271 $parameter_count++; 1272 }elseif (!in_array($piece,$operators)){ 1273 1274 if(strstr($piece,':')){ 1275 $_tmp_parts = explode(':',$piece); 1276 if($this->hasColumn($_tmp_parts[0])){ 1277 $query_values[$parameter_count] = isset($query_values[$parameter_count]) ? $query_values[$parameter_count] : $this->get($_tmp_parts[0]); 1278 switch (strtolower($_tmp_parts[1])) { 1279 case 'like': 1280 case '%like%': 1281 case 'is': 1282 case 'has': 1283 case 'contains': 1284 $query_values[$parameter_count] = '%'.$query_values[$parameter_count].'%'; 1285 $new_sql .= $_tmp_parts[0]." LIKE ? "; 1286 break; 1287 case 'like_left': 1288 case 'like%': 1289 case 'begins': 1290 case 'begins_with': 1291 case 'starts': 1292 case 'starts_with': 1293 $query_values[$parameter_count] = $query_values[$parameter_count].'%'; 1294 $new_sql .= $_tmp_parts[0]." LIKE ? "; 1295 break; 1296 case 'like_right': 1297 case '%like': 1298 case 'ends': 1299 case 'ends_with': 1300 case 'finishes': 1301 case 'finishes_with': 1302 $query_values[$parameter_count] = '%'.$query_values[$parameter_count]; 1303 $new_sql .= $_tmp_parts[0]." LIKE ? "; 1304 break; 1305 default: 1306 $query_values[$parameter_count] = $query_values[$parameter_count]; 1307 $new_sql .= $_tmp_parts[0].' '.$_tmp_parts[1].' ? '; 1308 break; 1309 } 1310 $requested_args[$parameter_count] = $_tmp_parts[0]; 1311 $parameter_count++; 1312 }else { 1313 $new_sql .= $_tmp_parts[0]; 1314 } 1315 }else{ 1316 $new_sql .= $piece.' '; 1317 } 1318 }else{ 1319 $new_sql .= $piece.' '; 1320 } 1321 } 1322 1323 return array($new_sql, $requested_args); 1324 } 1325 1326 1327 /** 1328 * Given a condition that uses bindings like "user = ? AND created_at > ?" will return a 1329 * string replacing the "?" bindings with the column values for current Active Record 1330 * 1331 * @return string 1332 */ 1333 function _getVariableSqlCondition($variable_condition) 1334 { 1335 $query_values = array(); 1336 list($sql, $requested_columns) = $this->_getFindBySqlAndColumns($variable_condition, $query_values); 1337 $replacements = array(); 1338 $sql = preg_replace('/((('.join($requested_columns,'|').') = \?) = \?)/','$2', $sql); 1339 foreach ($requested_columns as $attribute){ 1340 $replacements[$attribute] = $this->castAttributeForDatabase($attribute, $this->get($attribute)); 1341 } 1342 return trim(preg_replace('/('.join('|',array_keys($replacements)).')\s+([^\?]+)\s+\?/e', "isset(\$replacements['\\1']) ? '\\1 \\2 '.\$replacements['\\1']:'\\1 \\2 null'", $sql)); 1343 } 1344 1345 1346 function constructFinderSql($options, $select_from_prefix = 'default') 1347 { 1348 $sql = isset($options['select_prefix']) ? $options['select_prefix'] : ($select_from_prefix == 'default' ? 'SELECT '.(!empty($options['joins'])?$this->getTableName().'.':'') .'* FROM '.$this->getTableName() : $select_from_prefix); 1349 $sql .= !empty($options['joins']) ? ' '.$options['joins'] : ''; 1350 1351 $this->addConditions($sql, isset($options['conditions']) ? $options['conditions'] : array()); 1352 1353 // Create an alias for order 1354 if(empty($options['order']) && !empty($options['sort'])){ 1355 $options['order'] = $options['sort']; 1356 } 1357 1358 $sql .= !empty($options['group']) ? ' GROUP BY '.$options['group'] : ''; 1359 $sql .= !empty($options['order']) ? ' ORDER BY '.$options['order'] : ''; 1360 1361 $this->_db->addLimitAndOffset($sql,$options); 1362 1363 return $sql; 1364 } 1365 1366 1367 /** 1368 * Adds a sanitized version of $conditions to the $sql string. Note that the passed $sql string is changed. 1369 */ 1370 function addConditions(&$sql, $conditions = null, $table_alias = null) 1371 { 1372 $concat = empty($sql) ? '' : ' WHERE '; 1373 if (empty($conditions) && $this->_getDatabaseType() == 'sqlite') $conditions = '1'; // sqlite HACK 1374 if(!empty($conditions)){ 1375 $sql .= $concat.$conditions; 1376 $concat = ' AND '; 1377 } 1378 1379 if($this->getInheritanceColumn() !== false && $this->descendsFromActiveRecord($this)){ 1380 $type_condition = $this->typeCondition($table_alias); 1381 $sql .= !empty($type_condition) ? $concat.$type_condition : ''; 1382 } 1383 return $sql; 1384 } 1385 1386 /** 1387 * Gets a sanitized version of the input array. Each element will be escaped 1388 */ 1389 function getSanitizedConditionsArray($conditions_array) 1390 { 1391 $result = array(); 1392 foreach ($conditions_array as $k=>$v){ 1393 $k = str_replace(':','',$k); // Used for Oracle type bindings 1394 if($this->hasColumn($k)){ 1395 $v = $this->castAttributeForDatabase($k, $v); 1396 $result[$k] = $v; 1397 } 1398 } 1399 return $result; 1400 } 1401 1402 1403 /** 1404 * This functions is used to get the conditions from an AkRequest object 1405 */ 1406 function getConditions($conditions, $prefix = '', $model_name = null) 1407 { 1408 $model_name = isset($model_name) ? $model_name : $this->getModelName(); 1409 $model_conditions = !empty($conditions[$model_name]) ? $conditions[$model_name] : $conditions; 1410 if(is_a($this->$model_name)){ 1411 $model_instance =& $this->$model_name; 1412 }else{ 1413 $model_instance =& $this; 1414 } 1415 $new_conditions = array(); 1416 if(is_array($model_conditions)){ 1417 foreach ($model_conditions as $col=>$value){ 1418 if($model_instance->hasColumn($col)){ 1419 $new_conditions[$prefix.$col] = $value; 1420 } 1421 } 1422 } 1423 return $new_conditions; 1424 } 1425 1426 /** 1427 * 1428 * @access private 1429 */ 1430 function _quoteColumnName($column_name) 1431 { 1432 return $this->_db->nameQuote.$column_name.$this->_db->nameQuote; 1433 } 1434 1435 1436 1437 1438 /** 1439 * EXPERIMENTAL: Will allow to create finders when PHP includes aggregate_methods as a stable feature on PHP4, for PHP5 we might use __call 1440 * 1441 * @access private 1442 */ 1443 function _buildFinders($finderFunctions = array('find','findFirst')) 1444 { 1445 if(!$this->_dynamicMethods){ 1446 return; 1447 } 1448 $columns = !is_array($this->_dynamicMethods) ? array_keys($this->getColumns()) : $this->_dynamicMethods; 1449 $class_name = 'ak_'.md5(serialize($columns)); 1450 if(!class_exists($class_name)){ 1451 $permutations = Ak::permute($columns); 1452 $implementations = ''; 1453 foreach ($finderFunctions as $finderFunction){ 1454 foreach ($permutations as $permutation){ 1455 $permutation = array_map(array('AkInflector','camelize'),$permutation); 1456 foreach ($permutation as $k=>$v){ 1457 $method_name = $finderFunction.'By'.join($permutation,'And'); 1458 $implementation = 'function &'.$method_name.'('; 1459 $first_param = ''; 1460 $params = ''; 1461 $i = 1; 1462 foreach ($permutation as $column){ 1463 $column = AkInflector::underscore($column); 1464 $params .= "$$column, "; 1465 $first_param .= "$column "; 1466 $i++; 1467 } 1468 $implementation .= trim($params,' ,')."){\n"; 1469 $implementation .= '$options = func_num_args() == '.$i.' ? func_get_arg('.($i-1).') : array();'."\n"; 1470 $implementation .= 'return $this->'.$finderFunction.'By(\''.$first_param.'\', '.trim($params,' ,').", \$options);\n }\n"; 1471 $implementations[$method_name] = $implementation; 1472 array_shift($permutation); 1473 } 1474 } 1475 } 1476 eval('class '.$class_name.' { '.join("\n",$implementations).' } '); 1477 } 1478 1479 aggregate_methods(&$this, $class_name); 1480 } 1481 1482 1483 /** 1484 * Finder methods must instantiate through this method to work with the single-table inheritance model and 1485 * eager loading associations. 1486 * that makes it possible to create objects of different types from the same table. 1487 */ 1488 function &instantiate($record, $set_as_new = true) 1489 { 1490 $inheritance_column = $this->getInheritanceColumn(); 1491 if(!empty($record[$inheritance_column])){ 1492 $inheritance_column = $record[$inheritance_column]; 1493 $inheritance_model_name = AkInflector::camelize($inheritance_column); 1494 @require_once(AkInflector::toModelFilename($inheritance_model_name)); 1495 if(!class_exists($inheritance_model_name)){ 1496 trigger_error($this->t("The single-table inheritance mechanism failed to locate the subclass: '%class_name'. ". 1497 "This error is raised because the column '%column' is reserved for storing the class in case of inheritance. ". 1498 "Please rename this column if you didn't intend it to be used for storing the inheritance class ". 1499 "or overwrite #{self.to_s}.inheritance_column to use another column for that information.", 1500 array('%class_name'=>$inheritance_model_name, '%column'=>$this->getInheritanceColumn())),E_USER_ERROR); 1501 } 1502 } 1503 1504 $model_name = isset($inheritance_model_name) ? $inheritance_model_name : $this->getModelName(); 1505 $object =& new $model_name('attributes', $record); 1506 1507 $object->_newRecord = $set_as_new; 1508 1509 $object->afterInstantiate(); 1510 $object->notifyObservers('afterInstantiate'); 1511 1512 (AK_CLI && AK_ENVIRONMENT == 'development') ? $object ->toString() : null; 1513 1514 return $object; 1515 } 1516 1517 /*/Finding records*/ 1518 1519 1520 1521 /** 1522 Table inheritance 1523 ==================================================================== 1524 */ 1525 function descendsFromActiveRecord(&$object) 1526 { 1527 if(substr(strtolower(get_parent_class($object)),-12) == 'activerecord'){ 1528 return true; 1529 } 1530 if(!method_exists($object, 'getInheritanceColumn')){ 1531 return false; 1532 } 1533 $inheritance_column = $object->getInheritanceColumn(); 1534 return !empty($inheritance_column); 1535 } 1536 1537 /** 1538 * Gets the column name for use with single table inheritance. Can be overridden in subclasses. 1539 */ 1540 function getInheritanceColumn() 1541 { 1542 return empty($this->_inheritanceColumn) ? ($this->hasColumn('type') ? 'type' : false ) : $this->_inheritanceColumn; 1543 } 1544 1545 /** 1546 * Defines the column name for use with single table inheritance. Can be overridden in subclasses. 1547 */ 1548 function setInheritanceColumn($column_name) 1549 { 1550 if(!$this->hasColumn($column_name)){ 1551 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); 1552 return false; 1553 }elseif($this->getColumnType($column_name) != 'string'){ 1554 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); 1555 return false; 1556 }else{ 1557 $this->_inheritanceColumn = $column_name; 1558 return true; 1559 } 1560 } 1561 1562 1563 function getSubclasses() 1564 { 1565 $current_class = get_class($this); 1566 $subclasses = array(); 1567 $classes = get_declared_classes(); 1568 1569 while ($class = array_shift($classes)) { 1570 $parent_class = get_parent_class($class); 1571 if($parent_class == $current_class || in_array($parent_class,$subclasses)){ 1572 $subclasses[] = $class; 1573 }elseif(!empty($parent_class)){ 1574 $classes[] = $parent_class; 1575 } 1576 } 1577 $subclasses = array_unique(array_map(array(&$this,'_getModelName'),$subclasses)); 1578 return $subclasses; 1579 } 1580 1581 1582 function typeCondition($table_alias = null) 1583 { 1584 $inheritance_column = $this->getInheritanceColumn(); 1585 $type_condition = array(); 1586 $table_name = $this->getTableName(); 1587 $available_types = array_merge(array($this->getModelName()),$this->getSubclasses()); 1588 foreach ($available_types as $subclass){ 1589 $type_condition[] = ' '.($table_alias != null ? $table_alias : $table_name).'.'.$inheritance_column.' = \''.AkInflector::humanize(AkInflector::underscore($subclass)).'\' '; 1590 } 1591 return empty($type_condition) ? '' : '('.join('OR',$type_condition).') '; 1592 } 1593 1594 /*/Table inheritance*/ 1595 1596 1597 1598 /** 1599 Setting Attributes 1600 ==================================================================== 1601 See also: Getting Attributes, Model Attributes, Toggling Attributes, Counting Attributes. 1602 */ 1603 function setAttribute($attribute, $value, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_SETTERS, $compose_after_set = true) 1604 { 1605 if($attribute[0] == '_'){ 1606 return false; 1607 } 1608 1609 if($this->isFrozen()){ 1610 return false; 1611 } 1612 if($inspect_for_callback_child_method === true && method_exists($this,'set'.AkInflector::camelize($attribute))){ 1613 static $watchdog; 1614 $watchdog[$attribute] = @$watchdog[$attribute]+1; 1615 if($watchdog[$attribute] == 5000){ 1616 if((!defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION')) || defined('AK_ACTIVE_RECORD_PROTECT_SET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_SET_RECURSION){ 1617 trigger_error(Ak::t('You are calling recursively AkActiveRecord::setAttribute by placing parent::setAttribute() or parent::set() on your model "%method" method. In order to avoid this, set the 3rd paramenter of parent::setAttribute to FALSE. If this was the behaviour you expected, please define the constant AK_ACTIVE_RECORD_PROTECT_SET_RECURSION and set it to false',array('%method'=>'set'.AkInflector::camelize($attribute))),E_USER_ERROR); 1618 return false; 1619 } 1620 } 1621 $this->{$attribute.'_before_type_cast'} = $value; 1622 return $this->{'set'.AkInflector::camelize($attribute)}($value); 1623 } 1624 if($this->hasAttribute($attribute)){ 1625 $this->{$attribute.'_before_type_cast'} = $value; 1626 $this->$attribute = $value; 1627 if($compose_after_set && !empty($this->_combinedAttributes) && !$this->requiredForCombination($attribute)){ 1628 $combined_attributes = $this->_getCombinedAttributesWhereThisAttributeIsUsed($attribute); 1629 foreach ($combined_attributes as $combined_attribute){ 1630 $this->composeCombinedAttribute($combined_attribute); 1631 } 1632 } 1633 if ($compose_after_set && $this->isCombinedAttribute($attribute)){ 1634 $this->decomposeCombinedAttribute($attribute); 1635 } 1636 }elseif(substr($attribute,-12) == 'confirmation' && $this->hasAttribute(substr($attribute,0,-13))){ 1637 $this->$attribute = $value; 1638 } 1639 1640 if($this->_internationalize){ 1641 if(is_array($value)){ 1642 $this->setAttributeLocales($attribute, $value); 1643 }elseif(is_string($inspect_for_callback_child_method)){ 1644 $this->setAttributeByLocale($attribute, $value, $inspect_for_callback_child_method); 1645 }else{ 1646 $this->_groupInternationalizedAttribute($attribute, $value); 1647 } 1648 } 1649 return true; 1650 } 1651 1652 function set($attribute, $value = null, $inspect_for_callback_child_method = true, $compose_after_set = true) 1653 { 1654 if(is_array($attribute)){ 1655 return $this->setAttributes($attribute); 1656 } 1657 return $this->setAttribute($attribute, $value, $inspect_for_callback_child_method, $compose_after_set); 1658 } 1659 1660 /** 1661 * Allows you to set all the attributes at once by passing in an array with 1662 * keys matching the attribute names (which again matches the column names). 1663 * Sensitive attributes can be protected from this form of mass-assignment by 1664 * using the $this->setProtectedAttributes method. Or you can alternatively 1665 * specify which attributes can be accessed in with the $this->setAccessibleAttributes method. 1666 * Then all the attributes not included in that won?t be allowed to be mass-assigned. 1667 */ 1668 function setAttributes($attributes, $override_attribute_protection = false) 1669 { 1670 $this->parseAkelosArgs($attributes); 1671 if(!$override_attribute_protection){ 1672 $attributes = $this->removeAttributesProtectedFromMassAssignment($attributes); 1673 } 1674 if(!empty($attributes) && is_array($attributes)){ 1675 foreach ($attributes as $k=>$v){ 1676 $this->setAttribute($k, $v); 1677 } 1678 } 1679 } 1680 1681 1682 function setId($value) 1683 { 1684 if($this->isFrozen()){ 1685 return false; 1686 } 1687 $pk = $this->getPrimaryKey(); 1688 $this->$pk = $value; 1689 return true; 1690 } 1691 1692 1693 /*/Setting Attributes*/ 1694 1695 /** 1696 Getting Attributes 1697 ==================================================================== 1698 See also: Setting Attributes, Model Attributes, Toggling Attributes, Counting Attributes. 1699 */ 1700 1701 function getAttribute($attribute, $inspect_for_callback_child_method = AK_ACTIVE_RECORD_ENABLE_CALLBACK_GETTERS) 1702 { 1703 if($attribute[0] == '_'){ 1704 return false; 1705 } 1706 1707 if($inspect_for_callback_child_method === true && method_exists($this,'get'.AkInflector::camelize($attribute))){ 1708 static $watchdog; 1709 $watchdog[@$attribute] = @$watchdog[$attribute]+1; 1710 if($watchdog[$attribute] == 5000){ 1711 if((!defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION')) || defined('AK_ACTIVE_RECORD_PROTECT_GET_RECURSION') && AK_ACTIVE_RECORD_PROTECT_GET_RECURSION){ 1712 trigger_error(Ak::t('You are calling recursivelly AkActiveRecord::getAttribute by placing parent::getAttribute() or parent::get() on your model "%method" method. In order to avoid this, set the 2nd paramenter of parent::getAttribute to FALSE. If this was the behaviour you expected, please define the constant AK_ACTIVE_RECORD_PROTECT_GET_RECURSION and set it to false',array('%method'=>'get'.AkInflector::camelize($attribute))),E_USER_ERROR); 1713 return false; 1714 } 1715 } 1716 $value = $this->{'get'.AkInflector::camelize($attribute)}(); 1717 return $this->getInheritanceColumn() === $attribute ? AkInflector::humanize(AkInflector::underscore($value)) : $value; 1718 } 1719 if(isset($this->$attribute) || (!isset($this->$attribute) && $this->isCombinedAttribute($attribute))){ 1720 if($this->hasAttribute($attribute)){ 1721 if (!empty($this->_combinedAttributes) && $this->isCombinedAttribute($attribute)){ 1722 $this->composeCombinedAttribute($attribute); 1723 } 1724 return isset($this->$attribute) ? $this->$attribute : null; 1725 }elseif($this->_internationalize && $this->_isInternationalizeCandidate($attribute)){ 1726 if(!empty($this->$attribute) && is_string($this->$attribute)){ 1727 return $this->$attribute; 1728 } 1729 $current_locale = $this->getCurrentLocale(); 1730 if(!empty($this->$attribute[$current_locale]) && is_array($this->$attribute)){ 1731 return $this->$attribute[$current_locale]; 1732 } 1733 return $this->getAttribute($current_locale.'_'.$attribute); 1734 } 1735 } 1736 1737 if($this->_internationalize){ 1738 return $this->getAttributeByLocale($attribute, is_bool($inspect_for_callback_child_method) ? $this->getCurrentLocale() : $inspect_for_callback_child_method); 1739 } 1740 return null; 1741 } 1742 1743 function get($attribute = null, $inspect_for_callback_child_method = true) 1744 { 1745 return !isset($attribute) ? $this->getAttributes($inspect_for_callback_child_method) : $this->getAttribute($attribute, $inspect_for_callback_child_method); 1746 } 1747 1748 /** 1749 * Returns an array of all the attributes with their names as keys and clones of their objects as values in case they are objects. 1750 */ 1751 function getAttributes() 1752 { 1753 $attributes = array(); 1754 $available_attributes = $this->getAvailableAttributes(); 1755 foreach ($available_attributes as $available_attribute){ 1756 $attribute = $this->getAttribute($available_attribute['name']); 1757 $attributes[$available_attribute['name']] = AK_PHP5 && is_object($attribute) ? clone($attribute) : $attribute; 1758 } 1759 1760 if($this->_internationalize){ 1761 $current_locale = $this->getCurrentLocale(); 1762 foreach ($this->getInternationalizedColumns() as $column=>$languages){ 1763 if(empty($attributes[$column]) && isset($attributes[$current_locale.'_'.$column]) && in_array($current_locale,$languages)){ 1764 $attributes[$column] = $attributes[$current_locale.'_'.$column]; 1765 } 1766 } 1767 } 1768 1769 return $attributes; 1770 } 1771 1772 1773 /** 1774 * Every Active Record class must use "id" as their primary ID. This getter overwrites the native id method, which isn't being used in this context. 1775 */ 1776 function getId() 1777 { 1778 return $this->{$this->getPrimaryKey()}; 1779 } 1780 1781 /*/Getting Attributes*/ 1782 1783 1784 1785 /** 1786 Toggling Attributes 1787 ==================================================================== 1788 See also: Setting Attributes, Getting Attributes. 1789 */ 1790 /** 1791 * Turns an attribute that's currently true into false and vice versa. Returns attribute value. 1792 */ 1793 function toggleAttribute($attribute) 1794 { 1795 $value = $this->getAttribute($attribute); 1796 $new_value = $value ? false : true; 1797 $this->setAttribute($attribute, $new_value); 1798 return $new_value; 1799 } 1800 1801 1802 /** 1803 * Toggles the attribute and saves the record. 1804 */ 1805 function toggleAttributeAndSave($attribute) 1806 { 1807 $value = $this->toggleAttribute($attribute); 1808 if($this->updateAttribute($attribute, $value)){ 1809 return $value; 1810 } 1811 return null; 1812 } 1813 1814 /*/Toggling Attributes*/ 1815 1816 1817 /** 1818 Counting Attributes 1819 ==================================================================== 1820 See also: Counting Records, Setting Attributes, Getting Attributes. 1821 */ 1822 1823 /** 1824 * Increments the specified counter by one. So $DiscussionBoard->incrementCounter("post_count", 1825 * $discussion_board_id); would increment the "post_count" counter on the board responding to 1826 * $discussion_board_id. This is used for caching aggregate values, so that they doesn't need to 1827 * be computed every time. Especially important for looping over a collection where each element 1828 * require a number of aggregate values. Like the $DiscussionBoard that needs to list both the number of posts and comments. 1829 */ 1830 function incrementCounter($counter_name, $id, $difference = 1) 1831 { 1832 return $this->updateAll("$counter_name = $counter_name + $difference", $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 1; 1833 } 1834 1835 /** 1836 * Works like AkActiveRecord::incrementCounter, but decrements instead. 1837 */ 1838 function decrementCounter($counter_name, $id, $difference = 1) 1839 { 1840 return $this->updateAll("$counter_name = $counter_name - $difference", $this->getPrimaryKey().' = '.$this->castAttributeForDatabase($this->getPrimaryKey(), $id)) === 1; 1841 } 1842 1843 /** 1844 * Initializes the attribute to zero if null and subtracts one. Only makes sense for number-based attributes. Returns attribute value. 1845 */ 1846 function decrementAttribute($attribute) 1847 { 1848 if(!isset($this->$attribute)){ 1849 $this->$attribute = 0; 1850 } 1851 return $this->$attribute -= 1; 1852 } 1853 1854 /** 1855 * Decrements the attribute and saves the record. 1856 */ 1857 function decrementAndSaveAttribute($attribute) 1858 { 1859 return $this->updateAttribute($attribute,$this->decrementAttribute($attribute)); 1860 } 1861 1862 1863 /** 1864 * Initializes the attribute to zero if null and adds one. Only makes sense for number-based attributes. Returns attribute value. 1865 */ 1866 function incrementAttribute($attribute) 1867 { 1868 if(!isset($this->$attribute)){ 1869 $this->$attribute = 0; 1870 } 1871 return $this->$attribute += 1; 1872 } 1873 1874 /** 1875 * Increments the attribute and saves the record. 1876 */ 1877 function incrementAndSaveAttribute($attribute) 1878 { 1879 return $this->updateAttribute($attribute,$this->incrementAttribute($attribute)); 1880 } 1881 1882 /*/Counting Attributes*/ 1883 1884 /** 1885 Protecting attributes 1886 ==================================================================== 1887 */ 1888 1889 /** 1890 * If this macro is used, only those attributed named in it will be accessible 1891 * for mass-assignment, such as new ModelName($attributes) and $this->attributes($attributes). 1892 * This is the more conservative choice for mass-assignment protection. 1893 * If you'd rather start from an all-open default and restrict attributes as needed, 1894 * have a look at AkActiveRecord::setProtectedAttributes(). 1895 */ 1896 function setAccessibleAttributes() 1897 { 1898 $args = func_get_args(); 1899 $this->_accessibleAttributes = array_unique(array_merge((array)$this->_accessibleAttributes, $args)); 1900 } 1901 1902 /** 1903 * Attributes named in this macro are protected from mass-assignment, such as 1904 * new ModelName($attributes) and $this->attributes(attributes). Their assignment 1905 * will simply be ignored. Instead, you can use the direct writer methods to do assignment. 1906 * This is meant to protect sensitive attributes to be overwritten by URL/form hackers. 1907 * 1908 * Example: 1909 * <code> 1910 * class Customer extends ActiveRecord 1911 * { 1912 * function Customer() 1913 * { 1914 * $this->setProtectedAttributes('credit_rating'); 1915 * } 1916 * } 1917 * 1918 * $Customer = new Customer('name' => 'David', 'credit_rating' => 'Excellent'); 1919 * $Customer->credit_rating // => null 1920 * $Customer->attributes(array('description' => 'Jolly fellow', 'credit_rating' => 'Superb')); 1921 * $Customer->credit_rating // => null 1922 * 1923 * $Customer->credit_rating = 'Average' 1924 * $Customer->credit_rating // => 'Average' 1925 * </code> 1926 */ 1927 function setProtectedAttributes() 1928 { 1929 $args = func_get_args(); 1930 $this->_protectedAttributes = array_unique(array_merge((array)$this->_protectedAttributes, $args)); 1931 } 1932 1933 function removeAttributesProtectedFromMassAssignment($attributes) 1934 { 1935 if(!empty($this->_accessibleAttributes) && is_array($this->_accessibleAttributes) && is_array($attributes)){ 1936 foreach (array_keys($attributes) as $k){ 1937 if(!in_array($k,$this->_accessibleAttributes)){ 1938 unset($attributes[$k]); 1939 } 1940 } 1941 }elseif (!empty($this->_protectedAttributes) && is_array($this->_protectedAttributes) && is_array($attributes)){ 1942 foreach (array_keys($attributes) as $k){ 1943 if(in_array($k,$this->_protectedAttributes)){ 1944 unset($attributes[$k]); 1945 } 1946 } 1947 } 1948 return $attributes; 1949 } 1950 1951 /*/Protecting attributes*/ 1952 1953 1954 /** 1955 Model Attributes 1956 ==================================================================== 1957 See also: Getting Attributes, Setting Attributes. 1958 */ 1959 /** 1960 * Returns an array of all the attributes that have been specified for serialization as keys and the objects as values. 1961 */ 1962 function getSerializedAttributes() 1963 { 1964 return isset($this->_serializedAttributes) ? $this->_serializedAttributes : array(); 1965 } 1966 1967 function getAvailableAttributes() 1968 { 1969 return array_merge($this->getColumns(), $this->getAvailableCombinedAttributes()); 1970 } 1971 1972 function