| [ 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 getAttributeCaption($attribute) 1973 { 1974 return $this->t(AkInflector::humanize($attribute)); 1975 } 1976 1977 /** 1978 * This function is useful in case you need to know if attributes have been assigned to an object. 1979 */ 1980 function hasAttributesDefined() 1981 { 1982 $attributes = join('',$this->getAttributes()); 1983 return empty($attributes); 1984 } 1985 1986 1987 /** 1988 * Returns the primary key field. 1989 */ 1990 function getPrimaryKey() 1991 { 1992 if(!isset($this->_primaryKey)){ 1993 $this->setPrimaryKey(); 1994 } 1995 return $this->_primaryKey; 1996 } 1997 1998 function getColumnNames() 1999 { 2000 if(empty($this->_columnNames)){ 2001 $columns = $this->getColumns(); 2002 foreach ($columns as $column_name=>$details){ 2003 $this->_columnNames[$column_name] = isset($details->columnName) ? $this->t($details->columnName) : $this->getAttributeCaption($column_name); 2004 } 2005 } 2006 return $this->_columnNames; 2007 } 2008 2009 2010 /** 2011 * Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count", 2012 * and columns used for single table inheritance has been removed. 2013 */ 2014 function getContentColumns() 2015 { 2016 $inheritance_column = $this->getInheritanceColumn(); 2017 $columns = $this->getColumns(); 2018 foreach ($columns as $name=>$details){ 2019 if((substr($name,-3) == '_id' || substr($name,-6) == '_count') || 2020 !empty($details['primaryKey']) || ($inheritance_column !== false && $inheritance_column == $name)){ 2021 unset($columns[$name]); 2022 } 2023 } 2024 return $columns; 2025 } 2026 2027 /** 2028 * Returns an array of names for the attributes available on this object sorted alphabetically. 2029 */ 2030 function getAttributeNames() 2031 { 2032 if(!isset($this->_activeRecordHasBeenInstantiated)){ 2033 return Ak::handleStaticCall(); 2034 } 2035 $attributes = array_keys($this->getAvailableAttributes()); 2036 $names = array_combine($attributes,array_map(array(&$this,'getAttributeCaption'), $attributes)); 2037 natsort($names); 2038 return $names; 2039 } 2040 2041 2042 /** 2043 * Returns true if the specified attribute has been set by the user or by a database load and is neither null nor empty? 2044 */ 2045 function isAttributePresent($attribute) 2046 { 2047 $value = $this->getAttribute($attribute); 2048 return !empty($value); 2049 } 2050 2051 /** 2052 * Returns true if given attribute exists for this Model. 2053 * 2054 * @param string $attribute 2055 * @return boolean 2056 */ 2057 function hasAttribute ($attribute) 2058 { 2059 empty($this->_columns) ? $this->getColumns() : $this->_columns; // HINT: only used by HasAndBelongsToMany joinObjects, if the table is not present yet! 2060 return isset($this->_columns[$attribute]) || (!empty($this->_combinedAttributes) && $this->isCombinedAttribute($attribute)); 2061 } 2062 2063 /*/Model Attributes*/ 2064 2065 2066 /** 2067 Combined attributes 2068 ==================================================================== 2069 * 2070 * The Akelos Framework has a handy way to represent combined fields. 2071 * You can add a new attribute to your models using a printf patter to glue 2072 * multiple parameters in a single one. 2073 * 2074 * For example, If we set... 2075 * $this->addCombinedAttributeConfiguration('name', "%s %s", 'first_name', 'last_name'); 2076 * $this->addCombinedAttributeConfiguration('date', "%04d-%02d-%02d", 'year', 'month', 'day'); 2077 * $this->setAttributes('first_name=>','John','last_name=>','Smith','year=>',2005,'month=>',9,'day=>',27); 2078 * 2079 * $this->name // will have "John Smith" as value and 2080 * $this->date // will be 2005-09-27 2081 * 2082 * On the other hand if you do 2083 * 2084 * $this->setAttribute('date', '2008-11-30'); 2085 * 2086 * All the 'year', 'month' and 'day' getters will be fired (if they exist) the following attributes will be set 2087 * 2088 * $this->year // will be 2008 2089 * $this->month // will be 11 and 2090 * $this->day // will be 27 2091 * 2092 * Sometimes you might need a pattern for composing and another for decomposing attributes. In this case you can specify 2093 * an array as the pattern values, where first element will be the composing pattern and second element will be used 2094 * for decomposing. 2095 * 2096 * You can also specify a callback method from this object function instead of a pattern. You can also assign a callback 2097 * for composing and another for decomposing by passing their names as an array like on the patterns. 2098 * 2099 * <?php 2100 * class User extends ActiveRecord 2101 * { 2102 * function User() 2103 * { 2104 * // You can use a multiple patterns array where "%s, %s" will be used for combining fields and "%[^,], %s" will be used 2105 * // for decomposing fields. (as you can see you can also use regular expressions on your patterns) 2106 * $User->addCombinedAttributeConfiguration('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name'); 2107 * 2108 * //Here we set email_link so compose_email_link() will be triggered for building up the field and parse_email_link will 2109 * // be used for getting the fields out 2110 * $User->addCombinedAttributeConfiguration('email_link', array("compose_email_link","parse_email_link"), 'email', 'name'); 2111 * 2112 * // We need to tell the ActiveRecord to load it's magic (see the example below for a simpler solution) 2113 * $attributes = (array)func_get_args(); 2114 * return $this->init($attributes); 2115 * 2116 * } 2117 * function compose_email_link() 2118 * { 2119 * $args = func_get_arg(0); 2120 * return "<a href=\'mailto:{$args[\'email\']}\'>{$args[\'name\']}</a>"; 2121 * } 2122 * function parse_email_link($email_link) 2123 * { 2124 * $results = sscanf($email_link, "<a href=\'mailto:%[^\']\'>%[^<]</a>"); 2125 * return array(\'email\'=>$results[0],\'name\'=>$results[1]); 2126 * } 2127 * 2128 * } 2129 * ?> 2130 * 2131 * You can also simplify your live by declaring the combined attributes as a class variable like: 2132 * <?php 2133 * class User extends ActiveRecord 2134 * { 2135 * var $combined_attributes array( 2136 * array('name', array("%s, %s","%[^,], %s"), 'last_name', 'first_name') 2137 * array('email_link', array("compose_email_link","parse_email_link"), 'email', 'name') 2138 * ); 2139 * 2140 * // .... 2141 * } 2142 * ?> 2143 * 2144 */ 2145 2146 /** 2147 * Returns true if given attribute is a combined attribute for this Model. 2148 * 2149 * @param string $attribute 2150 * @return boolean 2151 */ 2152 function isCombinedAttribute ($attribute) 2153 { 2154 return !empty($this->_combinedAttributes) && isset($this->_combinedAttributes[$attribute]); 2155 } 2156 2157 function addCombinedAttributeConfiguration($attribute) 2158 { 2159 $args = is_array($attribute) ? $attribute : func_get_args(); 2160 $columns = array_slice($args,2); 2161 $invalid_columns = array(); 2162 foreach ($columns as $colum){ 2163 if(!$this->hasAttribute($colum)){ 2164 $invalid_columns[] = $colum; 2165 } 2166 } 2167 if(!empty($invalid_columns)){ 2168 trigger_error(Ak::t('There was an error while setting the composed field "%field_name", the following mapping column/s "%columns" do not exist', 2169 array('%field_name'=>$args[0],'%columns'=>join(', ',$invalid_columns))), E_USER_ERROR); 2170 }else{ 2171 $attribute = array_shift($args); 2172 $this->_combinedAttributes[$attribute] = $args; 2173 $this->composeCombinedAttribute($attribute); 2174 } 2175 } 2176 2177 function composeCombinedAttributes() 2178 { 2179 2180 if(!empty($this->_combinedAttributes)){ 2181 $attributes = array_keys($this->_combinedAttributes); 2182 foreach ($attributes as $attribute){ 2183 $this->composeCombinedAttribute($attribute); 2184 } 2185 } 2186 } 2187 2188 function composeCombinedAttribute($combined_attribute) 2189 { 2190 if($this->isCombinedAttribute($combined_attribute)){ 2191 $config = $this->_combinedAttributes[$combined_attribute]; 2192 $pattern = array_shift($config); 2193 2194 $pattern = is_array($pattern) ? $pattern[0] : $pattern; 2195 $got = array(); 2196 2197 foreach ($config as $attribute){ 2198 if(isset($this->$attribute)){ 2199 $got[$attribute] = $this->getAttribute($attribute); 2200 } 2201 } 2202 if(count($got) === count($config)){ 2203 $this->$combined_attribute = method_exists($this, $pattern) ? $this->{$pattern}($got) : vsprintf($pattern, $got); 2204 } 2205 } 2206 } 2207 2208 /** 2209 * @access private 2210 */ 2211 function _getCombinedAttributesWhereThisAttributeIsUsed($attribute) 2212 { 2213 $result = array(); 2214 foreach ($this->_combinedAttributes as $combined_attribute=>$settings){ 2215 if(in_array($attribute,$settings)){ 2216 $result[] = $combined_attribute; 2217 } 2218 } 2219 return $result; 2220 } 2221 2222 2223 function requiredForCombination($attribute) 2224 { 2225 foreach ($this->_combinedAttributes as $settings){ 2226 if(in_array($attribute,$settings)){ 2227 return true; 2228 } 2229 } 2230 return false; 2231 } 2232 2233 function hasCombinedAttributes() 2234 { 2235 return count($this->getCombinedSubattributes()) === 0 ? false :true; 2236 } 2237 2238 function getCombinedSubattributes($attribute) 2239 { 2240 $result = array(); 2241 if(is_array($this->_combinedAttributes[$attribute])){ 2242 $attributes = $this->_combinedAttributes[$attribute]; 2243 array_shift($attributes); 2244 foreach ($attributes as $attribute_to_check){ 2245 if(isset($this->_combinedAttributes[$attribute_to_check])){ 2246 $result[] = $attribute_to_check; 2247 } 2248 } 2249 } 2250 return $result; 2251 } 2252 2253 function decomposeCombinedAttributes() 2254 { 2255 if(!empty($this->_combinedAttributes)){ 2256 $attributes = array_keys($this->_combinedAttributes); 2257 foreach ($attributes as $attribute){ 2258 $this->decomposeCombinedAttribute($attribute); 2259 } 2260 } 2261 } 2262 2263 function decomposeCombinedAttribute($combined_attribute, $used_on_combined_fields = false) 2264 { 2265 if(isset($this->$combined_attribute) && $this->isCombinedAttribute($combined_attribute)){ 2266 $config = $this->_combinedAttributes[$combined_attribute]; 2267 $pattern = array_shift($config); 2268 $pattern = is_array($pattern) ? $pattern[1] : $pattern; 2269 2270 if(method_exists($this, $pattern)){ 2271 $pieces = $this->{$pattern}($this->$combined_attribute); 2272 if(is_array($pieces)){ 2273 foreach ($pieces as $k=>$v){ 2274 $is_combined = $this->isCombinedAttribute($k); 2275 if($is_combined){ 2276 $this->decomposeCombinedAttribute($k); 2277 } 2278 $this->setAttribute($k, $v, true, !$is_combined); 2279 } 2280 if($is_combined && !$used_on_combined_fields){ 2281 $combined_attributes_contained_on_this_attribute = $this->getCombinedSubattributes($combined_attribute); 2282 if(count($combined_attributes_contained_on_this_attribute)){ 2283 $this->decomposeCombinedAttribute($combined_attribute, true); 2284 } 2285 } 2286 } 2287 }else{ 2288 $got = sscanf($this->$combined_attribute, $pattern); 2289 for ($x=0; $x<count($got); $x++){ 2290 $attribute = $config[$x]; 2291 $is_combined = $this->isCombinedAttribute($attribute); 2292 if($is_combined){ 2293 $this->decomposeCombinedAttribute($attribute); 2294 } 2295 $this->setAttribute($attribute, $got[$x], true, !$is_combined); 2296 } 2297 } 2298 } 2299 } 2300 2301 function getAvailableCombinedAttributes() 2302 { 2303 $combined_attributes = array(); 2304 foreach ($this->_combinedAttributes as $attribute=>$details){ 2305 $combined_attributes[$attribute] = array('name'=>$attribute, 'type'=>'string', 'path' => array_shift($details), 'uses'=>$details); 2306 } 2307 return !empty($this->_combinedAttributes) && is_array($this->_combinedAttributes) ? $combined_attributes : array(); 2308 } 2309 2310 /*/Combined attributes*/ 2311 2312 2313 2314 2315 /** 2316 Database connection 2317 ==================================================================== 2318 */ 2319 /** 2320 * Establishes the connection to the database. Accepts either a profile name specified in config/config.php or 2321 * an array as input where the 'type' key must be specified with the name of a database adapter (in lower-case) 2322 * example for regular databases (MySQL, Postgresql, etc): 2323 * 2324 * $AkActiveRecord->establishConnection('development'); 2325 * $AkActiveRecord->establishConnection('super_user'); 2326 * 2327 * $AkActiveRecord->establishConnection( 2328 * array( 2329 * 'type' => "mysql", 2330 * 'host' => "localhost", 2331 * 'username' => "myuser", 2332 * 'password' => "mypass", 2333 * 'database' => "somedatabase" 2334 * )); 2335 * 2336 * Example for SQLite database: 2337 * 2338 * $AkActiveRecord->establishConnection( 2339 * array( 2340 * 'type' => "sqlite", 2341 * 'dbfile' => "path/to/dbfile" 2342 * ) 2343 * ) 2344 */ 2345 function &establishConnection($specification_or_profile = AK_DEFAULT_DATABASE_PROFILE) 2346 { 2347 $adapter =& AkDbAdapter::getInstance($specification_or_profile); 2348 return $this->setConnection(&$adapter); 2349 } 2350 2351 2352 /** 2353 * Returns true if a connection that's accessible to this class have already been opened. 2354 */ 2355 function isConnected() 2356 { 2357 return isset($this->_db); 2358 } 2359 2360 /** 2361 * Returns the connection currently associated with the class. This can also be used to 2362 * "borrow" the connection to do database work unrelated to any of the specific Active Records. 2363 */ 2364 function &getConnection() 2365 { 2366 return $this->_db; 2367 } 2368 2369 /** 2370 * Sets the connection for the class. 2371 */ 2372 function &setConnection($db_adapter = null) 2373 { 2374 if (is_null($db_adapter)){ 2375 $db_adapter =& AkDbAdapter::getInstance(); 2376 } 2377 return $this->_db =& $db_adapter; 2378 } 2379 2380 /** 2381 * @access private 2382 */ 2383 function _getDatabaseType() 2384 { 2385 return $this->_db->type(); 2386 } 2387 /*/Database connection*/ 2388 2389 2390 /** 2391 Table Settings 2392 ==================================================================== 2393 See also: Database Reflection. 2394 */ 2395 2396 /** 2397 * Defines the primary key field ? can be overridden in subclasses. 2398 */ 2399 function setPrimaryKey($primary_key = 'id') 2400 { 2401 if(!$this->hasColumn($primary_key)){ 2402 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); 2403 }else { 2404 $this->_primaryKey = $primary_key; 2405 } 2406 } 2407 2408 2409 function getTableName($modify_for_associations = true) 2410 { 2411 if(!isset($this->_tableName)){ 2412 // We check if we are on a inheritance Table Model 2413 $this->getClassForDatabaseTableMapping(); 2414 if(!isset($this->_tableName)){ 2415 $this->setTableName(); 2416 } 2417 } 2418 2419 if($modify_for_associations && isset($this->_associationTablePrefixes[$this->_tableName])){ 2420 return $this->_associationTablePrefixes[$this->_tableName]; 2421 } 2422 2423 return $this->_tableName; 2424 } 2425 2426 function setTableName($table_name = null, $check_for_existence = AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES, $check_mode = false) 2427 { 2428 static $available_tables; 2429 if(empty($table_name)){ 2430 $table_name = AkInflector::tableize($this->getModelName()); 2431 } 2432 if($check_for_existence){ 2433 if(!isset($available_tables) || $check_mode){ 2434 if(!isset($this->_db)){ 2435 $this->setConnection(); 2436 } 2437 if (!AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA || ($available_tables = AkDbSchemaCache::getAvailableTables()) === false) { 2438 $available_tables = $this->_db->availableTables(); 2439 if (AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA) { 2440 AkDbSchemaCache::setAvailableTables($available_tables); 2441 } 2442 } 2443 } 2444 if(!in_array($table_name,(array)$available_tables)){ 2445 if(!$check_mode){ 2446 trigger_error(Ak::t('Unable to set "%table_name" table for the model "%model".'. 2447 ' There is no "%table_name" available into current database layout.'. 2448 ' Set AK_ACTIVE_RECORD_VALIDATE_TABLE_NAMES constant to false in order to'. 2449 ' avoid table name validation',array('%table_name'=>$table_name,'%model'=>$this->getModelName())),E_USER_WARNING); 2450 } 2451 return false; 2452 } 2453 } 2454 $this->_tableName = $table_name; 2455 return true; 2456 } 2457 2458 2459 function getOnlyAvailableAttributes($attributes) 2460 { 2461 $table_name = $this->getTableName(); 2462 $ret_attributes = array(); 2463 if(!empty($attributes) && is_array($attributes)){ 2464 $available_attributes = $this->getAvailableAttributes(); 2465 2466 $keys = array_keys($attributes); 2467 $size = sizeOf($keys); 2468 for ($i=0; $i < $size; $i++){ 2469 $k = str_replace($table_name.'.','',$keys[$i]); 2470 if(isset($available_attributes[$k]['name'][$k])){ 2471 $ret_attributes[$k] =& $attributes[$keys[$i]]; 2472 } 2473 } 2474 } 2475 return $ret_attributes; 2476 } 2477 2478 function getColumnsForAttributes($attributes) 2479 { 2480 $ret_attributes = array(); 2481 $table_name = $this->getTableName(); 2482 if(!empty($attributes) && is_array($attributes)){ 2483 $columns = $this->getColumns(); 2484 foreach ($attributes as $k=>$v){ 2485 $k = str_replace($table_name.'.','',$k); 2486 if(isset($columns[$k]['name'][$k])){ 2487 $ret_attributes[$k] = $v; 2488 } 2489 } 2490 } 2491 return $ret_attributes; 2492 } 2493 2494 /** 2495 * Returns true if given attribute exists for this Model. 2496 * 2497 * @param string $name Name of table to look in 2498 * @return boolean 2499 */ 2500 function hasColumn($column) 2501 { 2502 empty($this->_columns) ? $this->getColumns() : $this->_columns; 2503 return isset($this->_columns[$column]); 2504 } 2505 2506 2507 /*/Table Settings*/ 2508 2509 /** 2510 Database Reflection 2511 ==================================================================== 2512 See also: Table Settings, Type Casting. 2513 */ 2514 2515 2516 /** 2517 * Initializes the attributes array with keys matching the columns from the linked table and 2518 * the values matching the corresponding default value of that column, so 2519 * that a new instance, or one populated from a passed-in array, still has all the attributes 2520 * that instances loaded from the database would. 2521 */ 2522 function attributesFromColumnDefinition() 2523 { 2524 $attributes = array(); 2525 2526 foreach ((array)$this->getColumns() as $column_name=>$column_settings){ 2527 if (!isset($column_settings['primaryKey']) && isset($column_settings['hasDefault'])) { 2528 $attributes[$column_name] = $this->_extractValueFromDefault($column_settings['defaultValue']); 2529 } else { 2530 $attributes[$column_name] = null; 2531 } 2532 } 2533 return $attributes; 2534 } 2535 2536 /** 2537 * Gets information from the database engine about a single table 2538 * 2539 * @access private 2540 */ 2541 function _databaseTableInternals($table) 2542 { 2543 if (!AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA || ($cache = AkDbSchemaCache::getDbTableInternals($table))===false) { 2544 $cache = $this->_db->getColumnDetails($table); 2545 if (AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA) { 2546 AkDbSchemaCache::setDbTableInternals($table,$cache); 2547 } 2548 } 2549 return $cache; 2550 } 2551 2552 function getColumnsWithRegexBoundaries() 2553 { 2554 $columns = array_keys($this->getColumns()); 2555 foreach ($columns as $k=>$column){ 2556 $columns[$k] = '/([^\.])\b('.$column.')\b/'; 2557 } 2558 return $columns; 2559 } 2560 2561 2562 /** 2563 * If is the first time we use a model this function will run the installer for the model if it exists 2564 * 2565 * @access private 2566 */ 2567 function _runCurrentModelInstallerIfExists(&$column_objects) 2568 { 2569 static $installed_models = array(); 2570 if(!defined('AK_AVOID_AUTOMATIC_ACTIVE_RECORD_INSTALLERS') && !in_array($this->getModelName(), $installed_models)){ 2571 $installed_models[] = $this->getModelName(); 2572 require_once (AK_LIB_DIR.DS.'AkInstaller.php'); 2573 $installer_name = $this->getModelName().'Installer'; 2574 $installer_file = AK_APP_DIR.DS.'installers'.DS.AkInflector::underscore($installer_name).'.php'; 2575 if(file_exists($installer_file)){ 2576 require_once($installer_file); 2577 if(class_exists($installer_name)){ 2578 $Installer = new $installer_name(); 2579 if(method_exists($Installer,'install')){ 2580 $Installer->install(); 2581 $column_objects = $this->_databaseTableInternals($this->getTableName()); 2582 return !empty($column_objects); 2583 } 2584 } 2585 } 2586 } 2587 return false; 2588 } 2589 2590 2591 /** 2592 * Returns an array of column objects for the table associated with this class. 2593 */ 2594 function getColumns($force_reload = false) 2595 { 2596 if(empty($this->_columns) || $force_reload){ 2597 $this->_columns = $this->getColumnSettings($force_reload); 2598 } 2599 2600 return (array)$this->_columns; 2601 } 2602 2603 function getColumnSettings($force_reload = false) 2604 { 2605 if(empty($this->_columnsSettings) || $force_reload){ 2606 $this->loadColumnsSettings($force_reload); 2607 $this->initiateColumnsToNull(); 2608 } 2609 return isset($this->_columnsSettings) ? $this->_columnsSettings : array(); 2610 } 2611 2612 function loadColumnsSettings($force_reload = false) 2613 { 2614 if(is_null($this->_db)){ 2615 $this->setConnection(); 2616 } 2617 $this->_columnsSettings = $force_reload ? null : $this->_getPersistedTableColumnSettings(); 2618 2619 if(empty($this->_columnsSettings) || !AK_ACTIVE_RECORD_ENABLE_PERSISTENCE){ 2620 if(empty($this->_dataDictionary)){ 2621 $this->_dataDictionary =& $this->_db->getDictionary(); 2622 } 2623 2624 $column_objects = $this->_databaseTableInternals($this->getTableName()); 2625 2626 if( !isset($this->_avoidTableNameValidation) && 2627 !is_array($column_objects) && 2628 !$this->_runCurrentModelInstallerIfExists($column_objects)){ 2629 trigger_error(Ak::t('Ooops! Could not fetch details for the table %table_name.', array('%table_name'=>$this->getTableName())), E_USER_ERROR); 2630 return false; 2631 }elseif (empty($column_objects)){ 2632 $this->_runCurrentModelInstallerIfExists($column_objects); 2633 } 2634 if(is_array($column_objects)){ 2635 foreach (array_keys($column_objects) as $k){ 2636 $this->setColumnSettings($column_objects[$k]->name, $column_objects[$k]); 2637 } 2638 } 2639 if(!empty($this->_columnsSettings) && AK_ACTIVE_RECORD_CACHE_DATABASE_SCHEMA){ 2640 $this->_persistTableColumnSettings(); 2641 } 2642 } 2643 return isset($this->_columnsSettings) ? $this->_columnsSettings : array(); 2644 } 2645 2646 2647 2648 function setColumnSettings($column_name, $column_object) 2649 { 2650 $this->_columnsSettings[$column_name] = array(); 2651 $this->_columnsSettings[$column_name]['name'] = $column_object->name; 2652 2653 if($this->_internationalize && $this->_isInternationalizeCandidate($column_object->name)){ 2654 $this->_addInternationalizedColumn($column_object->name); 2655 } 2656 2657 $this->_columnsSettings[$column_name]['type'] = $this->getAkelosDataType($column_object); 2658 if(!empty($column_object->primary_key)){ 2659 $this->_primaryKey = empty($this->_primaryKey) ? $column_object->name : $this->_primaryKey; 2660 $this->_columnsSettings[$column_name]['primaryKey'] = true; 2661 } 2662 if(!empty($column_object->auto_increment)){ 2663 $this->_columnsSettings[$column_name]['autoIncrement'] = true; 2664 } 2665 if(!empty($column_object->has_default)){ 2666 $this->_columnsSettings[$column_name]['hasDefault'] = true; 2667 } 2668 if(!empty($column_object->not_null)){ 2669 $this->_columnsSettings[$column_name]['notNull'] = true; 2670 } 2671 if(!empty($column_object->max_length) && $column_object->max_length > 0){ 2672 $this->_columnsSettings[$column_name]['maxLength'] = $column_object->max_length; 2673 } 2674 if(!empty($column_object->scale) && $column_object->scale > 0){ 2675 $this->_columnsSettings[$column_name]['scale'] = $column_object->scale; 2676 } 2677 if(isset($column_object->default_value)){ 2678 $this->_columnsSettings[$column_name]['defaultValue'] = $column_object->default_value; 2679 } 2680 } 2681 2682 2683 /** 2684 * Resets all the cached information about columns, which will cause they to be reloaded on the next request. 2685 */ 2686 function resetColumnInformation() 2687 { 2688 $this->_clearPersitedColumnSettings(); 2689 $this->_columnNames = $this->_columns = $this->_columnsSettings = $this->_contentColumns = array(); 2690 } 2691 2692 /** 2693 * @access private 2694 */ 2695 function _getColumnsSettings() 2696 { 2697 return AkDbSchemaCache::getColumnsSettings(); 2698 } 2699 2700 /** 2701 * @access private 2702 */ 2703 function _getModelColumnSettings() 2704 { 2705 return AkDbSchemaCache::getModelColumnSettings($this->getModelName()); 2706 2707 } 2708 2709 /** 2710 * @access private 2711 */ 2712 function _persistTableColumnSettings() 2713 { 2714 AkDbSchemaCache::setModelColumnSettings($this->getModelName(), $this->_columnsSettings); 2715 } 2716 2717 /** 2718 * @access private 2719 */ 2720 function _getPersistedTableColumnSettings() 2721 { 2722 return AkDbSchemaCache::getModelColumnSettings($this->getModelName()); 2723 2724 } 2725 2726 /** 2727 * @access private 2728 */ 2729 function _clearPersitedColumnSettings() 2730 { 2731 AkDbSchemaCache::clear($this->getModelName()); 2732 } 2733 2734 2735 2736 function initiateAttributeToNull($attribute) 2737 { 2738 if(!isset($this->$attribute)){ 2739 $this->$attribute = null; 2740 } 2741 } 2742 2743 function initiateColumnsToNull() 2744 { 2745 if(isset($this->_columnsSettings) && is_array($this->_columnsSettings)){ 2746 array_map(array(&$this,'initiateAttributeToNull'),array_keys($this->_columnsSettings)); 2747 } 2748 } 2749 2750 2751 /** 2752 * Akelos data types are mapped to phpAdodb data types 2753 * 2754 * Returns the Akelos data type for an Adodb Column Object 2755 * 2756 * 'C'=>'string', // Varchar, capped to 255 characters. 2757 * 'X' => 'text' // Larger varchar, capped to 4000 characters (to be compatible with Oracle). 2758 * 'XL' => 'text' // For Oracle, returns CLOB, otherwise the largest varchar size. 2759 * 2760 * 'C2' => 'string', // Multibyte varchar 2761 * 'X2' => 'string', // Multibyte varchar (largest size) 2762 * 2763 * 'B' => 'binary', // BLOB (binary large object) 2764 * 2765 * 'D' => array('date', 'datetime'), // Date (some databases do not support this, and we return a datetime type) 2766 * 'T' => array('datetime', 'timestamp'), //Datetime or Timestamp 2767 * 'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1) 2768 * 'I' => // Integer (mapped to I4) 2769 * 'I1' => 'integer', // 1-byte integer 2770 * 'I2' => 'integer', // 2-byte integer 2771 * 'I4' => 'integer', // 4-byte integer 2772 * 'I8' => 'integer', // 8-byte integer 2773 * 'F' => 'float', // Floating point number 2774 * 'N' => 'integer' // Numeric or decimal number 2775 * 2776 * @return string One of this 'string','text','integer','float','datetime','timestamp', 2777 * 'time', 'name','date', 'binary', 'boolean' 2778 */ 2779 function getAkelosDataType(&$adodb_column_object) 2780 { 2781 $config_var_name = AkInflector::variablize($adodb_column_object->name.'_data_type'); 2782 if(!empty($this->{$config_var_name})){ 2783 return $this->{$config_var_name}; 2784 } 2785 if(stristr($adodb_column_object->type, 'BLOB')){ 2786 return 'binary'; 2787 } 2788 if(!empty($adodb_column_object->auto_increment)) { 2789 return 'serial'; 2790 } 2791 $meta_type = $this->_dataDictionary->MetaType($adodb_column_object); 2792 $adodb_data_types = array( 2793 'C'=>'string', // Varchar, capped to 255 characters. 2794 'X' => 'text', // Larger varchar, capped to 4000 characters (to be compatible with Oracle). 2795 'XL' => 'text', // For Oracle, returns CLOB, otherwise the largest varchar size. 2796 2797 'C2' => 'string', // Multibyte varchar 2798 'X2' => 'string', // Multibyte varchar (largest size) 2799 2800 'B' => 'binary', // BLOB (binary large object) 2801 2802 'D' => array('date'), // Date 2803 'T' => array('datetime', 'timestamp'), //Datetime or Timestamp 2804 'L' => 'boolean', // Integer field suitable for storing booleans (0 or 1) 2805 'R' => 'serial', // Serial Integer 2806 'I' => 'integer', // Integer (mapped to I4) 2807 'I1' => 'integer', // 1-byte integer 2808 'I2' => 'integer', // 2-byte integer 2809 'I4' => 'integer', // 4-byte integer 2810 'I8' => 'integer', // 8-byte integer 2811 'F' => 'float', // Floating point number 2812 'N' => 'decimal' // Numeric or decimal number 2813 ); 2814 2815 $result = !isset($adodb_data_types[$meta_type]) ? 2816 'string' : 2817 (is_array($adodb_data_types[$meta_type]) ? $adodb_data_types[$meta_type][0] : $adodb_data_types[$meta_type]); 2818 2819 if($result == 'text'){ 2820 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 )){ 2821 return 'string'; 2822 } 2823 } 2824 2825 if($this->_getDatabaseType() == 'mysql'){ 2826 if($result == 'integer' && stristr($adodb_column_object->type, 'TINYINT')){ 2827 return 'boolean'; 2828 } 2829 }elseif($this->_getDatabaseType() == 'postgre'){ 2830 if($adodb_column_object->type == 'timestamp' || $result == 'datetime'){ 2831 $adodb_column_object->max_length = 19; 2832 } 2833 }elseif($this->_getDatabaseType() == 'sqlite'){ 2834 if($result == 'integer' && (int)$adodb_column_object->max_length === 1 && stristr($adodb_column_object->type, 'TINYINT')){ 2835 return 'boolean'; 2836 }elseif($result == 'integer' && stristr($adodb_column_object->type, 'DOUBLE')){ 2837 return 'float'; 2838 } 2839 } 2840 2841 if($result == 'datetime' && substr($adodb_column_object->name,-3) == '_on'){ 2842 $result = 'date'; 2843 } 2844 2845 return $result; 2846 } 2847 2848 2849 /** 2850 * This method retrieves current class name that will be used to map 2851 * your database to this object. 2852 */ 2853 function getClassForDatabaseTableMapping() 2854 { 2855 $class_name = get_class($this); 2856 if(is_subclass_of($this,'akactiverecord') || is_subclass_of($this,'AkActiveRecord')){ 2857 $parent_class = get_parent_class($this); 2858 while (substr(strtolower($parent_class),-12) != 'activerecord'){ 2859 $class_name = $parent_class; 2860 $parent_class = get_parent_class($parent_class); 2861 } 2862 } 2863 2864 $class_name = $this->_getModelName($class_name); 2865 // This is an Active Record Inheritance so we set current table to parent table. 2866 if(!empty($class_name) && strtolower($class_name) != 'activerecord'){ 2867 $this->_inheritanceClassName = $class_name; 2868 @$this->setTableName(AkInflector::tableize($class_name), false); 2869 } 2870 2871 return $class_name; 2872 } 2873 2874 function getDisplayField() 2875 { 2876 return empty($this->displayField) && $this->hasAttribute('name') ? 'name' : (isset($this->displayField) && $this->hasAttribute($this->displayField) ? $this->displayField : $this->getPrimaryKey()); 2877 } 2878 2879 function setDisplayField($attribute_name) 2880 { 2881 if($this->hasAttribute($attribute_name)){ 2882 $this->displayField = $attribute_name; 2883 return true; 2884 }else { 2885 return false; 2886 } 2887 } 2888 2889 2890 2891 2892 /*/Database Reflection*/ 2893 2894 /** 2895 Localization 2896 ==================================================================== 2897 */ 2898 2899 function t($string, $array = null) 2900 { 2901 return Ak::t($string, $array, AkInflector::underscore($this->getModelName())); 2902 } 2903 2904 function getInternationalizedColumns() 2905 { 2906 static $cache; 2907 $model = $this->getModelName(); 2908 $available_locales = $this->getAvailableLocales(); 2909 if(empty($cache[$model])){ 2910 $cache[$model] = array(); 2911 foreach ($this->getColumnSettings() as $column_name=>$details){ 2912 if(!empty($details['i18n'])){ 2913 $_tmp_pos = strpos($column_name,'_'); 2914 $column = substr($column_name,$_tmp_pos+1); 2915 $lang = substr($column_name,0,$_tmp_pos); 2916 if(in_array($lang, $available_locales)){ 2917 $cache[$model][$column] = empty($cache[$model][$column]) ? array($lang) : 2918 array_merge($cache[$model][$column] ,array($lang)); 2919 } 2920 } 2921 } 2922 } 2923 2924 return $cache[$model]; 2925 } 2926 2927 function getAvailableLocales() 2928 { 2929 static $available_locales; 2930 if(empty($available_locales)){ 2931 if(defined('AK_ACTIVE_RECORD_DEFAULT_LOCALES')){ 2932 $available_locales = Ak::stringToArray(AK_ACTIVE_RECORD_DEFAULT_LOCALES); 2933 }else{ 2934 $available_locales = Ak::langs(); 2935 } 2936 } 2937 return $available_locales; 2938 } 2939 2940 function getCurrentLocale() 2941 { 2942 static $current_locale; 2943 if(empty($current_locale)){ 2944 $current_locale = Ak::lang(); 2945 $available_locales = $this->getAvailableLocales(); 2946 if(!in_array($current_locale, $available_locales)){ 2947 $current_locale = array_shift($available_locales); 2948 } 2949 } 2950 return $current_locale; 2951 } 2952 2953 2954 function getAttributeByLocale($attribute, $locale) 2955 { 2956 $internationalizable_columns = $this->getInternationalizedColumns(); 2957 if(!empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){ 2958 return $this->getAttribute($locale.'_'.$attribute); 2959 } 2960 } 2961 2962 function getAttributeLocales($attribute) 2963 { 2964 $attribute_locales = array(); 2965 foreach ($this->getAvailableLocales() as $locale){ 2966 if($this->hasColumn($locale.'_'.$attribute)){ 2967 $attribute_locales[$locale] = $this->getAttributeByLocale($attribute, $locale); 2968 } 2969 } 2970 return $attribute_locales; 2971 } 2972 2973 function setAttributeByLocale($attribute, $value, $locale) 2974 { 2975 $internationalizable_columns = $this->getInternationalizedColumns(); 2976 2977 if($this->_isInternationalizeCandidate($locale.'_'.$attribute) && !empty($internationalizable_columns[$attribute]) && is_array($internationalizable_columns[$attribute]) && in_array($locale, $internationalizable_columns[$attribute])){ 2978 $this->setAttribute($locale.'_'.$attribute, $value); 2979 } 2980 2981 } 2982 2983 function setAttributeLocales($attribute, $values = array()) 2984 { 2985 foreach ($values as $locale=>$value){ 2986 $this->setAttributeByLocale($attribute, $value, $locale); 2987 } 2988 } 2989 2990 /** 2991 * @access private 2992 */ 2993 function _delocalizeAttribute($attribute) 2994 { 2995 return $this->_isInternationalizeCandidate($attribute) ? substr($attribute,3) : $attribute; 2996 } 2997 2998 /** 2999 * @access private 3000 */ 3001 function _isInternationalizeCandidate($column_name) 3002 { 3003 $pos = strpos($column_name,'_'); 3004 return $pos === 2 && in_array(substr($column_name,0,$pos),$this->getAvailableLocales()); 3005 } 3006 3007 /** 3008 * @access private 3009 */ 3010 function _addInternationalizedColumn($column_name) 3011 { 3012 $this->_columnsSettings[$column_name]['i18n'] = true; 3013 } 3014 3015 3016 /** 3017 * Adds an internationalized attribute to an array containing other locales for the same column name 3018 * 3019 * Example: 3020 * es_title and en_title will be available user title = array('es'=>'...', 'en' => '...') 3021 * 3022 * @access private 3023 */ 3024 function _groupInternationalizedAttribute($attribute, $value) 3025 { 3026 if($this->_internationalize && $this->_isInternationalizeCandidate($attribute)){ 3027 if(!empty($this->$attribute)){ 3028 $_tmp_pos = strpos($attribute,'_'); 3029 $column = substr($attribute,$_tmp_pos+1); 3030 $lang = substr($attribute,0,$_tmp_pos); 3031 $this->$column = empty($this->$column) ? array() : $this->$column; 3032 if(empty($this->$column) || (!empty($this->$column) && is_array($this->$column))){ 3033 $this->$column = empty($this->$column) ? array($lang=>$value) : array_merge($this->$column,array($lang=>$value)); 3034 } 3035 } 3036 } 3037 } 3038 3039 /*/Localization*/ 3040 3041 3042 3043 3044 /** 3045 Type Casting 3046 ==================================================================== 3047 See also: Database Reflection. 3048 */ 3049 3050 function getAttributesBeforeTypeCast() 3051 { 3052 $attributes_array = array(); 3053 $available_attributes = $this->getAvailableAttributes(); 3054 foreach ($available_attributes as $attribute){ 3055 $attribute_value = $this->getAttributeBeforeTypeCast($attribute['name']); 3056 if(!empty($attribute_value)){ 3057 $attributes_array[$attribute['name']] = $attribute_value; 3058 } 3059 } 3060 return $attributes_array; 3061 } 3062 3063 3064 function getAttributeBeforeTypeCast($attribute) 3065 { 3066 if(isset($this->{$attribute.'_before_type_cast'})){ 3067 return $this->{$attribute.'_before_type_cast'}; 3068 } 3069 return null; 3070 } 3071 3072 function quotedId() 3073 { 3074 return $this->castAttributeForDatabase($this->getPrimaryKey(), $this->getId()); 3075 } 3076 3077 /** 3078 * 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. 3079 */ 3080 function setSerializeAttribute($attr_name, $class_name = null) 3081 { 3082 if($this->hasColumn($attr_name)){ 3083 $this->_serializedAttributes[$attr_name] = $class_name; 3084 } 3085 } 3086 3087 function getAvailableAttributesQuoted() 3088 { 3089 return $this->getAttributesQuoted($this->getAttributes()); 3090 } 3091 3092 3093 function getAttributesQuoted($attributes_array) 3094 { 3095 $set = array(); 3096 $attributes_array = $this->getSanitizedConditionsArray($attributes_array); 3097 foreach (array_diff($attributes_array,array('')) as $k=>$v){ 3098 $set[$k] = $k.'='.$v; 3099 } 3100 3101 return $set; 3102 } 3103 3104 function getColumnType($column_name) 3105 { 3106 empty($this->_columns) ? $this->getColumns() : null; 3107 return empty($this->_columns[$column_name]['type']) ? false : $this->_columns[$column_name]['type']; 3108 } 3109 3110 function getColumnScale($column_name) 3111 { 3112 empty($this->_columns) ? $this->getColumns() : null; 3113 return empty($this->_columns[$column_name]['scale']) ? false : $this->_columns[$column_name]['scale']; 3114 } 3115 3116 function castAttributeForDatabase($column_name, $value, $add_quotes = true) 3117 { 3118 $result = ''; 3119 switch ($this->getColumnType($column_name)) { 3120 case 'datetime': 3121 if(!empty($value)){ 3122 $date_time = $this->_db->quote_datetime(Ak::getTimestamp($value)); 3123 $result = $add_quotes ? $date_time : trim($date_time ,"'"); 3124 }else{ 3125 $result = 'null'; 3126 } 3127 break; 3128 3129 case 'date': 3130 if(!empty($value)){ 3131 $date = $this->_db->quote_date(Ak::getTimestamp($value)); 3132 $result = $add_quotes ? $date : trim($date, "'"); 3133 }else{ 3134 $result = 'null'; 3135 } 3136 break; 3137 3138 case 'boolean': 3139 $result = is_null($value) ? 'null' : (!empty($value) ? "'1'" : "'0'"); 3140 break; 3141 3142 case 'binary': 3143 if($this->_getDatabaseType() == 'postgre'){ 3144 $result = is_null($value) ? 'null::bytea ' : " '".$this->_db->escape_blob($value)."'::bytea "; 3145 }else{ 3146 $result = is_null($value) ? 'null' : ($add_quotes ? $this->_db->quote_string($value) : $value); 3147 } 3148 break; 3149 3150 case 'decimal': 3151 if(is_null($value)){ 3152 $result = 'null'; 3153 }else{ 3154 if($scale = $this->getColumnScale($column_name)){ 3155 $value = number_format($value, $scale, '.', ''); 3156 } 3157 $result = $add_quotes ? $this->_db->quote_string($value) : $value; 3158 } 3159 break; 3160 3161 case 'serial': 3162 case 'integer': 3163 $result = (is_null($value) || $value==='') ? 'null' : (integer)$value; 3164 break; 3165 3166 case 'float': 3167 $result = (empty($value) && $value !== 0) ? 'null' : (is_numeric($value) ? $value : $this->_db->quote_string($value)); 3168 $result = !empty($this->_columns[$column_name]['notNull']) && $result == 'null' && $this->_getDatabaseType() == 'sqlite' ? '0' : $result; 3169 break; 3170 3171 default: 3172 $result = is_null($value) ? 'null' : ($add_quotes ? $this->_db->quote_string($value) : $value); 3173 break; 3174 } 3175 3176 // !! nullable vs. not nullable !! 3177 return empty($this->_columns[$column_name]['notNull']) ? ($result === '' ? "''" : $result) : ($result === 'null' ? '' : $result); 3178 } 3179 3180 function castAttributeFromDatabase($column_name, $value) 3181 { 3182 if($this->hasColumn($column_name)){ 3183 $column_type = $this->getColumnType($column_name); 3184 3185 if($column_type){ 3186 if('integer' == $column_type){ 3187 return is_null($value) ? null : (integer)$value; 3188 //return is_null($value) ? null : $value; // maybe for bigint we can do this 3189 }elseif('boolean' == $column_type){ 3190 if (is_null($value)) { 3191 return null; 3192 } 3193 if ($this->_getDatabaseType()=='postgre'){ 3194 return $value=='t' ? true : false; 3195 } 3196 return (integer)$value === 1 ? true : false; 3197 }elseif(!empty($value) && 'date' == $column_type && strstr(trim($value),' ')){ 3198 return substr($value,0,10) == '0000-00-00' ? null : str_replace(substr($value,strpos($value,' ')), '', $value); 3199 }elseif (!empty($value) && 'datetime' == $column_type && substr($value,0,10) == '0000-00-00'){ 3200 return null; 3201 }elseif ('binary' == $column_type && $this->_getDatabaseType() == 'postgre'){ 3202 $value = $this->_db->unescape_blob($value); 3203 $value = empty($value) || trim($value) == 'null' ? null : $value; 3204 } 3205 } 3206 } 3207 return $value; 3208 } 3209 3210 3211 /** 3212 * Joins date arguments into a single attribute. Like the array generated by the date_helper, so 3213 * array('published_on(1i)' => 2002, 'published_on(2i)' => 'January', 'published_on(3i)' => 24) 3214 * Will be converted to array('published_on'=>'2002-01-24') 3215 * 3216 * @access private 3217 */ 3218 function _castDateParametersFromDateHelper_(&$params) 3219 { 3220 if(empty($params)){ 3221 return; 3222 } 3223 $date_attributes = array(); 3224 foreach ($params as $k=>$v) { 3225 if(preg_match('/^([A-Za-z0-9_]+)\(([1-5]{1})i\)$/',$k,$match)){ 3226 $date_attributes[$match[1]][$match[2]] = $v; 3227 $this->$k = $v; 3228 unset($params[$k]); 3229 } 3230 } 3231 foreach ($date_attributes as $attribute=>$date){ 3232 $params[$attribute] = trim(@$date[1].'-'.@$date[2].'-'.@$date[3].' '.@$date[4].':'.@$date[5].':'.@$date[6],' :-'); 3233 } 3234 } 3235 3236 /** 3237 * @access private 3238 */ 3239 function _addBlobQueryStack($column_name, $blob_value) 3240 { 3241 $this->_BlobQueryStack[$column_name] = $blob_value; 3242 } 3243 3244 /** 3245 * @access private 3246 */ 3247 function _updateBlobFields($condition) 3248 { 3249 if(!empty($this->_BlobQueryStack) && is_array($this->_BlobQueryStack)){ 3250 foreach ($this->_BlobQueryStack as $column=>$value){ 3251 $this->_db->UpdateBlob($this->getTableName(), $column, $value, $condition); 3252 } 3253 $this->_BlobQueryStack = null; 3254 } 3255 } 3256 3257 /*/Type Casting*/ 3258 3259 /** 3260 Optimistic Locking 3261 ==================================================================== 3262 * 3263 * Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the 3264 * record increments the lock_version column and the locking facilities ensure that records instantiated twice 3265 * will let the last one saved return false on save() if the first was also updated. Example: 3266 * 3267 * $p1 = new Person(1); 3268 * $p2 = new Person(1); 3269 * 3270 * $p1->first_name = "Michael"; 3271 * $p1->save(); 3272 * 3273 * $p2->first_name = "should fail"; 3274 * $p2->save(); // Returns false 3275 * 3276 * You're then responsible for dealing with the conflict by checking the return value of save(); and either rolling back, merging, 3277 * or otherwise apply the business logic needed to resolve the conflict. 3278 * 3279 * You must ensure that your database schema defaults the lock_version column to 0. 3280 * 3281 * This behavior can be turned off by setting <tt>AkActiveRecord::lock_optimistically = false</tt>. 3282 */ 3283 function isLockingEnabled() 3284 { 3285 return (!isset($this->lock_optimistically) || $this->lock_optimistically !== false) && $this->hasColumn('lock_version'); 3286 } 3287 /*/Optimistic Locking*/ 3288 3289 3290 /** 3291 Callbacks 3292 ==================================================================== 3293 See also: Observers. 3294 * 3295 * Callbacks are hooks into the life-cycle of an Active Record object that allows you to trigger logic 3296 * before or after an alteration of the object state. This can be used to make sure that associated and 3297 * dependent objects are deleted when destroy is called (by overwriting beforeDestroy) or to massage attributes 3298 * before they're validated (by overwriting beforeValidation). As an example of the callbacks initiated, consider 3299 * the AkActiveRecord->save() call: 3300 * 3301 * - (-) save() 3302 * - (-) needsValidation() 3303 * - (1) beforeValidation() 3304 * - (2) beforeValidationOnCreate() / beforeValidationOnUpdate() 3305 * - (-) validate() 3306 * - (-) validateOnCreate() 3307 * - (4) afterValidation() 3308 * - (5) afterValidationOnCreate() / afterValidationOnUpdate() 3309 * - (6) beforeSave() 3310 * - (7) beforeCreate() / beforeUpdate() 3311 * - (-) create() 3312 * - (8) afterCreate() / afterUpdate() 3313 * - (9) afterSave() 3314 * - (10) afterDestroy() 3315 * - (11) beforeDestroy() 3316 * 3317 * 3318 * That's a total of 15 callbacks, which gives you immense power to react and prepare for each state in the 3319 * Active Record lifecycle. 3320 * 3321 * Examples: 3322 * class CreditCard extends ActiveRecord 3323 * { 3324 * // Strip everything but digits, so the user can specify "555 234 34" or 3325 * // "5552-3434" or both will mean "55523434" 3326 * function beforeValidationOnCreate 3327 * { 3328 * if(!empty($this->number)){ 3329 * $this->number = ereg_replace('[^0-9]*','',$this->number); 3330 * } 3331 * } 3332 * } 3333 * 3334 * class Subscription extends ActiveRecord 3335 * { 3336 * // Note: This is not implemented yet 3337 * var $beforeCreate = 'recordSignup'; 3338 * 3339 * function recordSignup() 3340 * { 3341 * $this->signed_up_on = date("Y-m-d"); 3342 * } 3343 * } 3344 * 3345 * class Firm extends ActiveRecord 3346 * { 3347 * //Destroys the associated clients and people when the firm is destroyed 3348 * // Note: This is not implemented yet 3349 * var $beforeDestroy = array('destroyAssociatedPeople', 'destroyAssociatedClients'); 3350 * 3351 * function destroyAssociatedPeople() 3352 * { 3353 * $Person = new Person(); 3354 * $Person->destroyAll("firm_id=>", $this->id); 3355 * } 3356 * 3357 * function destroyAssociatedClients() 3358 * { 3359 * $Client = new Client(); 3360 * $Client->destroyAll("client_of=>", $this->id); 3361 * } 3362 * } 3363 * 3364 * 3365 * == Canceling callbacks == 3366 * 3367 * If a before* callback returns false, all the later callbacks and the associated action are cancelled. If an after* callback returns 3368 * false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks 3369 * defined as methods on the model, which are called last. 3370 * 3371 * Override this methods to hook Active Records 3372 * 3373 * @access public 3374 */ 3375 3376 function beforeCreate(){return true;} 3377 function beforeValidation(){return true;} 3378 function beforeValidationOnCreate(){return true;} 3379 function beforeValidationOnUpdate(){return true;} 3380 function beforeSave(){return true;} 3381 function beforeUpdate(){return true;} 3382 function afterUpdate(){return true;} 3383 function afterValidation(){return true;} 3384 function afterValidationOnCreate(){return true;} 3385 function afterValidationOnUpdate(){return true;} 3386 function afterInstantiate(){return true;} 3387 function afterCreate(){return true;} 3388 function afterDestroy(){return true;} 3389 function beforeDestroy(){return true;} 3390 function afterSave(){return true;} 3391 3392 /*/Callbacks*/ 3393 3394 3395 /** 3396 Transactions 3397 ==================================================================== 3398 * 3399 * Transaction support for database operations 3400 * 3401 * Transactions are enabled automatically for Active record objects, But you can nest transactions within models. 3402 * This transactions are nested, and only the outermost will be executed 3403 * 3404 * $User->transactionStart(); 3405 * $User->create('username'=>'Bermi'); 3406 * $Members->create('username'=>'Bermi'); 3407 * 3408 * if(!checkSomething()){ 3409 * $User->transactionFail(); 3410 * } 3411 * 3412 * $User->transactionComplete(); 3413 */ 3414 3415 function transactionStart() 3416 { 3417 return $this->_db->startTransaction(); 3418 } 3419 3420 function transactionComplete() 3421 { 3422 return $this->_db->stopTransaction(); 3423 } 3424 3425 function transactionFail() 3426 { 3427 $this->_db->failTransaction(); 3428 return false; 3429 } 3430 3431 function transactionHasFailed() 3432 { 3433 return $this->_db->hasTransactionFailed(); 3434 } 3435 3436 /*/Transactions*/ 3437 3438 3439 3440 3441 /** 3442 Validators 3443 ==================================================================== 3444 See also: Error Handling. 3445 * 3446 * Active Records implement validation by overwriting AkActiveRecord::validate (or the variations, validateOnCreate and 3447 * validateOnUpdate). Each of these methods can inspect the state of the object, which usually means ensuring 3448 * that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression). 3449 * 3450 * Example: 3451 * 3452 * class Person extends ActiveRecord 3453 * { 3454 * function validate() 3455 * { 3456 * $this->addErrorOnEmpty(array('first_name', 'last_name')); 3457 * if(!preg_match('/[0-9]{4,12}/', $this->phone_number)){ 3458 * $this->addError("phone_number", "has invalid format"); 3459 * } 3460 * } 3461 * 3462 * function validateOnCreate() // is only run the first time a new object is saved 3463 * { 3464 * if(!isValidDiscount($this->membership_discount)){ 3465 * $this->addError("membership_discount", "has expired"); 3466 * } 3467 * } 3468 * 3469 * function validateOnUpdate() 3470 * { 3471 * if($this->countChangedAttributes() == 0){ 3472 * $this->addErrorToBase("No changes have occurred"); 3473 * } 3474 * } 3475 * } 3476 * 3477 * $Person = new Person(array("first_name" => "David", "phone_number" => "what?")); 3478 * $Person->save(); // => false (and doesn't do the save); 3479 * $Person->hasErrors(); // => false 3480 * $Person->countErrors(); // => 2 3481 * $Person->getErrorsOn("last_name"); // => "can't be empty" 3482 * $Person->getErrorsOn("phone_number"); // => "has invalid format" 3483 * $Person->yieldEachFullError(); // => "Last name can't be empty \n Phone number has invalid format" 3484 * 3485 * $Person->setAttributes(array("last_name" => "Heinemeier", "phone_number" => "555-555")); 3486 * $Person->save(); // => true (and person is now saved in the database) 3487 * 3488 * An "_errors" array is available for every Active Record. 3489 * 3490 */ 3491 3492 /** 3493 * Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: 3494 * 3495 * Model: 3496 * class Person extends ActiveRecord 3497 * { 3498 * function validate() 3499 * { 3500 * $this->validatesConfirmationOf('password'); 3501 * $this->validatesConfirmationOf('email_address', "should match confirmation"); 3502 * } 3503 * } 3504 * 3505 * View: 3506 * <?=$form_helper->password_field("person", "password"); ?> 3507 * <?=$form_helper->password_field("person", "password_confirmation"); ?> 3508 * 3509 * The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual. 3510 * It exists only as an in-memory variable for validating the password. This check is performed only if password_confirmation 3511 * is not null. 3512 * 3513 */ 3514 function validatesConfirmationOf($attribute_names, $message = 'confirmation') 3515 { 3516 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 3517 $attribute_names = Ak::toArray($attribute_names); 3518 foreach ($attribute_names as $attribute_name){ 3519 $attribute_accessor = $attribute_name.'_confirmation'; 3520 if(isset($this->$attribute_accessor) && @$this->$attribute_accessor != @$this->$attribute_name){ 3521 $this->addError($attribute_name, $message); 3522 } 3523 } 3524 } 3525 3526 /** 3527 * Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: 3528 * 3529 * class Person extends ActiveRecord 3530 * { 3531 * function validateOnCreate() 3532 * { 3533 * $this->validatesAcceptanceOf('terms_of_service'); 3534 * $this->validatesAcceptanceOf('eula', "must be abided"); 3535 * } 3536 * } 3537 * 3538 * The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed only if 3539 * terms_of_service is not null. 3540 * 3541 * 3542 * @param accept 1 3543 * Specifies value that is considered accepted. The default value is a string "1", which makes it easy to relate to an HTML checkbox. 3544 */ 3545 function validatesAcceptanceOf($attribute_names, $message = 'accepted', $accept = 1) 3546 { 3547 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 3548 3549 $attribute_names = Ak::toArray($attribute_names); 3550 foreach ($attribute_names as $attribute_name){ 3551 if(@$this->$attribute_name != $accept){ 3552 $this->addError($attribute_name, $message); 3553 } 3554 } 3555 } 3556 3557 /** 3558 * Validates whether the associated object or objects are all valid themselves. Works with any kind of association. 3559 * 3560 * class Book extends ActiveRecord 3561 * { 3562 * var $has_many = 'pages'; 3563 * var $belongs_to = 'library'; 3564 * 3565 * function validate(){ 3566 * $this->validatesAssociated(array('pages', 'library')); 3567 * } 3568 * } 3569 * 3570 * 3571 * Warning: If, after the above definition, you then wrote: 3572 * 3573 * class Page extends ActiveRecord 3574 * { 3575 * var $belongs_to = 'book'; 3576 * function validate(){ 3577 * $this->validatesAssociated('book'); 3578 * } 3579 * } 3580 * 3581 * ...this would specify a circular dependency and cause infinite recursion. 3582 * 3583 * NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association 3584 * is both present and guaranteed to be valid, you also need to use validatesPresenceOf. 3585 */ 3586 function validatesAssociated($attribute_names, $message = 'invalid') 3587 { 3588 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 3589 $attribute_names = Ak::toArray($attribute_names); 3590 foreach ($attribute_names as $attribute_name){ 3591 if(!empty($this->$attribute_name)){ 3592 if(is_array($this->$attribute_name)){ 3593 foreach(array_keys($this->$attribute_name) as $k){ 3594 if(method_exists($this->{$attribute_name}[$k],'isValid') && !$this->{$attribute_name}[$k]->isValid()){ 3595 $this->addError($attribute_name, $message); 3596 } 3597 } 3598 }elseif (method_exists($this->$attribute_name,'isValid') && !$this->$attribute_name->isValid()){ 3599 $this->addError($attribute_name, $message); 3600 } 3601 } 3602 } 3603 } 3604 3605 function isBlank($value = null) 3606 { 3607 return trim((string)$value) == ''; 3608 } 3609 3610 /** 3611 * Validates that the specified attributes are not blank (as defined by AkActiveRecord::isBlank()). 3612 */ 3613 function validatesPresenceOf($attribute_names, $message = 'blank') 3614 { 3615 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 3616 3617 $attribute_names = Ak::toArray($attribute_names); 3618 foreach ($attribute_names as $attribute_name){ 3619 $this->addErrorOnBlank($attribute_name, $message); 3620 } 3621 } 3622 3623 /** 3624 * Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: 3625 * 3626 * class Person extends ActiveRecord 3627 * { 3628 * function validate() 3629 * { 3630 * $this->validatesLengthOf('first_name', array('maximum'=>30)); 3631 * $this->validatesLengthOf('last_name', array('maximum'=>30,'message'=> "less than %d if you don't mind")); 3632 * $this->validatesLengthOf('last_name', array('within'=>array(7, 32))); 3633 * $this->validatesLengthOf('last_name', array('in'=>array(6, 20), 'too_long' => "pick a shorter name", 'too_short' => "pick a longer name")); 3634 * $this->validatesLengthOf('fav_bra_size', array('minimum'=>1, 'too_short'=>"please enter at least %d character")); 3635 * $this->validatesLengthOf('smurf_leader', array('is'=>4, 'message'=>"papa is spelled with %d characters... don't play me.")); 3636 * } 3637 * } 3638 * 3639 * 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. 3640 * 3641 * Configuration options: 3642 * <tt>minimum</tt> - The minimum size of the attribute 3643 * <tt>maximum</tt> - The maximum size of the attribute 3644 * <tt>is</tt> - The exact size of the attribute 3645 * <tt>within</tt> - A range specifying the minimum and maximum size of the attribute 3646 * <tt>in</tt> - A synonym(or alias) for :within 3647 * <tt>allow_null</tt> - Attribute may be null; skip validation. 3648 * 3649 * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default "is" "is too long (max is %d characters)") 3650 * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default "is" "is too short (min is %d characters)") 3651 * <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)") 3652 * <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 3653 */ 3654 function validatesLengthOf($attribute_names, $options = array()) 3655 { 3656 // Merge given options with defaults. 3657 $default_options = array( 3658 'too_long' => $this->_defaultErrorMessages['too_long'], 3659 'too_short' => $this->_defaultErrorMessages['too_short'], 3660 'wrong_length' => $this->_defaultErrorMessages['wrong_length'], 3661 'allow_null' => false 3662 ); 3663 3664 $range_options = array(); 3665 foreach ($options as $k=>$v){ 3666 if(in_array($k,array('minimum','maximum','is','in','within'))){ 3667 $range_options[$k] = $v; 3668 $option = $k; 3669 $option_value = $v; 3670 } 3671 } 3672 3673 // Ensure that one and only one range option is specified. 3674 switch (count($range_options)) { 3675 case 0: 3676 trigger_error(Ak::t('Range unspecified. Specify the "within", "maximum", "minimum, or "is" option.'), E_USER_ERROR); 3677 return false; 3678 break; 3679 case 1: 3680 $options = array_merge($default_options, $options); 3681 break; 3682 default: 3683 trigger_error(Ak::t('Too many range options specified. Choose only one.'), E_USER_ERROR); 3684 return false; 3685 break; 3686 } 3687 3688 3689 switch ($option) { 3690 case 'within': 3691 case 'in': 3692 if(empty($option_value) || !is_array($option_value) || count($option_value) != 2 || !is_numeric($option_value[0]) || !is_numeric($option_value[1])){ 3693 trigger_error(Ak::t('%option must be a Range (array(min, max))',array('%option',$option)), E_USER_ERROR); 3694 return false; 3695 } 3696 $attribute_names = Ak::toArray($attribute_names); 3697 3698 foreach ($attribute_names as $attribute_name){ 3699 if((!empty($option['allow_null']) && !isset($this->$attribute_name)) || (Ak::size($this->$attribute_name)) < $option_value[0]){ 3700 $this->addError($attribute_name, sprintf($options['too_short'], $option_value[0])); 3701 }elseif((!empty($option['allow_null']) && !isset($this->$attribute_name)) || (Ak::size($this->$attribute_name)) > $option_value[1]){ 3702 $this->addError($attribute_name, sprintf($options['too_long'], $option_value[1])); 3703 } 3704 } 3705 break; 3706 3707 case 'is': 3708 case 'minimum': 3709 case 'maximum': 3710 3711 if(empty($option_value) || !is_numeric($option_value) || $option_value <= 0){ 3712 trigger_error(Ak::t('%option must be a nonnegative Integer',array('%option',$option_value)), E_USER_ERROR); 3713 return false; 3714 } 3715 3716 // Declare different validations per option. 3717 $validity_checks = array('is' => "==", 'minimum' => ">=", 'maximum' => "<="); 3718 $message_options = array('is' => 'wrong_length', 'minimum' => 'too_short', 'maximum' => 'too_long'); 3719 3720 $message = sprintf(!empty($options['message']) ? $options['message'] : $options[$message_options[$option]],$option_value); 3721 3722 $attribute_names = Ak::toArray($attribute_names); 3723 foreach ($attribute_names as $attribute_name){ 3724 if((!$options['allow_null'] && !isset($this->$attribute_name)) || 3725 eval("return !(".Ak::size(@$this->$attribute_name)." {$validity_checks[$option]} $option_value);")){ 3726 $this->addError($attribute_name, $message); 3727 } 3728 } 3729 break; 3730 default: 3731 break; 3732 } 3733 3734 return true; 3735 } 3736 3737 function validatesSizeOf($attribute_names, $options = array()) 3738 { 3739 return validatesLengthOf($attribute_names, $options); 3740 } 3741 3742 /** 3743 * Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user 3744 * can be named "davidhh". 3745 * 3746 * class Person extends ActiveRecord 3747 * { 3748 * function validate() 3749 * { 3750 * $this->validatesUniquenessOf('passport_number'); 3751 * $this->validatesUniquenessOf('user_name', array('scope' => "account_id")); 3752 * } 3753 * } 3754 * 3755 * It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, 3756 * making sure that a teacher can only be on the schedule once per semester for a particular class. 3757 * 3758 * class TeacherSchedule extends ActiveRecord 3759 * { 3760 * function validate() 3761 * { 3762 * $this->validatesUniquenessOf('passport_number'); 3763 * $this->validatesUniquenessOf('teacher_id', array('scope' => array("semester_id", "class_id")); 3764 * } 3765 * } 3766 * 3767 * 3768 * 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 3769 * attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. 3770 * 3771 * Configuration options: 3772 * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken") 3773 * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope" 3774 * <tt>case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (true by default). 3775 * <tt>if</tt> - Specifies a method to call or a string to evaluate to determine if the validation should 3776 * occur (e.g. 'if' => 'allowValidation', or 'if' => '$this->signup_step > 2'). The 3777 * method, or string should return or evaluate to a true or false value. 3778 */ 3779 function validatesUniquenessOf($attribute_names, $options = array()) 3780 { 3781 $default_options = array('case_sensitive'=>true, 'message'=>'taken'); 3782 $options = array_merge($default_options, $options); 3783 3784 if(!empty($options['if'])){ 3785 if(method_exists($this,$options['if'])){ 3786 if($this->{$options['if']}() === false){ 3787 return true; 3788 } 3789 }else { 3790 eval('$__eval_result = ('.rtrim($options['if'],';').');'); 3791 if(empty($__eval_result)){ 3792 return true; 3793 } 3794 } 3795 } 3796 3797 $message = isset($this->_defaultErrorMessages[$options['message']]) ? $this->t($this->_defaultErrorMessages[$options['message']]) : $options['message']; 3798 unset($options['message']); 3799 3800 foreach ((array)$attribute_names as $attribute_name){ 3801 $value = isset($this->$attribute_name) ? $this->$attribute_name : null; 3802 3803 if($value === null || ($options['case_sensitive'] || !$this->hasColumn($attribute_name))){ 3804 $condition_sql = $this->getTableName().'.'.$attribute_name.' '.$this->getAttributeCondition($value); 3805 $condition_params = array($value); 3806 }else{ 3807 $condition_sql = 'LOWER('.$this->getTableName().'.'.$attribute_name.') '.$this->getAttributeCondition($value); 3808 $condition_params = array(is_array($value) ? array_map('utf8_strtolower',$value) : utf8_strtolower($value)); 3809 } 3810 3811 if(!empty($options['scope'])){ 3812 foreach ((array)$options['scope'] as $scope_item){ 3813 $scope_value = $this->get($scope_item); 3814 $condition_sql .= ' AND '.$this->getTableName().'.'.$scope_item.' '.$this->getAttributeCondition($scope_value); 3815 $condition_params[] = $scope_value; 3816 } 3817 } 3818 3819 if(!$this->isNewRecord()){ 3820 $condition_sql .= ' AND '.$this->getTableName().'.'.$this->getPrimaryKey().' <> ?'; 3821 $condition_params[] = $this->getId(); 3822 } 3823 array_unshift($condition_params,$condition_sql); 3824 if ($this->find('first', array('conditions' => $condition_params))){ 3825 $this->addError($attribute_name, $message); 3826 } 3827 } 3828 } 3829 3830 3831 3832 /** 3833 * Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression 3834 * provided. 3835 * 3836 * <code> 3837 * class Person extends ActiveRecord 3838 * { 3839 * function validate() 3840 * { 3841 * $this->validatesFormatOf('email', "/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/"); 3842 * } 3843 * } 3844 * </code> 3845 * 3846 * A regular expression must be provided or else an exception will be raised. 3847 * 3848 * There are some regular expressions bundled with the Akelos Framework. 3849 * 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. 3850 * This are predefined perl-like regular extensions. 3851 * 3852 * * AK_NOT_EMPTY_REGULAR_EXPRESSION ---> /.+/ 3853 * * 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 3854 * * AK_NUMBER_REGULAR_EXPRESSION ---> /^[0-9]+$/ 3855 * * AK_PHONE_REGULAR_EXPRESSION ---> /^([\+]?[(]?[\+]?[ ]?[0-9]{2,3}[)]?[ ]?)?[0-9 ()\-]{4,25}$/ 3856 * * 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}$/ 3857 * * 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])$/ 3858 * * AK_POST_CODE_REGULAR_EXPRESSION ---> /^[0-9A-Za-z -]{2,7}$/ 3859 * 3860 * 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. 3861 * 3862 * Params: 3863 * <tt>$message</tt> - A custom error message (default is: "is invalid") 3864 * <tt>$regular_expression</tt> - The regular expression used to validate the format with (note: must be supplied!) 3865 */ 3866 function validatesFormatOf($attribute_names, $regular_expression, $message = 'invalid', $regex_function = 'preg_match') 3867 { 3868 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 3869 3870 $attribute_names = Ak::toArray($attribute_names); 3871 foreach ($attribute_names as $attribute_name){ 3872 if(!isset($this->$attribute_name) || !$regex_function($regular_expression, $this->$attribute_name)){ 3873 $this->addError($attribute_name, $message); 3874 } 3875 } 3876 } 3877 3878 /** 3879 * Validates whether the value of the specified attribute is available in a particular array of elements. 3880 * 3881 * class Person extends ActiveRecord 3882 * { 3883 * function validate() 3884 * { 3885 * $this->validatesInclusionOf('gender', array('male', 'female'), "woah! what are you then!??!!"); 3886 * $this->validatesInclusionOf('age', range(0, 99)); 3887 * } 3888 * 3889 * Parameters: 3890 * <tt>$array_of_ possibilities</tt> - An array of available items 3891 * <tt>$message</tt> - Specifies a customer error message (default is: "is not included in the list") 3892 * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false) 3893 */ 3894 function validatesInclusionOf($attribute_names, $array_of_possibilities, $message = 'inclusion', $allow_null = false) 3895 { 3896 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 3897 3898 $attribute_names = Ak::toArray($attribute_names); 3899 foreach ($attribute_names as $attribute_name){ 3900 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 )){ 3901 $this->addError($attribute_name, $message); 3902 } 3903 } 3904 } 3905 3906 /** 3907 * Validates that the value of the specified attribute is not in a particular array of elements. 3908 * 3909 * class Person extends ActiveRecord 3910 * { 3911 * function validate() 3912 * { 3913 * $this->validatesExclusionOf('username', array('admin', 'superuser'), "You don't belong here"); 3914 * $this->validatesExclusionOf('age', range(30,60), "This site is only for under 30 and over 60"); 3915 * } 3916 * } 3917 * 3918 * Parameters: 3919 * <tt>$array_of_possibilities</tt> - An array of items that the value shouldn't be part of 3920 * <tt>$message</tt> - Specifies a customer error message (default is: "is reserved") 3921 * <tt>$allow_null</tt> - If set to true, skips this validation if the attribute is null (default is: false) 3922 */ 3923 function validatesExclusionOf($attribute_names, $array_of_possibilities, $message = 'exclusion', $allow_null = false) 3924 { 3925 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 3926 3927 $attribute_names = Ak::toArray($attribute_names); 3928 foreach ($attribute_names as $attribute_name){ 3929 3930 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 )){ 3931 $this->addError($attribute_name, $message); 3932 } 3933 } 3934 } 3935 3936 3937 3938 3939 /** 3940 * Validates whether the value of the specified attribute is numeric. 3941 * 3942 * class Person extends ActiveRecord 3943 * { 3944 * function validate() 3945 * { 3946 * $this->validatesNumericalityOf('value'); 3947 * } 3948 * } 3949 * 3950 * Parameters: 3951 * <tt>$message</tt> - A custom error message (default is: "is not a number") 3952 * <tt>$only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false) 3953 * <tt>$allow_null</tt> Skip validation if attribute is null (default is false). 3954 */ 3955 function validatesNumericalityOf($attribute_names, $message = 'not_a_number', $only_integer = false, $allow_null = false) 3956 { 3957 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 3958 3959 $attribute_names = Ak::toArray($attribute_names); 3960 foreach ($attribute_names as $attribute_name){ 3961 if (isset($this->$attribute_name)){ 3962 $value = $this->$attribute_name; 3963 if ($only_integer){ 3964 $is_int = is_numeric($value) && (int)$value == $value; 3965 $has_error = !$is_int; 3966 }else{ 3967 $has_error = !is_numeric($value); 3968 } 3969 }else{ 3970 $has_error = $allow_null ? false : true; 3971 } 3972 3973 if ($has_error){ 3974 $this->addError($attribute_name, $message); 3975 } 3976 } 3977 } 3978 3979 3980 3981 /** 3982 * Returns true if no errors were added otherwise false. 3983 */ 3984 function isValid() 3985 { 3986 $this->clearErrors(); 3987 if($this->beforeValidation() && $this->notifyObservers('beforeValidation')){ 3988 3989 3990 if($this->_set_default_attribute_values_automatically){ 3991 $this->_setDefaultAttributeValuesAutomatically(); 3992 } 3993 3994 $this->validate(); 3995 3996 if($this->_automated_validators_enabled){ 3997 $this->_runAutomatedValidators(); 3998 } 3999 4000 $this->afterValidation(); 4001 $this->notifyObservers('afterValidation'); 4002 4003 if ($this->isNewRecord()){ 4004 if($this->beforeValidationOnCreate()){ 4005 $this->notifyObservers('beforeValidationOnCreate'); 4006 $this->validateOnCreate(); 4007 $this->afterValidationOnCreate(); 4008 $this->notifyObservers('afterValidationOnCreate'); 4009 } 4010 }else{ 4011 if($this->beforeValidationOnUpdate()){ 4012 $this->notifyObservers('beforeValidationOnUpdate'); 4013 $this->validateOnUpdate(); 4014 $this->afterValidationOnUpdate(); 4015 $this->notifyObservers('afterValidationOnUpdate'); 4016 } 4017 } 4018 } 4019 4020 return !$this->hasErrors(); 4021 } 4022 4023 /** 4024 * By default the Active Record will validate for the maximum length for database columns. You can 4025 * disable the automated validators by setting $this->_automated_validators_enabled to false. 4026 * Specific validators are (for now): 4027 * $this->_automated_max_length_validator = false; // false by default, but you can set it to true on your model 4028 * $this->_automated_not_null_validator = false; // disabled by default 4029 * 4030 * @access private 4031 */ 4032 function _runAutomatedValidators() 4033 { 4034 foreach ($this->_columns as $column_name=>$column_settings){ 4035 if($this->_automated_max_length_validator && 4036 empty($column_settings['primaryKey']) && 4037 !empty($this->$column_name) && 4038 !empty($column_settings['maxLength']) && $column_settings['maxLength'] > 0 && 4039 strlen($this->$column_name) > $column_settings['maxLength']){ 4040 $this->addError($column_name, sprintf($this->_defaultErrorMessages['too_long'], $column_settings['maxLength'])); 4041 }elseif($this->_automated_not_null_validator && empty($column_settings['primaryKey']) && !empty($column_settings['notNull']) && (!isset($this->$column_name) || is_null($this->$column_name))){ 4042 $this->addError($column_name,'empty'); 4043 } 4044 } 4045 } 4046 4047 /** 4048 * $this->_set_default_attribute_values_automatically = true; // This enables automated attribute setting from database definition 4049 * 4050 * @access private 4051 */ 4052 function _setDefaultAttributeValuesAutomatically() 4053 { 4054 foreach ($this->_columns as $column_name=>$column_settings){ 4055 if(empty($column_settings['primaryKey']) && isset($column_settings['hasDefault']) && $column_settings['hasDefault'] && (!isset($this->$column_name) || is_null($this->$column_name))){ 4056 if(empty($column_settings['defaultValue'])){ 4057 if($column_settings['type'] == 'integer' && empty($column_settings['notNull'])){ 4058 $this->$column_name = 0; 4059 }elseif(($column_settings['type'] == 'string' || $column_settings['type'] == 'text') && empty($column_settings['notNull'])){ 4060 $this->$column_name = ''; 4061 } 4062 }else { 4063 $this->$column_name = $column_settings['defaultValue']; 4064 } 4065 } 4066 } 4067 } 4068 4069 /** 4070 * Overwrite this method for validation checks on all saves and use addError($field, $message); for invalid attributes. 4071 */ 4072 function validate() 4073 { 4074 } 4075 4076 /** 4077 * Overwrite this method for validation checks used only on creation. 4078 */ 4079 function validateOnCreate() 4080 { 4081 } 4082 4083 /** 4084 * Overwrite this method for validation checks used only on updates. 4085 */ 4086 function validateOnUpdate() 4087 { 4088 } 4089 4090 /*/Validators*/ 4091 4092 4093 /** 4094 Observers 4095 ==================================================================== 4096 See also: Callbacks. 4097 */ 4098 4099 /** 4100 * $state store the state of this observable object 4101 * 4102 * @access private 4103 */ 4104 var $_observable_state; 4105 4106 /** 4107 * @access private 4108 */ 4109 function _instantiateDefaultObserver() 4110 { 4111 $default_observer_name = ucfirst($this->getModelName().'Observer'); 4112 if(class_exists($default_observer_name)){ 4113 //$Observer =& new $default_observer_name($this); 4114 Ak::singleton($default_observer_name, $this); 4115 } 4116 } 4117 4118 /** 4119 * Calls the $method using the reference to each 4120 * registered observer. 4121 * @return true (this is used internally for triggering observers on default callbacks) 4122 */ 4123 function notifyObservers ($method = null) 4124 { 4125 $observers =& $this->getObservers(); 4126 $observer_count = count($observers); 4127 4128 if(!empty($method)){ 4129 $this->setObservableState($method); 4130 } 4131 4132 $model_name = $this->getModelName(); 4133 for ($i=0; $i<$observer_count; $i++) { 4134 if(in_array($model_name, $observers[$i]->_observing)){ 4135 if(method_exists($observers[$i], $method)){ 4136 $observers[$i]->$method($this); 4137 }else{ 4138 $observers[$i]->update($this->getObservableState(), &$this); 4139 } 4140 }else{ 4141 $observers[$i]->update($this->getObservableState(), &$this); 4142 } 4143 } 4144 $this->setObservableState(''); 4145 4146 return true; 4147 } 4148 4149 4150 function setObservableState($state_message) 4151 { 4152 $this->_observable_state = $state_message; 4153 } 4154 4155 function getObservableState() 4156 { 4157 return $this->_observable_state; 4158 } 4159 4160 /** 4161 * Register the reference to an object object 4162 * 4163 * 4164 * @param $observer AkObserver 4165 * @param $options array of options for the observer 4166 * @return void 4167 */ 4168 function addObserver(&$observer) 4169 { 4170 $staticVarNs='AkActiveRecord::observers::' . $this->_modelName; 4171 $observer_class_name = get_class($observer); 4172 /** 4173 * get the statically stored observers for the namespace 4174 */ 4175 $observers = &Ak::getStaticVar($staticVarNs); 4176 if (!is_array($observers)) { 4177 $observers = array('classes'=>array(),'objects'=>array()); 4178 } 4179 /** 4180 * if not already registered, the observerclass will 4181 * be registered now 4182 */ 4183 if (!in_array($observer_class_name,$observers['classes'])) { 4184 $observers['classes'][] = $observer_class_name; 4185 $observers['objects'][] = &$observer; 4186 Ak::setStaticVar($staticVarNs, $observers); 4187 4188 } 4189 } 4190 /** 4191 * Register the reference to an object object 4192 * @return void 4193 */ 4194 function &getObservers() 4195 { 4196 $staticVarNs='AkActiveRecord::observers::' . $this->_modelName; 4197 $key = 'objects'; 4198 4199 $array = array(); 4200 $observers_arr =& Ak::getStaticVar($staticVarNs); 4201 if (isset($observers_arr[$key])) { 4202 $observers = &$observers_arr[$key]; 4203 } else { 4204 $observers = &$array; 4205 } 4206 4207 return $observers; 4208 } 4209 4210 /*/Observers*/ 4211 4212 4213 4214 4215 /** 4216 Error Handling 4217 ==================================================================== 4218 See also: Validators. 4219 */ 4220 4221 4222 /** 4223 * Returns the Errors array that holds all information about attribute error messages. 4224 */ 4225 function getErrors() 4226 { 4227 return $this->_errors; 4228 } 4229 4230 /** 4231 * Adds an error to the base object instead of any particular attribute. This is used 4232 * to report errors that doesn't tie to any specific attribute, but rather to the object 4233 * as a whole. These error messages doesn't get prepended with any field name when iterating 4234 * with yieldEachFullError, so they should be complete sentences. 4235 */ 4236 function addErrorToBase($message) 4237 { 4238 $this->addError($this->getModelName(), $message); 4239 } 4240 4241 /** 4242 * Returns errors assigned to base object through addToBase according to the normal rules of getErrorsOn($attribute). 4243 */ 4244 function getBaseErrors() 4245 { 4246 $errors = $this->getErrors(); 4247 return (array)@$errors[$this->getModelName()]; 4248 } 4249 4250 4251 /** 4252 * Adds an error message ($message) to the ($attribute), which will be returned on a call to <tt>getErrorsOn($attribute)</tt> 4253 * for the same attribute and ensure that this error object returns false when asked if <tt>hasErrors</tt>. More than one 4254 * error can be added to the same $attribute in which case an array will be returned on a call to <tt>getErrorsOn($attribute)</tt>. 4255 * If no $message is supplied, "invalid" is assumed. 4256 */ 4257 function addError($attribute, $message = 'invalid') 4258 { 4259 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 4260 $this->_errors[$attribute][] = $message; 4261 } 4262 4263 /** 4264 * Will add an error message to each of the attributes in $attributes that is empty. 4265 */ 4266 function addErrorOnEmpty($attribute_names, $message = 'empty') 4267 { 4268 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 4269 $attribute_names = Ak::toArray($attribute_names); 4270 foreach ($attribute_names as $attribute){ 4271 if(empty($this->$attribute)){ 4272 $this->addError($attribute, $message); 4273 } 4274 } 4275 } 4276 4277 /** 4278 * Will add an error message to each of the attributes in $attributes that is blank (using $this->isBlank). 4279 */ 4280 function addErrorOnBlank($attribute_names, $message = 'blank') 4281 { 4282 $message = isset($this->_defaultErrorMessages[$message]) ? $this->t($this->_defaultErrorMessages[$message]) : $message; 4283 $attribute_names = Ak::toArray($attribute_names); 4284 foreach ($attribute_names as $attribute){ 4285 if($this->isBlank(@$this->$attribute)){ 4286 $this->addError($attribute, $message); 4287 } 4288 } 4289 } 4290 4291 /** 4292 * Will add an error message to each of the attributes in $attributes that has a length outside of the passed boundary $range. 4293 * If the length is above the boundary, the too_long_message message will be used. If below, the too_short_message. 4294 */ 4295 function addErrorOnBoundaryBreaking($attribute_names, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short') 4296 { 4297 $too_long_message = isset($this->_defaultErrorMessages[$too_long_message]) ? $this->_defaultErrorMessages[$too_long_message] : $too_long_message; 4298 $too_short_message = isset($this->_defaultErrorMessages[$too_short_message]) ? $this->_defaultErrorMessages[$too_short_message] : $too_short_message; 4299 4300 $attribute_names = Ak::toArray($attribute_names); 4301 foreach ($attribute_names as $attribute){ 4302 if(@$this->$attribute < $range_begin){ 4303 $this->addError($attribute, $too_short_message); 4304 } 4305 if(@$this->$attribute > $range_end){ 4306 $this->addError($attribute, $too_long_message); 4307 } 4308 } 4309 4310 } 4311 4312 function addErrorOnBoundryBreaking ($attributes, $range_begin, $range_end, $too_long_message = 'too_long', $too_short_message = 'too_short') 4313 { 4314 $this->addErrorOnBoundaryBreaking($attributes, $range_begin, $range_end, $too_long_message, $too_short_message); 4315 } 4316 4317 /** 4318 * Returns true if the specified $attribute has errors associated with it. 4319 */ 4320 function isInvalid($attribute) 4321 { 4322 return $this->getErrorsOn($attribute); 4323 } 4324 4325 /** 4326 * Returns false, if no errors are associated with the specified $attribute. 4327 * Returns the error message, if one error is associated with the specified $attribute. 4328 * Returns an array of error messages, if more than one error is associated with the specified $attribute. 4329 */ 4330 function getErrorsOn($attribute) 4331 { 4332 if (empty($this->_errors[$attribute])){ 4333 return false; 4334 }elseif (count($this->_errors[$attribute]) == 1){ 4335 $k = array_keys($this->_errors[$attribute]); 4336 return $this->_errors[$attribute][$k[0]]; 4337 }else{ 4338 return $this->_errors[$attribute]; 4339 } 4340 } 4341 4342 4343 /** 4344 * Yields each attribute and associated message per error added. 4345 */ 4346 function yieldEachError() 4347 { 4348 foreach ($this->_errors as $errors){ 4349 foreach ($errors as $error){ 4350 $this->yieldError($error); 4351 } 4352 } 4353 } 4354 4355 function yieldError($message) 4356 { 4357 $messages = is_array($message) ? $message : array($message); 4358 foreach ($messages as $message){ 4359 echo "<div class='error'><p>$message</p></div>\n"; 4360 } 4361 4362 } 4363 4364 /** 4365 * Yields each full error message added. So Person->addError("first_name", "can't be empty") will be returned 4366 * through iteration as "First name can't be empty". 4367 */ 4368 function yieldEachFullError() 4369 { 4370 $full_messages = $this->getFullErrorMessages(); 4371 foreach ($full_messages as $full_message){ 4372 $this->yieldError($full_message); 4373 } 4374 } 4375 4376 4377 /** 4378 * Returns all the full error messages in an array. 4379 */ 4380 function getFullErrorMessages() 4381 { 4382 $full_messages = array(); 4383 4384 foreach ($this->_errors as $attribute=>$errors){ 4385 $full_messages[$attribute] = array(); 4386 foreach ($errors as $error){ 4387 $full_messages[$attribute][] = $this->t('%attribute_name %error', array( 4388 '%attribute_name'=>AkInflector::humanize($this->_delocalizeAttribute($attribute)), 4389 '%error'=>$error 4390 )); 4391 } 4392 } 4393 return $full_messages; 4394 } 4395 4396 /** 4397 * Returns true if no errors have been added. 4398 */ 4399 function hasErrors() 4400 { 4401 return !empty($this->_errors); 4402 } 4403 4404 /** 4405 * Removes all the errors that have been added. 4406 */ 4407 function clearErrors() 4408 { 4409 $this->_errors = array(); 4410 } 4411 4412 /** 4413 * Returns the total number of errors added. Two errors added to the same attribute will be counted as such 4414 * with this as well. 4415 */ 4416 function countErrors() 4417 { 4418 $error_count = 0; 4419 foreach ($this->_errors as $errors){ 4420 $error_count = count($errors)+$error_count; 4421 } 4422 4423 return $error_count; 4424 } 4425 4426 4427 function errorsToString($print = false) 4428 { 4429 $result = "\n<div id='errors'>\n<ul class='error'>\n"; 4430 foreach ($this->getFullErrorMessages() as $error){ 4431 $result .= is_array($error) ? "<li class='error'>".join('</li><li class=\'error\'>',$error)."</li>\n" : "<li class='error'>$error</li>\n"; 4432 } 4433 $result .= "</ul>\n</div>\n"; 4434 4435 if($print){ 4436 echo $result; 4437 } 4438 return $result; 4439 } 4440 4441 /*/Error Handling*/ 4442 4443 4444 4445 /** 4446 Act as Behaviours 4447 ==================================================================== 4448 See also: Acts as List, Acts as Tree, Acts as Nested Set. 4449 */ 4450 4451 /** 4452 * actAs provides a method for extending Active Record models. 4453 * 4454 * Example: 4455 * $this->actsAs('list', array('scope' => 'todo_list')); 4456 */ 4457 function actsAs($behaviour, $options = array()) 4458 { 4459 $class_name = $this->_getActAsClassName($behaviour); 4460 $underscored_place_holder = AkInflector::underscore($behaviour); 4461 $camelized_place_holder = AkInflector::camelize($underscored_place_holder); 4462 4463 if($this->$underscored_place_holder =& $this->_getActAsInstance($class_name, $options)){ 4464 $this->$camelized_place_holder =& $this->$underscored_place_holder; 4465 if($this->$underscored_place_holder->init($options)){ 4466 $this->__ActsLikeAttributes[$underscored_place_holder] = $underscored_place_holder; 4467 } 4468 } 4469 } 4470 4471 /** 4472 * @access private 4473 */ 4474 function _getActAsClassName($behaviour) 4475 { 4476 $class_name = AkInflector::camelize($behaviour); 4477 return file_exists(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkActsAsBehaviours'.DS.'AkActsAs'.$class_name.'.php') && !class_exists('ActsAs'.$class_name) ? 4478 'AkActsAs'.$class_name : 'ActsAs'.$class_name; 4479 } 4480 4481 /** 4482 * @access private 4483 */ 4484 function &_getActAsInstance($class_name, $options) 4485 { 4486 if(!class_exists($class_name)){ 4487 if(substr($class_name,0,2) == 'Ak'){ 4488 include_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkActsAsBehaviours'.DS.$class_name.'.php'); 4489 }else{ 4490 include_once(AK_APP_PLUGINS_DIR.DS.AkInflector::underscore($class_name).DS.'lib'.DS.$class_name.'.php'); 4491 } 4492 } 4493 if(!class_exists($class_name)){ 4494 trigger_error(Ak::t('The class %class used for handling an "act_as %class" does not exist',array('%class'=>$class_name)), E_USER_ERROR); 4495 $false = false; 4496 return $false; 4497 }else{ 4498 $ActAsInstance =& new $class_name($this, $options); 4499 return $ActAsInstance; 4500 } 4501 } 4502 4503 /** 4504 * @access private 4505 */ 4506 function _loadActAsBehaviours() 4507 { 4508 //$this->act_as = !empty($this->acts_as) ? $this->acts_as : (empty($this->act_as) ? false : $this->act_as); 4509 if(!empty($this->act_as)){ 4510 if(is_string($this->act_as)){ 4511 $this->act_as = array_unique(array_diff(array_map('trim',explode(',',$this->act_as.',')), array(''))); 4512 foreach ($this->act_as as $type){ 4513 $this->actsAs($type); 4514 } 4515 }elseif (is_array($this->act_as)){ 4516 foreach ($this->act_as as $type=>$options){ 4517 $this->actsAs($type, $options); 4518 } 4519 } 4520 } 4521 } 4522 4523 /** 4524 * Returns a comma separated list of possible acts like (active record, nested set, list).... 4525 */ 4526 function actsLike() 4527 { 4528 $result = 'active record'; 4529 foreach ($this->__ActsLikeAttributes as $type){ 4530 if(!empty($this->$type) && is_object($this->$type) && method_exists($this->{$type}, 'getType')){ 4531 $result .= ','.$this->{$type}->getType(); 4532 } 4533 } 4534 return $result; 4535 } 4536 4537 /*/Act as Behaviours*/ 4538 4539 /** 4540 Debugging 4541 ==================================================================== 4542 */ 4543 4544 4545 function dbug() 4546 { 4547 if(!$this->isConnected()){ 4548 $this->setConnection(); 4549 } 4550 $this->_db->connection->debug = $this->_db->connection->debug ? false : true; 4551 $this->db_debug =& $this->_db->connection->debug; 4552 } 4553 4554 function toString($print = false) 4555 { 4556 $result = ''; 4557 if(!AK_CLI || (AK_ENVIRONMENT == 'testing' && !AK_CLI)){ 4558 $result = "<h2>Details for ".AkInflector::humanize(AkInflector::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()."</h2>\n<dl>\n"; 4559 foreach ($this->getColumnNames() as $column=>$caption){ 4560 $result .= "<dt>$caption</dt>\n<dd>".$this->getAttribute($column)."</dd>\n"; 4561 } 4562 $result .= "</dl>\n<hr />"; 4563 if($print){ 4564 echo $result; 4565 } 4566 }elseif(AK_DEV_MODE){ 4567 $result = "\n". 4568 str_replace("\n"," ",var_export($this->getAttributes(),true)); 4569 $result .= "\n"; 4570 echo $result; 4571 return ''; 4572 }elseif (AK_CLI){ 4573 $result = "\n-------\n Details for ".AkInflector::humanize(AkInflector::underscore($this->getModelName()))." with ".$this->getPrimaryKey()." ".$this->getId()." ==\n\n/==\n"; 4574 foreach ($this->getColumnNames() as $column=>$caption){ 4575 $result .= "\t * $caption: ".$this->getAttribute($column)."\n"; 4576 } 4577 $result .= "\n\n-------\n"; 4578 if($print){ 4579 echo $result; 4580 } 4581 } 4582 return $result; 4583 } 4584 4585 function dbugging($trace_this_on_debug_mode = null) 4586 { 4587 if(!empty($this->_db->debug) && !empty($trace_this_on_debug_mode)){ 4588 $message = !is_scalar($trace_this_on_debug_mode) ? var_export($trace_this_on_debug_mode, true) : (string)$trace_this_on_debug_mode; 4589 Ak::trace($message); 4590 } 4591 return !empty($this->_db->debug); 4592 } 4593 4594 4595 4596 function debug ($data = 'active_record_class', $_functions=0) 4597 { 4598 if(!AK_DEBUG && !AK_DEV_MODE){ 4599 return; 4600 } 4601 4602 $data = $data == 'active_record_class' ? (AK_PHP5 ? clone($this) : $this) : $data; 4603 4604 if($_functions!=0) { 4605 $sf=1; 4606 } else { 4607 $sf=0 ; 4608 } 4609 4610 if (isset ($data)) { 4611 if (is_array($data) || is_object($data)) { 4612 4613 if (count ($data)) { 4614 echo AK_CLI ? "/--\n" : "<ol>\n"; 4615 while (list ($key,$value) = each ($data)) { 4616 if($key{0} == '_'){ 4617 continue; 4618 } 4619 $type=gettype($value); 4620 if ($type=="array") { 4621 AK_CLI ? printf ("\t* (%s) %s:\n",$type, $key) : 4622 printf ("<li>(%s) <b>%s</b>:\n",$type, $key); 4623 ob_start(); 4624 Ak::debug ($value,$sf); 4625 $lines = explode("\n",ob_get_clean()."\n"); 4626 foreach ($lines as $line){ 4627 echo "\t".$line."\n"; 4628 } 4629 }elseif($type == "object"){ 4630 if(method_exists($value,'hasColumn') && $value->hasColumn($key)){ 4631 $value->toString(true); 4632 AK_CLI ? printf ("\t* (%s) %s:\n",$type, $key) : 4633 printf ("<li>(%s) <b>%s</b>:\n",$type, $key); 4634 ob_start(); 4635 Ak::debug ($value,$sf); 4636 $lines = explode("\n",ob_get_clean()."\n"); 4637 foreach ($lines as $line){ 4638 echo "\t".$line."\n"; 4639 } 4640 } 4641 }elseif (eregi ("function", $type)) { 4642 if ($sf) { 4643 AK_CLI ? printf ("\t* (%s) %s:\n",$type, $key, $value) : 4644 printf ("<li>(%s) <b>%s</b> </li>\n",$type, $key, $value); 4645 } 4646 } else { 4647 if (!$value) { 4648 $value="(none)"; 4649 } 4650 AK_CLI ? printf ("\t* (%s) %s = %s\n",$type, $key, $value) : 4651 printf ("<li>(%s) <b>%s</b> = %s</li>\n",$type, $key, $value); 4652 } 4653 } 4654 echo AK_CLI ? "\n--/\n" : "</ol>fin.\n"; 4655 } else { 4656 echo "(empty)"; 4657 } 4658 } 4659 } 4660 } 4661 4662 /*/Debugging*/ 4663 4664 4665 4666 /** 4667 Utilities 4668 ==================================================================== 4669 */ 4670 /** 4671 * Selects and filters a search result to include only specified columns 4672 * 4673 * $people_for_select = $People->select($People->find(),'name','email'); 4674 * 4675 * Now $people_for_select will hold an array with 4676 * array ( 4677 * array ('name' => 'Jose','email' => 'jose@example.com'), 4678 * array ('name' => 'Alicia','email' => 'alicia@example.com'), 4679 * array ('name' => 'Hilario','email' => 'hilario@example.com'), 4680 * array ('name' => 'Bermi','email' => 'bermi@example.com') 4681 * ); 4682 */ 4683 function select(&$source_array) 4684 { 4685 $resulting_array = array(); 4686 if(!empty($source_array) && is_array($source_array) && func_num_args() > 1) { 4687 (array)$args = array_filter(array_slice(func_get_args(),1),array($this,'hasColumn')); 4688 foreach ($source_array as $source_item){ 4689 $item_fields = array(); 4690 foreach ($args as $arg){ 4691 $item_fields[$arg] =& $source_item->get($arg); 4692 } 4693 $resulting_array[] =& $item_fields; 4694 } 4695 } 4696 return $resulting_array; 4697 } 4698 4699 4700 /** 4701 * Collect is a function for selecting items from double depth array 4702 * like the ones returned by the AkActiveRecord. This comes useful when you just need some 4703 * fields for generating tables, select lists with only desired fields. 4704 * 4705 * $people_for_select = Ak::select($People->find(),'id','email'); 4706 * 4707 * Returns something like: 4708 * array ( 4709 * array ('10' => 'jose@example.com'), 4710 * array ('15' => 'alicia@example.com'), 4711 * array ('16' => 'hilario@example.com'), 4712 * array ('18' => 'bermi@example.com') 4713 * ); 4714 */ 4715 function collect(&$source_array, $key_index, $value_index) 4716 { 4717 $resulting_array = array(); 4718 if(!empty($source_array) && is_array($source_array)) { 4719 foreach ($source_array as $source_item){ 4720 $resulting_array[$source_item->get($key_index)] = $source_item->get($value_index); 4721 } 4722 } 4723 return $resulting_array; 4724 } 4725 4726 /** 4727 * Generate a json representation of the model record. 4728 * 4729 * parameters: 4730 * 4731 * @param array $options 4732 * 4733 * option parameters: 4734 * array( 4735 * 'collection' => array($Person1,$Person), // array of ActiveRecords 4736 * 'include' => array('association1','association2'), // include the associations when exporting 4737 * 'exclude' => array('id','name'), // exclude the attribtues 4738 * 'only' => array('email','last_name') // only export these attributes 4739 * ) 4740 * @return string in Json Format 4741 */ 4742 function toJson($options = array()) 4743 { 4744 if (is_array($options) && isset($options[0]) && is_a($options[0], 'AkActiveRecord')) { 4745 $options = array('collection'=>$options); 4746 } 4747 if (isset($options['collection']) && is_array($options['collection']) && $options['collection'][0]->_modelName == $this->_modelName) { 4748 $json = ''; 4749 4750 $collection = $options['collection']; 4751 unset($options['collection']); 4752 $jsonVals = array(); 4753 foreach ($collection as $element) { 4754 $jsonVals[]= $element->toJson($options); 4755 } 4756 $json = '['.implode(',',$jsonVals).']'; 4757 return $json; 4758 } 4759 /** 4760 * see if we need to include associations 4761 */ 4762 $associatedIds = array(); 4763 if (isset($options['include']) && !empty($options['include'])) { 4764 $options['include'] = is_array($options['include'])?$options['include']:preg_split('/,\s*/',$options['include']); 4765 foreach ($this->_associations as $key => $obj) { 4766 if (in_array($key,$options['include'])) { 4767 $associatedIds[$obj->getAssociationId() . '_id'] = array('name'=>$key,'type'=>$obj->getType()); 4768 } 4769 } 4770 } 4771 if (isset($options['only'])) { 4772 $options['only'] = is_array($options['only'])?$options['only']:preg_split('/,\s*/',$options['only']); 4773 } 4774 if (isset($options['except'])) { 4775 $options['except'] = is_array($options['except'])?$options['except']:preg_split('/,\s*/',$options['except']); 4776 } 4777 foreach ($this->_columns as $key => $def) { 4778 4779 if (isset($options['except']) && in_array($key, $options['except'])) { 4780 continue; 4781 } else if (isset($options['only']) && !in_array($key, $options['only'])) { 4782 continue; 4783 } else { 4784 $val = $this->$key; 4785 $type = $this->getColumnType($key); 4786 if (($type == 'serial' || $type=='integer') && $val!==null) $val = intval($val); 4787 if ($type == 'float' && $val!==null) $val = floatval($val); 4788 if ($type == 'boolean') $val = $val?1:0; 4789 $data[$key] = $val; 4790 } 4791 } 4792 if (isset($options['include'])) { 4793 foreach($this->_associationIds as $key=>$val) { 4794 if ((in_array($key,$options['include']) || in_array($val,$options['include']))) { 4795 $this->$key->load(); 4796 $associationElement = $key; 4797 $associationElement = $this->_convert_column_to_xml_element($associationElement); 4798 if (is_array($this->$key)) { 4799 $data[$associationElement] = array(); 4800 foreach ($this->$key as $el) { 4801 if (is_a($el,'AkActiveRecord')) { 4802 $attributes = $el->getAttributes(); 4803 foreach($attributes as $ak=>$av) { 4804 $type = $el->getColumnType($ak); 4805 if (($type == 'serial' || $type=='integer') && $av!==null) $av = intval($av); 4806 if ($type == 'float' && $av!==null) $av = floatval($av); 4807 if ($type == 'boolean') $av = $av?1:0; 4808 $attributes[$ak]=$av; 4809 } 4810 $data[$associationElement][] = $attributes; 4811 } 4812 } 4813 } else { 4814 $el = &$this->$key->load(); 4815 if (is_a($el,'AkActiveRecord')) { 4816 $attributes = $el->getAttributes(); 4817 foreach($attributes as $ak=>$av) { 4818 $type = $el->getColumnType($ak); 4819 if (($type == 'serial' || $type=='integer') && $av!==null) $av = intval($av); 4820 if ($type == 'float' && $av!==null) $av = floatval($av); 4821 if ($type == 'boolean') $av = $av?1:0; 4822 $attributes[$ak]=$av; 4823 } 4824 $data[$associationElement] = $attributes; 4825 } 4826 } 4827 } 4828 } 4829 } 4830 return Ak::toJson($data); 4831 } 4832 function _convert_column_to_xml_element($col) 4833 { 4834 return str_replace('_','-',$col); 4835 } 4836 function _convert_column_from_xml_element($col) 4837 { 4838 return str_replace('-','_',$col); 4839 } 4840 4841 function _parseXmlAttributes($attributes) 4842 { 4843 $new = array(); 4844 foreach($attributes as $key=>$value) 4845 { 4846 $new[$this->_convert_column_from_xml_element($key)] = $value; 4847 } 4848 return $new; 4849 } 4850 4851 function &_generateModelFromArray($modelName,$attributes) 4852 { 4853 if (isset($attributes[0]) && is_array($attributes[0])) { 4854 $attributes = $attributes[0]; 4855 } 4856 $record = new $modelName('attributes',$this->_parseXmlAttributes($attributes)); 4857 $record->_newRecord = !empty($attributes['id']); 4858 4859 $associatedIds = array(); 4860 foreach ($record->getAssociatedIds() as $key) { 4861 if (isset($attributes[$key]) && is_array($attributes[$key])) { 4862 $class = $record->$key->_AssociationHandler->getOption($key,'class_name'); 4863 $related = $this->_generateModelFromArray($class,$attributes[$key]); 4864 $record->$key->build($related->getAttributes(),false); 4865 $related = &$record->$key->load(); 4866 $record->$key = &$related; 4867 } 4868 } 4869 return $record; 4870 } 4871 4872 function _fromArray($array) 4873 { 4874 $data = $array; 4875 $modelName = $this->getModelName(); 4876 $values = array(); 4877 if (!isset($data[0])) { 4878 $data = array($data); 4879 } 4880 foreach ($data as $key => $value) { 4881 if (is_array($value)){ 4882 $values[] = &$this->_generateModelFromArray($modelName,$value); 4883 } 4884 } 4885 return count($values)==1?$values[0]:$values; 4886 } 4887 4888 /** 4889 * Reads Xml in the following format: 4890 * 4891 * 4892 * <?xml version="1.0" encoding="UTF-8"?> 4893 * <person> 4894 * <id>1</id> 4895 * <first-name>Hansi</first-name> 4896 * <last-name>Müller</last-name> 4897 * <email>hans@mueller.com</email> 4898 * <created-at type="datetime">2008-01-01 13:01:23</created-at> 4899 * </person> 4900 * 4901 * and returns an ActiveRecord Object 4902 * 4903 * @param string $xml 4904 * @return AkActiveRecord 4905 */ 4906 function fromXml($xml) 4907 { 4908 $array = Ak::xml_to_array($xml); 4909 $array = $this->_fromXmlCleanup($array); 4910 return $this->_fromArray($array); 4911 } 4912 4913 function _fromXmlCleanup($array) 4914 { 4915 $result = array(); 4916 $key = key($array); 4917 while(is_string($key) && is_array($array[$key]) && count($array[$key])==1) { 4918 $array = $array[$key][0]; 4919 $key = key($array); 4920 } 4921 if (is_string($key) && is_array($array[$key])) { 4922 $array = $array[$key]; 4923 } 4924 return $array; 4925 } 4926 /** 4927 * Reads Json string in the following format: 4928 * 4929 * {"id":1,"first_name":"Hansi","last_name":"M\u00fcller", 4930 * "email":"hans@mueller.com","created_at":"2008-01-01 13:01:23"} 4931 * 4932 * and returns an ActiveRecord Object 4933 * 4934 * @param string $json 4935 * @return AkActiveRecord 4936 */ 4937 function fromJson($json) 4938 { 4939 $json = Ak::fromJson($json); 4940 $array = Ak::convert('Object','Array',$json); 4941 return $this->_fromArray($array); 4942 } 4943 4944 /** 4945 * Generate a xml representation of the model record. 4946 * 4947 * Example result: 4948 * 4949 * <?xml version="1.0" encoding="UTF-8"?> 4950 * <person> 4951 * <id>1</id> 4952 * <first-name>Hansi</first-name> 4953 * <last-name>Müller</last-name> 4954 * <email>hans@mueller.com</email> 4955 * <created-at type="datetime">2008-01-01 13:01:23</created-at> 4956 * </person> 4957 * 4958 * parameters: 4959 * 4960 * @param array $options 4961 * 4962 * option parameters: 4963 * array( 4964 * 'collection' => array($Person1,$Person), // array of ActiveRecords 4965 * 'include' => array('association1','association2'), // include the associations when exporting 4966 * 'exclude' => array('id','name'), // exclude the attribtues 4967 * 'only' => array('email','last_name') // only export these attributes 4968 * ) 4969 * @return string in Xml Format 4970 */ 4971 function toXml($options = array()) 4972 { 4973 if (is_array($options) && isset($options[0]) && is_a($options[0], 'AkActiveRecord')) { 4974 $options = array('collection'=>$options); 4975 } 4976 if (isset($options['collection']) && is_array($options['collection']) && $options['collection'][0]->_modelName == $this->_modelName) { 4977 $root = strtolower(AkInflector::pluralize($this->_modelName)); 4978 $root = $this->_convert_column_to_xml_element($root); 4979 $xml = ''; 4980 if (!(isset($options['skip_instruct']) && $options['skip_instruct'] == true)) { 4981 $xml .= '<?xml version="1.0" encoding="UTF-8"?>'; 4982 } 4983 $xml .= '<' . $root . '>'; 4984 $collection = $options['collection']; 4985 unset($options['collection']); 4986 $options['skip_instruct'] = true; 4987 foreach ($collection as $element) { 4988 $xml .= $element->toXml($options); 4989 } 4990 $xml .= '</' . $root .'>'; 4991 return $xml; 4992 } 4993 /** 4994 * see if we need to include associations 4995 */ 4996 $associatedIds = array(); 4997 if (isset($options['include']) && !empty($options['include'])) { 4998 $options['include'] = is_array($options['include'])?$options['include']:preg_split('/,\s*/',$options['include']); 4999 foreach ($this->_associations as $key => $obj) { 5000 if (in_array($key,$options['include'])) { 5001 if ($obj->getType()!='hasAndBelongsToMany') { 5002 $associatedIds[$obj->getAssociationId() . '_id'] = array('name'=>$key,'type'=>$obj->getType()); 5003 } else { 5004 $associatedIds[$key] = array('name'=>$key,'type'=>$obj->getType()); 5005 } 5006 } 5007 } 5008 } 5009 if (isset($options['only'])) { 5010 $options['only'] = is_array($options['only'])?$options['only']:preg_split('/,\s*/',$options['only']); 5011 } 5012 if (isset($options['except'])) { 5013 $options['except'] = is_array($options['except'])?$options['except']:preg_split('/,\s*/',$options['except']); 5014 } 5015 $xml = ''; 5016 if (!(isset($options['skip_instruct']) && $options['skip_instruct'] == true)) { 5017 $xml .= '<?xml version="1.0" encoding="UTF-8"?>'; 5018 } 5019 $root = $this->_convert_column_to_xml_element(strtolower($this->_modelName)); 5020 5021 $xml .= '<' . $root . '>'; 5022 $xml .= "\n"; 5023 foreach ($this->_columns as $key => $def) { 5024 5025 if (isset($options['except']) && in_array($key, $options['except'])) { 5026 continue; 5027 } else if (isset($options['only']) && !in_array($key, $options['only'])) { 5028 continue; 5029 } else { 5030 $columnType = $def['type']; 5031 $elementName = $this->_convert_column_to_xml_element($key); 5032 $xml .= '<' . $elementName; 5033 $val = $this->$key; 5034 if (!in_array($columnType,array('string','text','serial'))) { 5035 $xml .= ' type="' . $columnType . '"'; 5036 if ($columnType=='boolean') $val = $val?1:0; 5037 } 5038 $xml .= '>' . Ak::utf8($val) . '</' . $elementName . '>'; 5039 $xml .= "\n"; 5040 } 5041 } 5042 if (isset($options['include'])) { 5043 foreach($this->_associationIds as $key=>$val) { 5044 if ((in_array($key,$options['include']) || in_array($val,$options['include']))) { 5045 if (is_array($this->$key)) { 5046 5047 $associationElement = $key; 5048 $associationElement = AkInflector::pluralize($associationElement); 5049 $associationElement = $this->_convert_column_to_xml_element($associationElement); 5050 $xml .= '<'.$associationElement.'>'; 5051 foreach ($this->$key as $el) { 5052 if (is_a($el,'AkActiveRecord')) { 5053 $xml .= $el->toXml(array('skip_instruct'=>true)); 5054 } 5055 } 5056 $xml .= '</' . $associationElement .'>'; 5057 } else { 5058 $el = &$this->$key->load(); 5059 if (is_a($el,'AkActiveRecord')) { 5060 $xml.=$el->toXml(array('skip_instruct'=>true)); 5061 } 5062 } 5063 } 5064 } 5065 } 5066 $xml .= '</' . $root . '>'; 5067 return $xml; 5068 } 5069 /** 5070 * converts to yaml-strings 5071 * 5072 * examples: 5073 * User::toYaml($users->find('all')); 5074 * $Bermi->toYaml(); 5075 * 5076 * @param array of ActiveRecords[optional] $data 5077 */ 5078 function toYaml($data = null) 5079 { 5080 return Ak::convert('active_record', 'yaml', empty($data) ? $this : $data); 5081 } 5082 5083 5084 /** 5085 * Parses an special formated array as a list of keys and values 5086 * 5087 * This function generates an array with values and keys from an array with numeric keys. 5088 * 5089 * This allows to parse an array to a function in the following manner. 5090 * create('first_name->', 'Bermi', 'last_name->', 'Ferrer'); 5091 * //Previous code will be the same that 5092 * create(array('first_name'=>'Bermi', 'last_name'=> 'Ferrer')); 5093 * 5094 * Use this syntax only for quick testings, not for production environments. If the number of arguments varies, the result might be unpredictable. 5095 * 5096 * This function syntax is disabled by default. You need to define('AK_ENABLE_AKELOS_ARGS', true) 5097 * if you need this functionality. 5098 * 5099 * @deprecated 5100 */ 5101 function parseAkelosArgs(&$args) 5102 { 5103 if(!AK_ENABLE_AKELOS_ARGS){ 5104 $this->_castDateParametersFromDateHelper_($args); 5105 return ; 5106 } 5107 $k = array_keys($args); 5108 if(isset($k[1]) && substr($args[$k[0]],-1) == '>'){ 5109 $size = sizeOf($k); 5110 $params = array(); 5111 for($i = 0; $i < $size; $i++ ) { 5112 $v = $args[$k[$i]]; 5113 if(!isset($key) && is_string($args[$k[$i]]) && substr($v,-1) == '>'){ 5114 $key = rtrim($v, '=-> '); 5115 }elseif(isset($key)) { 5116 $params[$key] = $v; 5117 unset($key); 5118 }else{ 5119 $params[$k[$i]] = $v; 5120 } 5121 } 5122 if(!empty($params)){ 5123 $args = $params; 5124 } 5125 } 5126 $this->_castDateParametersFromDateHelper_($args); 5127 } 5128 /** 5129 * Gets an array from a string. 5130 * 5131 * Acts like Php explode() function but uses any of this as valid separators ' AND ',' and ',' + ',' ',',',';' 5132 */ 5133 function getArrayFromAkString($string) 5134 { 5135 if(is_array($string)){ 5136 return $string; 5137 } 5138 $string = str_replace(array(' AND ',' and ',' + ',' ',',',';'),array('|','|','|','','|','|'),trim($string)); 5139 return strstr($string,'|') ? explode('|', $string) : array($string); 5140 } 5141 /*/Utilities*/ 5142 5143 5144 function getAttributeCondition($argument) 5145 { 5146 if(is_array($argument)){ 5147 return 'IN (?)'; 5148 }elseif (is_null($argument)){ 5149 return 'IS ?'; 5150 }else{ 5151 return '= ?'; 5152 } 5153 } 5154 5155 5156 /** 5157 Calculations 5158 ==================================================================== 5159 */ 5160 5161 /** 5162 * @access private 5163 */ 5164 var $_calculation_options = array('conditions', 'joins', 'order', 'select', 'group', 'having', 'distinct', 'limit', 'offset'); 5165 5166 /** 5167 * Count operates using three different approaches. 5168 * 5169 * * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. 5170 * * Count by conditions or joins 5171 * * Count using options will find the row count matched by the options used. 5172 * 5173 * The last approach, count using options, accepts an option hash as the only parameter. The options are: 5174 * 5175 * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array("user_name = ?", $username ). See conditions in the intro. 5176 * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). 5177 * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). 5178 * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. 5179 * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join. 5180 * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... 5181 * 5182 * Examples for counting all: 5183 * $Person->count(); // returns the total count of all people 5184 * 5185 * Examples for count by +conditions+ and +joins+ (this has been deprecated): 5186 * $Person->count("age > 26"); // returns the number of people older than 26 5187 * $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(*). 5188 * 5189 * Examples for count with options: 5190 * $Person->count('conditions' => "age > 26"); 5191 * $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. 5192 * $Person->count('id', 'conditions' => "age > 26"); // Performs a COUNT(id) 5193 * $Person->count('all', 'conditions' => "age > 26"); // Performs a COUNT(*) ('all' is an alias for '*') 5194 * 5195 * Note: $Person->count('all') will not work because it will use 'all' as the condition. Use $Person->count() instead. 5196 */ 5197 function count() 5198 { 5199 $args = func_get_args(); 5200 list($column_name, $options) = $this->_constructCountOptionsFromLegacyArgs($args); 5201 return $this->calculate('count', $column_name, $options); 5202 } 5203 5204 /** 5205 * Calculates average value on a given column. The value is returned as a float. See #calculate for examples with options. 5206 * 5207 * $Person->average('age'); 5208 */ 5209 function average($column_name, $options = array()) 5210 { 5211 return $this->calculate('avg', $column_name, $options); 5212 } 5213 5214 /** 5215 * 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. 5216 * 5217 * $Person->minimum('age'); 5218 */ 5219 function minimum($column_name, $options = array()) 5220 { 5221 return $this->calculate('min', $column_name, $options); 5222 } 5223 5224 /** 5225 * 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. 5226 * 5227 * $Person->maximum('age'); 5228 */ 5229 function maximum($column_name, $options = array()) 5230 { 5231 return $this->calculate('max', $column_name, $options); 5232 } 5233 5234 /** 5235 * 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. 5236 * 5237 * $Person->sum('age'); 5238 */ 5239 function sum($column_name, $options = array()) 5240 { 5241 return $this->calculate('sum', $column_name, $options); 5242 } 5243 5244 /** 5245 * This calculates aggregate values in the given column: Methods for count, sum, average, minimum, and maximum have been added as shortcuts. 5246 * Options such as 'conditions', 'order', 'group', 'having', and 'joins' can be passed to customize the query. 5247 * 5248 * There are two basic forms of output: 5249 * * 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. 5250 * * Grouped values: This returns an ordered hash of the values and groups them by the 'group' option. It takes a column name. 5251 * 5252 * $values = $Person->maximum('age', array('group' => 'last_name')); 5253 * echo $values["Drake"] 5254 * => 43 5255 * 5256 * Options: 5257 * * <tt>'conditions'</tt>: An SQL fragment like "administrator = 1" or array( "user_name = ?", username ). See conditions in the intro. 5258 * * <tt>'joins'</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). 5259 * The records will be returned read-only since they will have attributes that do not correspond to the table's columns. 5260 * * <tt>'order'</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). 5261 * * <tt>'group'</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. 5262 * * <tt>'select'</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join. 5263 * * <tt>'distinct'</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... 5264 * 5265 * Examples: 5266 * $Person->calculate('count', 'all'); // The same as $Person->count(); 5267 * $Person->average('age'); // SELECT AVG(age) FROM people... 5268 * $Person->minimum('age', array('conditions' => array('last_name != ?', 'Drake'))); // Selects the minimum age for everyone with a last name other than 'Drake' 5269 * $Person->minimum('age', array('having' => 'min(age) > 17', 'group' => 'last'_name)); // Selects the minimum age for any family without any minors 5270 */ 5271 function calculate($operation, $column_name, $options = array()) 5272 { 5273 $this->_validateCalculationOptions($options); 5274 $column_name = empty($options['select']) ? $column_name : $options['select']; 5275 $column_name = $column_name == 'all' ? '*' : $column_name; 5276 $column = $this->_getColumnFor($column_name); 5277 if (!empty($options['group'])){ 5278 return $this->_executeGroupedCalculation($operation, $column_name, $column, $options); 5279 }else{ 5280 return $this->_executeSimpleCalculation($operation, $column_name, $column, $options); 5281 } 5282 5283 return 0; 5284 } 5285 5286 /** 5287 * @access private 5288 */ 5289 function _constructCountOptionsFromLegacyArgs($args) 5290 { 5291 $options = array(); 5292 $column_name = 'all'; 5293 5294 /* 5295 We need to handle 5296 count() 5297 count(options=array()) 5298 count($column_name='all', $options=array()) 5299 count($conditions=null, $joins=null) 5300 */ 5301 if(count($args) > 2){ 5302 trigger_error(Ak::t("Unexpected parameters passed to count(\$options=array())", E_USER_ERROR)); 5303 }elseif(count($args) > 0){ 5304 if(!empty($args[0]) && is_array($args[0])){ 5305 $options = $args[0]; 5306 }elseif(!empty($args[1]) && is_array($args[1])){ 5307 $column_name = array_shift($args); 5308 $options = array_shift($args); 5309 }else{ 5310 $options = array('conditions' => $args[0]); 5311 if(!empty($args[1])){ 5312 $options = array_merge($options, array('joins' => $args[1])); 5313 } 5314 } 5315 } 5316 return array($column_name, $options); 5317 } 5318 5319 5320 /** 5321 * @access private 5322 */ 5323 function _constructCalculationSql($operation, $column_name, $options) 5324 { 5325 $operation = strtolower($operation); 5326 $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name); 5327 $use_workaround = $operation == 'count' && !empty($options['distinct']) && $this->_getDatabaseType() == 'sqlite'; 5328 5329 $sql = $use_workaround ? 5330 "SELECT COUNT(*) AS $aggregate_alias" : // A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT. 5331 "SELECT $operation(".(empty($options['distinct'])?'':'DISTINCT ')."$column_name) AS $aggregate_alias"; 5332 5333 5334 $sql .= empty($options['group']) ? '' : ", {$options['group_field']} AS {$options['group_alias']}"; 5335 $sql .= $use_workaround ? " FROM (SELECT DISTINCT {$column_name}" : ''; 5336 $sql .= " FROM ".$this->getTableName()." "; 5337 5338 $sql .= empty($options['joins']) ? '' : " {$options['joins']} "; 5339 5340 empty($options['conditions']) ? null : $this->addConditions($sql, $options['conditions']); 5341 5342 if (!empty($options['group'])){ 5343 $sql .= " GROUP BY {$options['group_field']} "; 5344 $sql .= empty($options['having']) ? '' : " HAVING {$options['having']} "; 5345 } 5346 5347 $sql .= empty($options['order']) ? '' : " ORDER BY {$options['order']} "; 5348 $this->_db->addLimitAndOffset($sql, $options); 5349 $sql .= $use_workaround ? ')' : ''; 5350 return $sql; 5351 } 5352 5353 5354 /** 5355 * @access private 5356 */ 5357 function _executeSimpleCalculation($operation, $column_name, $column, $options) 5358 { 5359 $value = $this->_db->selectValue($this->_constructCalculationSql($operation, $column_name, $options)); 5360 return $this->_typeCastCalculatedValue($value, $column, $operation); 5361 } 5362 5363 /** 5364 * @access private 5365 */ 5366 function _executeGroupedCalculation($operation, $column_name, $column, $options) 5367 { 5368 $group_field = $options['group']; 5369 $group_alias = $this->_getColumnAliasFor($group_field); 5370 $group_column = $this->_getColumnFor($group_field); 5371 $options = array_merge(array('group_field' => $group_field, 'group_alias' => $group_alias),$options); 5372 $sql = $this->_constructCalculationSql($operation, $column_name, $options); 5373 $calculated_data = $this->_db->select($sql); 5374 $aggregate_alias = $this->_getColumnAliasFor($operation, $column_name); 5375 5376 $all = array(); 5377 foreach ($calculated_data as $row){ 5378 $key = $this->_typeCastCalculatedValue($row[$group_alias], $group_column); 5379 $all[$key] = $this->_typeCastCalculatedValue($row[$aggregate_alias], $column, $operation); 5380 } 5381 return $all; 5382 } 5383 5384 /** 5385 * @access private 5386 */ 5387 function _validateCalculationOptions($options = array()) 5388 { 5389 $invalid_options = array_diff(array_keys($options),$this->_calculation_options); 5390 if(!empty($invalid_options)){ 5391 trigger_error(Ak::t('%options are not valid calculation options.', array('%options'=>join(', ',$invalid_options))), E_USER_ERROR); 5392 } 5393 } 5394 5395 /** 5396 * Converts a given key to the value that the database adapter returns as 5397 * as a usable column name. 5398 * users.id #=> users_id 5399 * sum(id) #=> sum_id 5400 * count(distinct users.id) #=> count_distinct_users_id 5401 * count(*) #=> count_all 5402 * 5403 * @access private 5404 */ 5405 function _getColumnAliasFor() 5406 { 5407 $args = func_get_args(); 5408 $keys = strtolower(join(' ',(!empty($args) ? (is_array($args[0]) ? $args[0] : $args) : array()))); 5409 return preg_replace(array('/\*/','/\W+/','/^ +/','/ +$/','/ +/'),array('all',' ','','','_'), $keys); 5410 } 5411 5412 /** 5413 * @access private 5414 */ 5415 function _getColumnFor($field) 5416 { 5417 $field_name = ltrim(substr($field,strpos($field,'.')),'.'); 5418 if(in_array($field_name,$this->getColumnNames())){ 5419 return $field_name; 5420 } 5421 return $field; 5422 } 5423 5424 /** 5425 * @access private 5426 */ 5427 function _typeCastCalculatedValue($value, $column, $operation = null) 5428 { 5429 $operation = strtolower($operation); 5430 if($operation == 'count'){ 5431 return intval($value); 5432 }elseif ($operation == 'avg'){ 5433 return floatval($value); 5434 }else{ 5435 return empty($column) ? $value : AkActiveRecord::castAttributeFromDatabase($column, $value); 5436 } 5437 } 5438 5439 /*/Calculations*/ 5440 5441 function hasBeenModified() 5442 { 5443 return Ak::objectHasBeenModified($this); 5444 } 5445 5446 /** 5447 * Just freeze the attributes hash, such that associations are still accessible even on destroyed records. 5448 * 5449 * @todo implement freeze correctly for its intended use 5450 */ 5451 function freeze() 5452 { 5453 return $this->_freeze = true; 5454 } 5455 5456 function isFrozen() 5457 { 5458 return !empty($this->_freeze); 5459 } 5460 5461 /** 5462 * Alias for getModelName() 5463 */ 5464 function getType() 5465 { 5466 return $this->getModelName(); 5467 } 5468 5469 function &objectCache() 5470 { 5471 static $cache; 5472 $false = false; 5473 $args =& func_get_args(); 5474 if(count($args) == 2){ 5475 if(!isset($cache[$args[0]])){ 5476 $cache[$args[0]] =& $args[1]; 5477 } 5478 }elseif(!isset($cache[$args[0]])){ 5479 return $false; 5480 } 5481 return $cache[$args[0]]; 5482 } 5483 5484 5485 /** 5486 Connection adapters 5487 ==================================================================== 5488 Right now Akelos uses phpAdodb for bd abstraction. This are functionalities not 5489 provided in phpAdodb and that will move to a separated driver for each db 5490 engine in a future 5491 */ 5492 function _extractValueFromDefault($default) 5493 { 5494 if($this->_getDatabaseType() == 'postgre'){ 5495 if(preg_match("/^'(.*)'::/", $default, $match)){ 5496 return $match[1]; 5497 } 5498 // a postgre HACK; we dont know the column-type here 5499 if ($default=='true') { 5500 return true; 5501 } 5502 if ($default=='false') { 5503 return false; 5504 } 5505 } 5506 return $default; 5507 } 5508 5509 5510 } 5511 5512 5513 ?>
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Mon Oct 27 12:43:49 2008 | Cross-referenced by PHPXref 0.6 |