| [ Index ] |
|
Code source de PHPonTrax 2.6.6-svn |
1 <?php 2 /** 3 * File containing the ActiveRecord class 4 * 5 * (PHP 5) 6 * 7 * @package PHPonTrax 8 * @version $Id: active_record.php 263 2006-09-04 04:50:57Z john $ 9 * @copyright (c) 2005 John Peterson 10 * 11 * Permission is hereby granted, free of charge, to any person obtaining 12 * a copy of this software and associated documentation files (the 13 * "Software"), to deal in the Software without restriction, including 14 * without limitation the rights to use, copy, modify, merge, publish, 15 * distribute, sublicense, and/or sell copies of the Software, and to 16 * permit persons to whom the Software is furnished to do so, subject to 17 * the following conditions: 18 * 19 * The above copyright notice and this permission notice shall be 20 * included in all copies or substantial portions of the Software. 21 * 22 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 26 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 27 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 28 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 */ 30 31 /** 32 * Load the {@link http://pear.php.net/manual/en/package.pear.php PEAR base class} 33 */ 34 require_once('PEAR.php'); 35 36 /** 37 * Load the {@link http://pear.php.net/manual/en/package.database.mdb2.php PEAR MDB2 package} 38 * PEAR::DB is now deprecated. 39 * (This package(DB) been superseded by MDB2 but is still maintained for bugs and security fixes) 40 */ 41 require_once('MDB2.php'); 42 43 /** 44 * Base class for the ActiveRecord design pattern 45 * 46 * <p>Each subclass of this class is associated with a database table 47 * in the Model section of the Model-View-Controller architecture. 48 * By convention, the name of each subclass is the CamelCase singular 49 * form of the table name, which is in the lower_case_underscore 50 * plural notation. For example, 51 * a table named "order_details" would be associated with a subclass 52 * of ActiveRecord named "OrderDetail", and a table named "people" 53 * would be associated with subclass "Person". See the tutorial 54 * {@tutorial PHPonTrax/naming.pkg}</p> 55 * 56 * <p>For a discussion of the ActiveRecord design pattern, see 57 * "Patterns of Enterprise 58 * Application Architecture" by Martin Fowler, pp. 160-164.</p> 59 * 60 * <p>Unit tester: {@link ActiveRecordTest}</p> 61 * 62 * @tutorial PHPonTrax/ActiveRecord.cls 63 */ 64 class ActiveRecord { 65 66 /** 67 * Reference to the database object 68 * 69 * Reference to the database object returned by 70 * {@link http://pear.php.net/manual/en/package.database.mdb2.intro-connect.php PEAR MDB2::Connect()} 71 * @var object DB 72 * see 73 * {@link http://pear.php.net/manual/en/package.database.mdb2.php PEAR MDB2} 74 */ 75 private static $db = null; 76 77 /** 78 * Description of a row in the associated table in the database 79 * 80 * <p>Retrieved from the RDBMS by {@link set_content_columns()}. 81 * See {@link 82 * http://pear.php.net/manual/en/package.database.db.db-common.tableinfo.php 83 * DB_common::tableInfo()} for the format. <b>NOTE:</b> Some 84 * RDBMS's don't return all values.</p> 85 * 86 * <p>An additional element 'human_name' is added to each column 87 * by {@link set_content_columns()}. The actual value contained 88 * in each column is stored in an object variable with the name 89 * given by the 'name' element of the column description for each 90 * column.</p> 91 * 92 * <p><b>NOTE:</b>The information from the database about which 93 * columns are primary keys is <b>not used</b>. Instead, the 94 * primary keys in the table are listed in {@link $primary_keys}, 95 * which is maintained independently.</p> 96 * @var string[] 97 * @see $primary_keys 98 * @see quoted_attributes() 99 * @see __set() 100 */ 101 public $content_columns = null; # info about each column in the table 102 103 /** 104 * Table Info 105 * 106 * Array to hold all the info about table columns. Indexed on $table_name. 107 * @var array 108 */ 109 public static $table_info = array(); 110 111 /** 112 * Class name 113 * 114 * Name of the child class. (this is optional and will automatically be determined) 115 * Normally set to the singular camel case form of the table name. 116 * May be overridden. 117 * @var string 118 */ 119 public $class_name = null; 120 121 /** 122 * Table name 123 * 124 * Name of the table in the database associated with the subclass. 125 * Normally set to the pluralized lower case underscore form of 126 * the class name by the constructor. May be overridden. 127 * @var string 128 */ 129 public $table_name = null; 130 131 /** 132 * Table prefix 133 * 134 * Name to prefix to the $table_name. May be overridden. 135 * @var string 136 */ 137 public $table_prefix = null; 138 139 /** 140 * Database name override 141 * 142 * Name of the database to use, if you are not using the value 143 * read from file config/database.ini 144 * @var string 145 */ 146 public $database_name = null; 147 148 /** 149 * Index into the $active_connections array 150 * 151 * Name of the index to use to return or set the current db connection 152 * Mainly used if you want to connect to different databases between 153 * different models. 154 * @var string 155 */ 156 public $connection_name = TRAX_ENV; 157 158 /** 159 * Stores the database settings 160 */ 161 public static $database_settings = array(); 162 163 /** 164 * Stores the active connections. Indexed on $connection_name. 165 */ 166 public static $active_connections = array(); 167 168 /** 169 * Mode to use when fetching data from database 170 * 171 * See {@link 172 * http://pear.php.net/manual/en/package.database.db.db-common.setfetchmode.php 173 * the relevant PEAR DB class documentation} 174 * @var integer 175 */ 176 public $fetch_mode = MDB2_FETCHMODE_ASSOC; 177 178 /** 179 * Force reconnect to database 180 * 181 * @var boolean 182 */ 183 public $force_reconnect = false; # should we force a connection everytime 184 185 /** 186 * find_all() returns an array of objects, 187 * each object index is off of this field 188 * 189 * @var boolean 190 */ 191 public $index_on = "id"; 192 193 /** 194 * Not yet implemented (page 222 Rails books) 195 * 196 * @var boolean 197 */ 198 public $lock_optimistically = true; 199 200 /** 201 * Composite custom user created objects 202 * @var mixed 203 */ 204 public $composed_of = null; 205 206 # Table associations 207 /** 208 * @todo Document this variable 209 * @var string[] 210 */ 211 protected $has_many = null; 212 213 /** 214 * @todo Document this variable 215 * @var string[] 216 */ 217 protected $has_one = null; 218 219 /** 220 * @todo Document this variable 221 * @var string[] 222 */ 223 protected $has_and_belongs_to_many = null; 224 225 /** 226 * @todo Document this variable 227 * @var string[] 228 */ 229 protected $belongs_to = null; 230 231 /** 232 * @todo Document this variable 233 * @var string[] 234 */ 235 protected $habtm_attributes = null; 236 237 /** 238 * @todo Document this property 239 */ 240 protected $save_associations = array(); 241 242 /** 243 * @todo Document this property 244 * @var boolean 245 */ 246 public $auto_save_associations = true; # where or not to auto save defined associations if set 247 248 /** 249 * Whether this object represents a new record 250 * 251 * true => This object was created without reading a row from the 252 * database, so use SQL 'INSERT' to put it in the database. 253 * false => This object was a row read from the database, so use 254 * SQL 'UPDATE' to update database with new values. 255 * @var boolean 256 */ 257 protected $new_record = true; 258 259 /** 260 * Names of automatic update timestamp columns 261 * 262 * When a row containing one of these columns is updated and 263 * {@link $auto_timestamps} is true, update the contents of the 264 * timestamp columns with the current date and time. 265 * @see $auto_timestamps 266 * @see $auto_create_timestamps 267 * @var string[] 268 */ 269 protected $auto_update_timestamps = array("updated_at","updated_on"); 270 271 /** 272 * Names of automatic create timestamp columns 273 * 274 * When a row containing one of these columns is created and 275 * {@link $auto_timestamps} is true, store the current date and 276 * time in the timestamp columns. 277 * @see $auto_timestamps 278 * @see $auto_update_timestamps 279 * @var string[] 280 */ 281 protected $auto_create_timestamps = array("created_at","created_on"); 282 283 /** 284 * Date format for use with auto timestamping 285 * 286 * The format for this should be compatiable with the php date() function. 287 * http://www.php.net/date 288 * @var string 289 */ 290 protected $date_format = "Y-m-d"; 291 292 /** 293 * Time format for use with auto timestamping 294 * 295 * The format for this should be compatiable with the php date() function. 296 * http://www.php.net/date 297 * @var string 298 */ 299 protected $time_format = "H:i:s"; 300 301 /** 302 * Whether to keep date/datetime fields NULL if not set 303 * 304 * true => If date field is not set it try to preserve NULL 305 * false => Don't try to preserve NULL if field is already NULL 306 * @var boolean 307 */ 308 protected $preserve_null_dates = true; 309 310 /** 311 * SQL aggregate functions that may be applied to the associated 312 * table. 313 * 314 * SQL defines aggregate functions AVG, COUNT, MAX, MIN and SUM. 315 * Not all of these functions are implemented by all DBMS's 316 * @var string[] 317 */ 318 protected $aggregations = array("count","sum","avg","max","min"); 319 320 /** 321 * Primary key of the associated table 322 * 323 * Array element(s) name the primary key column(s), as used to 324 * specify the row to be updated or deleted. To be a primary key 325 * a column must be listed both here and in {@link 326 * $content_columns}. <b>NOTE:</b>This 327 * field is maintained by hand. It is not derived from the table 328 * description read from the database. 329 * @var string[] 330 * @see $content_columns 331 * @see find() 332 * @see find_all() 333 * @see find_first() 334 */ 335 public $primary_keys = array("id"); 336 337 /** 338 * Default for how many rows to return from {@link find_all()} 339 * @var integer 340 */ 341 public $rows_per_page_default = 20; 342 343 /** 344 * @todo Document this variable 345 */ 346 public $display = 10; # Pagination how many numbers in the list << < 1 2 3 4 > >> 347 348 /** 349 * @todo Document this variable 350 */ 351 public $pagination_count = 0; 352 353 /** 354 * Description of non-fatal errors found 355 * 356 * For every non-fatal error found, an element describing the 357 * error is added to $errors. Initialized to an empty array in 358 * {@link valid()} before validating object. When an error 359 * message is associated with a particular attribute, the message 360 * should be stored with the attribute name as its key. If the 361 * message is independent of attributes, store it with a numeric 362 * key beginning with 0. 363 * 364 * @var string[] 365 * @see add_error() 366 * @see get_errors() 367 */ 368 public $errors = array(); 369 370 /** 371 * Whether to automatically update timestamps in certain columns 372 * 373 * @see $auto_create_timestamps 374 * @see $auto_update_timestamps 375 * @var boolean 376 */ 377 public $auto_timestamps = true; 378 379 /** 380 * @todo Document this variable 381 */ 382 public $auto_save_habtm = true; # auto insert / update $has_and_belongs_to_many tables 383 384 /** 385 * @todo Document this variable 386 */ 387 public $auto_delete_habtm = true; # auto delete $has_and_belongs_to_many associations 388 389 /** 390 * Transactions (only use if your db supports it) 391 */ 392 private static $begin_executed = false; # this is for transactions only to let query() know that a 'BEGIN' has been executed 393 394 /** 395 * Transactions (only use if your db supports it) 396 */ 397 public static $use_transactions = false; # this will issue a rollback command if any sql fails 398 399 /** 400 * Keep a log of queries executed if in development env 401 */ 402 public static $query_log = array(); 403 404 /** 405 * Construct an ActiveRecord object 406 * 407 * <ol> 408 * <li>Establish a connection to the database</li> 409 * <li>Find the name of the table associated with this object</li> 410 * <li>Read description of this table from the database</li> 411 * <li>Optionally apply update information to column attributes</li> 412 * </ol> 413 * @param string[] $attributes Updates to column attributes 414 * @uses establish_connection() 415 * @uses set_content_columns() 416 * @uses $table_name 417 * @uses set_table_name_using_class_name() 418 * @uses update_attributes() 419 */ 420 function __construct($attributes = null) { 421 # Open the database connection 422 $this->establish_connection(); 423 424 # Set $table_name 425 if($this->table_name == null) { 426 $this->set_table_name_using_class_name(); 427 } 428 429 # Set column info 430 if($this->table_name) { 431 $this->set_content_columns($this->table_name); 432 } 433 434 # If $attributes array is passed in update the class with its contents 435 $this->update_attributes($attributes); 436 } 437 438 /** 439 * Override get() if they do $model->some_association->field_name 440 * dynamically load the requested contents from the database. 441 * @todo Document this API 442 * @uses $belongs_to 443 * @uses get_association_type() 444 * @uses $has_and_belongs_to_many 445 * @uses $has_many 446 * @uses $has_one 447 * @uses find_all_has_many() 448 * @uses find_all_habtm() 449 * @uses find_one_belongs_to() 450 * @uses find_one_has_one() 451 */ 452 function __get($key) { 453 if($association_type = $this->get_association_type($key)) { 454 //error_log("association_type:$association_type"); 455 switch($association_type) { 456 case "has_many": 457 $parameters = is_array($this->has_many) ? $this->has_many[$key] : null; 458 $this->$key = $this->find_all_has_many($key, $parameters); 459 break; 460 case "has_one": 461 $parameters = is_array($this->has_one) ? $this->has_one[$key] : null; 462 $this->$key = $this->find_one_has_one($key, $parameters); 463 break; 464 case "belongs_to": 465 $parameters = is_array($this->belongs_to) ? $this->belongs_to[$key] : null; 466 $this->$key = $this->find_one_belongs_to($key, $parameters); 467 break; 468 case "has_and_belongs_to_many": 469 $parameters = is_array($this->has_and_belongs_to_many) ? $this->has_and_belongs_to_many[$key] : null; 470 $this->$key = $this->find_all_habtm($key, $parameters); 471 break; 472 } 473 } elseif($this->is_composite($key)) { 474 $composite_object = $this->get_composite_object($key); 475 if(is_object($composite_object)) { 476 $this->$key = $composite_object; 477 } 478 } 479 //echo "<pre>getting: $key = ".$this->$key."<br></pre>"; 480 return $this->$key; 481 } 482 483 /** 484 * Store column value or description of the table format 485 * 486 * If called with key 'table_name', $value is stored as the 487 * description of the table format in $content_columns. 488 * Any other key causes an object variable with the same name to 489 * be created and stored into. If the value of $key matches the 490 * name of a column in content_columns, the corresponding object 491 * variable becomes the content of the column in this row. 492 * @uses $auto_save_associations 493 * @uses get_association_type() 494 * @uses set_content_columns() 495 */ 496 function __set($key, $value) { 497 //echo "setting: $key = $value<br>"; 498 if($key == "table_name") { 499 $this->set_content_columns($value); 500 # this elseif checks if first its an object if its parent is ActiveRecord 501 } elseif(is_object($value) && get_parent_class($value) == __CLASS__ && $this->auto_save_associations) { 502 if($association_type = $this->get_association_type($key)) { 503 $this->save_associations[$association_type][] = $value; 504 if($association_type == "belongs_to") { 505 $primary_key = $value->primary_keys[0]; 506 $foreign_key = Inflector::singularize($value->table_name)."_".$primary_key; 507 $this->$foreign_key = $value->$primary_key; 508 } 509 } 510 # this elseif checks if its an array of objects and if its parent is ActiveRecord 511 } elseif(is_array($value) && $this->auto_save_associations) { 512 if($association_type = $this->get_association_type($key)) { 513 $this->save_associations[$association_type][] = $value; 514 } 515 } 516 517 // Assignment to something else, do it 518 $this->$key = $value; 519 } 520 521 /** 522 * Override call() to dynamically call the database associations 523 * @todo Document this API 524 * @uses $aggregations 525 * @uses aggregate_all() 526 * @uses get_association_type() 527 * @uses $belongs_to 528 * @uses $has_one 529 * @uses $has_and_belongs_to_many 530 * @uses $has_many 531 * @uses find_all_by() 532 * @uses find_by() 533 */ 534 function __call($method_name, $parameters) { 535 if(method_exists($this, $method_name)) { 536 # If the method exists, just call it 537 $result = call_user_func_array(array($this, $method_name), $parameters); 538 } else { 539 # ... otherwise, check to see if the method call is one of our 540 # special Trax methods ... 541 # ... first check for method names that match any of our explicitly 542 # declared associations for this model ( e.g. $this->has_many = array("movies" => null) ) ... 543 if(is_array($parameters[0])) { 544 $parameters = $parameters[0]; 545 } 546 $association_type = $this->get_association_type($method_name); 547 switch($association_type) { 548 case "has_many": 549 $result = $this->find_all_has_many($method_name, $parameters); 550 break; 551 case "has_one": 552 $result = $this->find_one_has_one($method_name, $parameters); 553 break; 554 case "belongs_to": 555 $result = $this->find_one_belongs_to($method_name, $parameters); 556 break; 557 case "has_and_belongs_to_many": 558 $result = $this->find_all_habtm($method_name, $parameters); 559 break; 560 } 561 562 # check for the [count,sum,avg,etc...]_all magic functions 563 if(substr($method_name, -4) == "_all" && in_array(substr($method_name, 0, -4), $this->aggregations)) { 564 //echo "calling method: $method_name<br>"; 565 $result = $this->aggregate_all($method_name, $parameters); 566 } 567 # check for the find_all_by_* magic functions 568 elseif(strlen($method_name) > 11 && substr($method_name, 0, 11) == "find_all_by") { 569 //echo "calling method: $method_name<br>"; 570 $result = $this->find_by($method_name, $parameters, "all"); 571 } 572 # check for the find_by_* magic functions 573 elseif(strlen($method_name) > 7 && substr($method_name, 0, 7) == "find_by") { 574 //echo "calling method: $method_name<br>"; 575 $result = $this->find_by($method_name, $parameters); 576 } 577 # check for find_or_create_by_* magic functions 578 elseif(strlen($method_name) > 17 && substr($method_name, 0, 17) == "find_or_create_by") { 579 $result = $this->find_by($method_name, $parameters, "find_or_create"); 580 } 581 } 582 return $result; 583 } 584 585 /** 586 * Find all records using a "has_and_belongs_to_many" relationship 587 * (many-to-many with a join table in between). Note that you can also 588 * specify an optional "paging limit" by setting the corresponding "limit" 589 * instance variable. For example, if you want to return 10 movies from the 590 * 5th movie on, you could set $this->movies_limit = "10, 5" 591 * 592 * Parameters: $this_table_name: The name of the database table that has the 593 * one row you are interested in. E.g. genres 594 * $other_table_name: The name of the database table that has the 595 * many rows you are interested in. E.g. movies 596 * Returns: An array of ActiveRecord objects. (e.g. Movie objects) 597 * @todo Document this API 598 */ 599 private function find_all_habtm($other_table_name, $parameters = null) { 600 $additional_conditions = null; 601 # Use any passed-in parameters 602 if(!is_null($parameters)) { 603 if(@array_key_exists("conditions", $parameters)) { 604 $additional_conditions = " AND (".$parameters['conditions'].")"; 605 } elseif($parameters[0] != "") { 606 $additional_conditions = " AND (".$parameters[0].")"; 607 } 608 if(@array_key_exists("order", $parameters)) { 609 $order = $parameters['order']; 610 } elseif($parameters[1] != "") { 611 $order = $parameters[1]; 612 } 613 if(@array_key_exists("limit", $parameters)) { 614 $limit = $parameters['limit']; 615 } elseif($parameters[2] != "") { 616 $limit = $parameters[2]; 617 } 618 if(@array_key_exists("class_name", $parameters)) { 619 $other_object_name = $parameters['class_name']; 620 } 621 if(@array_key_exists("join_table", $parameters)) { 622 $join_table = $parameters['join_table']; 623 } 624 if(@array_key_exists("foreign_key", $parameters)) { 625 $this_foreign_key = $parameters['foreign_key']; 626 } 627 if(@array_key_exists("association_foreign_key", $parameters)) { 628 $other_foreign_key = $parameters['association_foreign_key']; 629 } 630 if(@array_key_exists("finder_sql", $parameters)) { 631 $finder_sql = $parameters['finder_sql']; 632 } 633 } 634 635 if(!is_null($other_object_name)) { 636 $other_class_name = Inflector::camelize($other_object_name); 637 } else { 638 $other_class_name = Inflector::classify($other_table_name); 639 } 640 641 # Instantiate an object to access find_all 642 $other_class_object = new $other_class_name(); 643 644 # If finder_sql is specified just use it instead of determining the joins/sql 645 if(!is_null($finder_sql)) { 646 $conditions = $finder_sql; 647 $order = null; 648 $limit = null; 649 $joins = null; 650 } else { 651 # Prepare the join table name primary keys (fields) to do the join on 652 if(is_null($join_table)) { 653 $join_table = $this->get_join_table_name($this->table_name, $other_table_name); 654 } 655 656 # Primary keys 657 $this_primary_key = $this->primary_keys[0]; 658 $other_primary_key = $other_class_object->primary_keys[0]; 659 660 # Foreign keys 661 if(is_null($this_foreign_key)) { 662 $this_foreign_key = Inflector::singularize($this->table_name)."_".$this_primary_key; 663 } 664 if(is_null($other_foreign_key)) { 665 $other_foreign_key = Inflector::singularize($other_table_name)."_".$other_primary_key; 666 } 667 668 # Primary key value 669 if($this->attribute_is_string($this_primary_key)) { 670 $this_primary_key_value = "'".$this->$this_primary_key."'"; 671 } elseif(is_numeric($this->$this_primary_key)) { 672 $this_primary_key_value = $this->$this_primary_key; 673 } else { 674 $this_primary_key_value = 0; 675 } 676 677 # Set up the SQL segments 678 $conditions = "{$join_table}.{$this_foreign_key} = {$this_primary_key_value}".$additional_conditions; 679 $joins = "LEFT JOIN {$join_table} ON {$other_table_name}.{$other_primary_key} = {$join_table}.{$other_foreign_key}"; 680 } 681 682 # Get the list of other_class_name objects 683 return $other_class_object->find_all($conditions, $order, $limit, $joins); 684 } 685 686 /** 687 * Find all records using a "has_many" relationship (one-to-many) 688 * 689 * Parameters: $other_table_name: The name of the other table that contains 690 * many rows relating to this object's id. 691 * Returns: An array of ActiveRecord objects. (e.g. Contact objects) 692 * @todo Document this API 693 */ 694 private function find_all_has_many($other_table_name, $parameters = null) { 695 $additional_conditions = null; 696 # Use any passed-in parameters 697 if(is_array($parameters)) { 698 if(@array_key_exists("conditions", $parameters)) { 699 $additional_conditions = " AND (".$parameters['conditions'].")"; 700 } elseif($parameters[0] != "") { 701 $additional_conditions = " AND (".$parameters[0].")"; 702 } 703 if(@array_key_exists("order", $parameters)) { 704 $order = $parameters['order']; 705 } elseif($parameters[1] != "") { 706 $order = $parameters[1]; 707 } 708 if(@array_key_exists("limit", $parameters)) { 709 $limit = $parameters['limit']; 710 } elseif($parameters[2] != "") { 711 $limit = $parameters[2]; 712 } 713 if(@array_key_exists("foreign_key", $parameters)) { 714 $foreign_key = $parameters['foreign_key']; 715 } 716 if(@array_key_exists("class_name", $parameters)) { 717 $other_object_name = $parameters['class_name']; 718 } 719 if(@array_key_exists("finder_sql", $parameters)) { 720 $finder_sql = $parameters['finder_sql']; 721 } 722 } 723 724 if(!is_null($other_object_name)) { 725 $other_class_name = Inflector::camelize($other_object_name); 726 } else { 727 $other_class_name = Inflector::classify($other_table_name); 728 } 729 730 # Instantiate an object to access find_all 731 $other_class_object = new $other_class_name(); 732 733 # If finder_sql is specified just use it instead of determining the association 734 if(!is_null($finder_sql)) { 735 $conditions = $finder_sql; 736 $order = null; 737 $limit = null; 738 $joins = null; 739 } else { 740 # This class primary key 741 $this_primary_key = $this->primary_keys[0]; 742 743 if(!$foreign_key) { 744 # this should end up being like user_id or account_id but if you specified 745 # a primaray key other than 'id' it will be like user_field 746 $foreign_key = Inflector::singularize($this->table_name)."_".$this_primary_key; 747 } 748 749 $foreign_key_value = $this->$this_primary_key; 750 if($other_class_object->attribute_is_string($foreign_key)) { 751 $conditions = "{$foreign_key} = '{$foreign_key_value}'"; 752 } elseif(is_numeric($foreign_key_value)) { 753 $conditions = "{$foreign_key} = {$foreign_key_value}"; 754 } else { 755 $conditions = "{$foreign_key} = 0"; 756 } 757 $conditions .= $additional_conditions; 758 } 759 760 # Get the list of other_class_name objects 761 return $other_class_object->find_all($conditions, $order, $limit, $joins); 762 } 763 764 /** 765 * Find all records using a "has_one" relationship (one-to-one) 766 * (the foreign key being in the other table) 767 * Parameters: $other_table_name: The name of the other table that contains 768 * many rows relating to this object's id. 769 * Returns: An array of ActiveRecord objects. (e.g. Contact objects) 770 * @todo Document this API 771 */ 772 private function find_one_has_one($other_object_name, $parameters = null) { 773 $additional_conditions = null; 774 # Use any passed-in parameters 775 if(is_array($parameters)) { 776 //echo "<pre>";print_r($parameters); 777 if(@array_key_exists("conditions", $parameters)) { 778 $additional_conditions = " AND (".$parameters['conditions'].")"; 779 } elseif($parameters[0] != "") { 780 $additional_conditions = " AND (".$parameters[0].")"; 781 } 782 if(@array_key_exists("order", $parameters)) { 783 $order = $parameters['order']; 784 } elseif($parameters[1] != "") { 785 $order = $parameters[1]; 786 } 787 if(@array_key_exists("foreign_key", $parameters)) { 788 $foreign_key = $parameters['foreign_key']; 789 } 790 if(@array_key_exists("class_name", $parameters)) { 791 $other_object_name = $parameters['class_name']; 792 } 793 } 794 795 $other_class_name = Inflector::camelize($other_object_name); 796 797 # Instantiate an object to access find_all 798 $other_class_object = new $other_class_name(); 799 800 # This class primary key 801 $this_primary_key = $this->primary_keys[0]; 802 803 if(!$foreign_key){ 804 $foreign_key = Inflector::singularize($this->table_name)."_".$this_primary_key; 805 } 806 807 $foreign_key_value = $this->$this_primary_key; 808 if($other_class_object->attribute_is_string($foreign_key)) { 809 $conditions = "{$foreign_key} = '{$foreign_key_value}'"; 810 } elseif(is_numeric($foreign_key_value)) { 811 $conditions = "{$foreign_key} = {$foreign_key_value}"; 812 } else { 813 $conditions = "{$foreign_key} = 0"; 814 } 815 816 $conditions .= $additional_conditions; 817 818 # Get the list of other_class_name objects 819 $result = $other_class_object->find_first($conditions, $order); 820 821 # There should only be one result, an object, if so return it 822 return (is_object($result) ? $result : null); 823 } 824 825 /** 826 * Find all records using a "belongs_to" relationship (one-to-one) 827 * (the foreign key being in the table itself) 828 * Parameters: $other_object_name: The singularized version of a table name. 829 * E.g. If the Contact class belongs_to the 830 * Customer class, then $other_object_name 831 * will be "customer". 832 * @todo Document this API 833 */ 834 private function find_one_belongs_to($other_object_name, $parameters = null) { 835 836 $additional_conditions = null; 837 # Use any passed-in parameters 838 if(is_array($parameters)) { 839 //echo "<pre>";print_r($parameters); 840 if(@array_key_exists("conditions", $parameters)) { 841 $additional_conditions = " AND (".$parameters['conditions'].")"; 842 } elseif($parameters[0] != "") { 843 $additional_conditions = " AND (".$parameters[0].")"; 844 } 845 if(@array_key_exists("order", $parameters)) { 846 $order = $parameters['order']; 847 } elseif($parameters[1] != "") { 848 $order = $parameters[1]; 849 } 850 if(@array_key_exists("foreign_key", $parameters)) { 851 $foreign_key = $parameters['foreign_key']; 852 } 853 if(@array_key_exists("class_name", $parameters)) { 854 $other_object_name = $parameters['class_name']; 855 } 856 } 857 858 $other_class_name = Inflector::camelize($other_object_name); 859 860 # Instantiate an object to access find_all 861 $other_class_object = new $other_class_name(); 862 863 # This class primary key 864 $other_primary_key = $other_class_object->primary_keys[0]; 865 866 if(!$foreign_key) { 867 $foreign_key = $other_object_name."_".$other_primary_key; 868 } 869 870 $other_primary_key_value = $this->$foreign_key; 871 if($other_class_object->attribute_is_string($other_primary_key)) { 872 $conditions = "{$other_primary_key} = '{$other_primary_key_value}'"; 873 } elseif(is_numeric($other_primary_key_value)) { 874 $conditions = "{$other_primary_key} = {$other_primary_key_value}"; 875 } else { 876 $conditions = "{$other_primary_key} = 0"; 877 } 878 $conditions .= $additional_conditions; 879 880 # Get the list of other_class_name objects 881 $result = $other_class_object->find_first($conditions, $order); 882 883 # There should only be one result, an object, if so return it 884 return (is_object($result) ? $result : null); 885 } 886 887 /** 888 * Implement *_all() functions (SQL aggregate functions) 889 * 890 * Apply one of the SQL aggregate functions to a column of the 891 * table associated with this object. The SQL aggregate 892 * functions are AVG, COUNT, MAX, MIN and SUM. Not all DBMS's 893 * implement all of these functions. 894 * @param string $agrregrate_type SQL aggregate function to 895 * apply, suffixed '_all'. The aggregate function is one of 896 * the strings in {@link $aggregations}. 897 * @param string[] $parameters Conditions to apply to the 898 * aggregate function. If present, must be an array of three 899 * strings:<ol> 900 * <li>$parameters[0]: If present, expression to apply 901 * the aggregate function to. Otherwise, '*' will be used. 902 * <b>NOTE:</b>SQL uses '*' only for the COUNT() function, 903 * where it means "including rows with NULL in this column".</li> 904 * <li>$parameters[1]: argument to WHERE clause</li> 905 * <li>$parameters[2]: joins??? @todo Document this parameter</li> 906 * </ol> 907 * @throws {@link ActiveRecordError} 908 * @uses query() 909 * @uses is_error() 910 */ 911 private function aggregate_all($aggregate_type, $parameters = null) { 912 $aggregate_type = strtoupper(substr($aggregate_type, 0, -4)); 913 ($parameters[0]) ? $field = $parameters[0] : $field = "*"; 914 $sql = "SELECT $aggregate_type($field) AS agg_result FROM $this->table_name "; 915 916 # Use any passed-in parameters 917 if(is_array($parameters[1])) { 918 extract($parameters[1]); 919 } elseif(!is_null($parameters)) { 920 $conditions = $parameters[1]; 921 $order = $parameters[2]; 922 $joins = $parameters[3]; 923 } 924 925 if(!empty($joins)) $sql .= " $joins "; 926 if(!empty($conditions)) $sql .= " WHERE $conditions "; 927 if(!empty($order)) $sql .= " ORDER BY $order "; 928 929 # echo "$aggregate_type sql:$sql<br>"; 930 if($this->is_error($rs = $this->query($sql))) { 931 $this->raise($rs->getMessage()); 932 } else { 933 $row = $rs->fetchRow(); 934 if($row["agg_result"]) { 935 return $row["agg_result"]; 936 } 937 } 938 return 0; 939 } 940 941 /** 942 * Returns a the name of the join table that would be used for the two 943 * tables. The join table name is decided from the alphabetical order 944 * of the two tables. e.g. "genres_movies" because "g" comes before "m" 945 * 946 * Parameters: $first_table, $second_table: the names of two database tables, 947 * e.g. "movies" and "genres" 948 * @todo Document this API 949 */ 950 public function get_join_table_name($first_table, $second_table) { 951 $tables = array($first_table, $second_table); 952 @sort($tables); 953 return @implode("_", $tables); 954 } 955 956 /** 957 * Test whether this object represents a new record 958 * @uses $new_record 959 * @return boolean Whether this object represents a new record 960 */ 961 function is_new_record() { 962 return $this->new_record; 963 } 964 965 /** 966 * get the attributes for a specific column. 967 * @uses $content_columns 968 * @todo Document this API 969 */ 970 function column_for_attribute($attribute) { 971 if(is_array($this->content_columns)) { 972 foreach($this->content_columns as $column) { 973 if($column['name'] == $attribute) { 974 return $column; 975 } 976 } 977 } 978 return null; 979 } 980 981 /** 982 * get the columns data type. 983 * @uses column_for_attribute() 984 * @todo Document this API 985 */ 986 function column_type($attribute) { 987 $column = $this->column_for_attribute($attribute); 988 if(isset($column['type'])) { 989 return $column['type']; 990 } 991 return null; 992 } 993 994 /** 995 * Check whether a column exists in the associated table 996 * 997 * When called, {@link $content_columns} lists the columns in 998 * the table described by this object. 999 * @param string Name of the column 1000 * @return boolean true=>the column exists; false=>it doesn't 1001 * @uses content_columns 1002 */ 1003 function column_attribute_exists($attribute) { 1004 if(is_array($this->content_columns)) { 1005 foreach($this->content_columns as $column) { 1006 if($column['name'] == $attribute) { 1007 return true; 1008 } 1009 } 1010 } 1011 return false; 1012 } 1013 1014 /** 1015 * Get contents of one column of record selected by id and table 1016 * 1017 * When called, {@link $id} identifies one record in the table 1018 * identified by {@link $table}. Fetch from the database the 1019 * contents of column $column of this record. 1020 * @param string Name of column to retrieve 1021 * @uses $db 1022 * @uses column_attribute_exists() 1023 * @throws {@link ActiveRecordError} 1024 * @uses is_error() 1025 */ 1026 function send($column) { 1027 if($this->column_attribute_exists($column) && ($conditions = $this->get_primary_key_conditions())) { 1028 # Run the query to grab a specific columns value. 1029 $sql = "SELECT {$column} FROM {$this->table_name} WHERE {$conditions}"; 1030 $this->log_query($sql); 1031 $result = self::$db->queryOne($sql); 1032 if($this->is_error($result)) { 1033 $this->raise($result->getMessage()); 1034 } 1035 } 1036 return $result; 1037 } 1038 1039 /** 1040 * Only used if you want to do transactions and your db supports transactions 1041 * 1042 * @uses $db 1043 * @todo Document this API 1044 */ 1045 function begin() { 1046 self::$db->query("BEGIN"); 1047 $this->begin_executed = true; 1048 } 1049 1050 /** 1051 * Only used if you want to do transactions and your db supports transactions 1052 * 1053 * @uses $db 1054 * @todo Document this API 1055 */ 1056 function commit() { 1057 self::$db->query("COMMIT"); 1058 $this->begin_executed = false; 1059 } 1060 1061 /** 1062 * Only used if you want to do transactions and your db supports transactions 1063 * 1064 * @uses $db 1065 * @todo Document this API 1066 */ 1067 function rollback() { 1068 self::$db->query("ROLLBACK"); 1069 } 1070 1071 /** 1072 * Perform an SQL query and return the results 1073 * 1074 * @param string $sql SQL for the query command 1075 * @return $mdb2->query {@link http://pear.php.net/manual/en/package.database.mdb2.intro-query.php} 1076 * Result set from query 1077 * @uses $db 1078 * @uses is_error() 1079 * @uses log_query() 1080 * @throws {@link ActiveRecordError} 1081 */ 1082 function query($sql) { 1083 # Run the query 1084 $this->log_query($sql); 1085 $rs =& self::$db->query($sql); 1086 if ($this->is_error($rs)) { 1087 if(self::$use_transactions && self::$begin_executed) { 1088 $this->rollback(); 1089 } 1090 $this->raise($rs->getMessage()); 1091 } 1092 return $rs; 1093 } 1094 1095 /** 1096 * Implement find_by_*() and =_* methods 1097 * 1098 * Converts a method name beginning 'find_by_' or 'find_all_by_' 1099 * into a query for rows matching the rest of the method name and 1100 * the arguments to the function. The part of the method name 1101 * after '_by' is parsed for columns and logical relationships 1102 * (AND and OR) to match. For example, the call 1103 * find_by_fname('Ben') 1104 * is converted to 1105 * SELECT * ... WHERE fname='Ben' 1106 * and the call 1107 * find_by_fname_and_lname('Ben','Dover') 1108 * is converted to 1109 * SELECT * ... WHERE fname='Ben' AND lname='Dover' 1110 * 1111 * @uses find_all() 1112 * @uses find_first() 1113 */ 1114 private function find_by($method_name, $parameters, $find_type = null) { 1115 if($find_type == "find_or_create") { 1116 $explode_len = 18; 1117 } elseif($find_type == "all") { 1118 $explode_len = 12; 1119 } else { 1120 $explode_len = 8; 1121 } 1122 $method_name = substr(strtolower($method_name), $explode_len); 1123 $method_parts = explode("|", str_replace("_and_", "|AND|", $method_name)); 1124 if(count($method_parts)) { 1125 $options = array(); 1126 $create_fields = array(); 1127 $param_index = 0; 1128 foreach($method_parts as $part) { 1129 if($part == "AND") { 1130 $conditions .= " AND "; 1131 $param_index++; 1132 } else { 1133 $value = $this->attribute_is_string($part) ? 1134 "'".$parameters[$param_index]."'" : 1135 $parameters[$param_index]; 1136 $create_fields[$part] = $parameters[$param_index]; 1137 $conditions .= "{$part} = {$value}"; 1138 } 1139 } 1140 # If last param exists and is a string set it as the ORDER BY clause 1141 # or if the last param is an array set it as the $options 1142 if($last_param = $parameters[++$param_index]) { 1143 if(is_string($last_param)) { 1144 $options['order'] = $last_param; 1145 } elseif(is_array($last_param)) { 1146 $options = $last_param; 1147 } 1148 } 1149 # Set the conditions 1150 if($options['conditions'] && $conditions) { 1151 $options['conditions'] = "(".$options['conditions'].") AND (".$conditions.")"; 1152 } else { 1153 $options['conditions'] = $conditions; 1154 } 1155 1156 # Now do the actual find with condtions from above 1157 if($find_type == "find_or_create") { 1158 # see if we can find a record with specified parameters 1159 $object = $this->find($options); 1160 if(is_object($object)) { 1161 # we found a record with the specified parameters so return it 1162 return $object; 1163 } elseif(count($create_fields)) { 1164 # can't find a record with specified parameters so create a new record 1165 # and return new object 1166 foreach($create_fields as $field => $value) { 1167 $this->$field = $value; 1168 } 1169 $this->save(); 1170 return $this->find($options); 1171 } 1172 } elseif($find_type == "all") { 1173 return $this->find_all($options); 1174 } else { 1175 return $this->find($options); 1176 } 1177 } 1178 } 1179 1180 /** 1181 * Return rows selected by $conditions 1182 * 1183 * If no rows match, an empty array is returned. 1184 * @param string SQL to use in the query. If 1185 * $conditions contains "SELECT", then $order, $limit and 1186 * $joins are ignored and the query is completely specified by 1187 * $conditions. If $conditions is omitted or does not contain 1188 * "SELECT", "SELECT * FROM" will be used. If $conditions is 1189 * specified and does not contain "SELECT", the query will 1190 * include "WHERE $conditions". If $conditions is null, the 1191 * entire table is returned. 1192 * @param string Argument to "ORDER BY" in query. 1193 * If specified, the query will include 1194 * "ORDER BY $order". If omitted, no ordering will be 1195 * applied. 1196 * @param integer[] Page, rows per page??? 1197 * @param string ??? 1198 * @todo Document the $limit and $joins parameters 1199 * @uses $rows_per_page_default 1200 * @uses $rows_per_page 1201 * @uses $offset 1202 * @uses $page 1203 * @uses is_error() 1204 * @uses $new_record 1205 * @uses query() 1206 * @return object[] Array of objects of the same class as this 1207 * object, one object for each row returned by the query. 1208 * If the column 'id' was in the results, it is used as the key 1209 * for that object in the array. 1210 * @throws {@link ActiveRecordError} 1211 */ 1212 function find_all($conditions = null, $order = null, $limit = null, $joins = null) { 1213 //error_log("find_all(".(is_null($conditions)?'null':$conditions) 1214 // .', ' . (is_null($order)?'null':$order) 1215 // .', ' . (is_null($limit)?'null':var_export($limit,true)) 1216 // .', ' . (is_null($joins)?'null':$joins).')'); 1217 1218 $offset = null; 1219 $per_page = null; 1220 $select = null; 1221 1222 # this is if they passed in an associative array to emulate 1223 # named parameters. 1224 if(is_array($conditions)) { 1225 if(@array_key_exists("per_page", $conditions) && !is_numeric($conditions['per_page'])) { 1226 extract($conditions); 1227 $per_page = 0; 1228 } else { 1229 extract($conditions); 1230 } 1231 # If conditions wasn't in the array set it to null 1232 if(is_array($conditions)) { 1233 $conditions = null; 1234 } 1235 } 1236 1237 # Test source of SQL for query 1238 if(stristr($conditions, "SELECT")) { 1239 # SQL completely specified in argument so use it as is 1240 $sql = $conditions; 1241 } else { 1242 1243 # If select fields not specified just do a SELECT * 1244 if(is_null($select)) { 1245 $select = "*"; 1246 } 1247 1248 # SQL will be built from specifications in argument 1249 $sql = "SELECT {$select} FROM {$this->table_name} "; 1250 1251 # If join specified, include it 1252 if(!is_null($joins)) { 1253 $sql .= " $joins "; 1254 } 1255 1256 # If conditions specified, include them 1257 if(!is_null($conditions)) { 1258 $sql .= "WHERE $conditions "; 1259 } 1260 1261 # If ordering specified, include it 1262 if(!is_null($order)) { 1263 $sql .= "ORDER BY $order "; 1264 } 1265 1266 # Is output to be generated in pages? 1267 if(is_numeric($limit) || is_numeric($offset) || is_numeric($per_page)) { 1268 1269 if(is_numeric($limit)) { 1270 $this->rows_per_page = $limit; 1271 } 1272 if(is_numeric($per_page)) { 1273 $this->rows_per_page = $per_page; 1274 } 1275 # Default for rows_per_page: 1276 if ($this->rows_per_page <= 0) { 1277 $this->rows_per_page = $this->rows_per_page_default; 1278 } 1279 1280 $this->page = $_REQUEST['page']; 1281 if($this->page <= 0) { 1282 $this->page = 1; 1283 } 1284 1285 # Set the LIMIT string segment for the SQL 1286 if(!$offset) { 1287 $offset = ($this->page - 1) * $this->rows_per_page; 1288 } 1289 1290 $sql .= "LIMIT {$this->rows_per_page} OFFSET {$offset}"; 1291 # $sql .= "LIMIT $offset, $this->rows_per_page"; 1292 1293 # Set number of total pages in result set 1294 if($count = $this->count_all($this->primary_keys[0], $conditions, $joins)) { 1295 $this->pagination_count = $count; 1296 $this->pages = ( 1297 ($count % $this->rows_per_page) == 0) 1298 ? $count / $this->rows_per_page 1299 : floor($count / $this->rows_per_page) + 1; 1300 } 1301 } 1302 } 1303 1304 # echo "ActiveRecord::find_all() - sql: $sql\n<br>"; 1305 # echo "query: $sql\n"; 1306 # error_log("ActiveRecord::find_all -> $sql"); 1307 if($this->is_error($rs = $this->query($sql))) { 1308 $this->raise($rs->getMessage()); 1309 } 1310 1311 $objects = array(); 1312 while($row = $rs->fetchRow()) { 1313 $class_name = $this->get_class_name(); 1314 $object = new $class_name(); 1315 $object->new_record = false; 1316 $objects_key = null; 1317 foreach($row as $field => $value) { 1318 $object->$field = $value; 1319 if($field == $this->index_on) { 1320 $objects_key = $value; 1321 } 1322 } 1323 $objects[$objects_key] = $object; 1324 unset($object); 1325 } 1326 return $objects; 1327 } 1328 1329 /** 1330 * Find row(s) with specified value(s) 1331 * 1332 * Find all the rows in the table which match the argument $id. 1333 * Return zero or more objects of the same class as this 1334 * class representing the rows that matched the argument. 1335 * @param mixed[] $id If $id is an array then a query will be 1336 * generated selecting all of the array values in column "id". 1337 * If $id is a string containing "=" then the string value of 1338 * $id will be inserted in a WHERE clause in the query. If $id 1339 * is a scalar not containing "=" then a query will be generated 1340 * selecting the first row WHERE id = '$id'. 1341 * <b>NOTE</b> The column name "id" is used regardless of the 1342 * value of {@link $primary_keys}. Therefore if you need to 1343 * select based on some column other than "id", you must pass a 1344 * string argument ready to insert in the SQL SELECT. 1345 * @param string $order Argument to "ORDER BY" in query. 1346 * If specified, the query will include "ORDER BY 1347 * $order". If omitted, no ordering will be applied. 1348 * @param integer[] $limit Page, rows per page??? 1349 * @param string $joins ??? 1350 * @todo Document the $limit and $joins parameters 1351 * @uses find_all() 1352 * @uses find_first() 1353 * @return mixed Results of query. If $id was a scalar then the 1354 * result is an object of the same class as this class and 1355 * matching $id conditions, or if no row matched the result is 1356 * null. 1357 * 1358 * If $id was an array then the result is an array containing 1359 * objects of the same class as this class and matching the 1360 * conditions set by $id. If no rows matched, the array is 1361 * empty. 1362 * @throws {@link ActiveRecordError} 1363 */ 1364 function find($id, $order = null, $limit = null, $joins = null) { 1365 $find_all = false; 1366 if(is_array($id)) { 1367 if($id[0]) { 1368 # passed in array of numbers array(1,2,4,23) 1369 $primary_key = $this->primary_keys[0]; 1370 $primary_key_values = $this->attribute_is_string($primary_key) ? 1371 "'".implode("','", $id)."'" : 1372 implode(",", $id); 1373 $options['conditions'] = "{$primary_key} IN({$primary_key_values})"; 1374 $find_all = true; 1375 } else { 1376 # passed in an options array 1377 $options = $id; 1378 } 1379 } elseif(stristr($id, "=")) { 1380 # has an "=" so must be a WHERE clause 1381 $options['conditions'] = $id; 1382 } else { 1383 # find an single record with id = $id 1384 $primary_key = $this->primary_keys[0]; 1385 $primary_key_value = $this->attribute_is_string($primary_key) ? "'".$id."'" : $id ; 1386 $options['conditions'] = "{$primary_key} = {$primary_key_value}"; 1387 } 1388 if(!is_null($order)) $options['order'] = $order; 1389 if(!is_null($limit)) $options['limit'] = $limit; 1390 if(!is_null($joins)) $options['joins'] = $joins; 1391 1392 1393 if($find_all) { 1394 return $this->find_all($options); 1395 } else { 1396 return $this->find_first($options); 1397 } 1398 } 1399 1400 /** 1401 * Return first row selected by $conditions 1402 * 1403 * If no rows match, null is returned. 1404 * @param string $conditions SQL to use in the query. If 1405 * $conditions contains "SELECT", then $order, $limit and 1406 * $joins are ignored and the query is completely specified by 1407 * $conditions. If $conditions is omitted or does not contain 1408 * "SELECT", "SELECT * FROM" will be used. If $conditions is 1409 * specified and does not contain "SELECT", the query will 1410 * include "WHERE $conditions". If $conditions is null, the 1411 * entire table is returned. 1412 * @param string $order Argument to "ORDER BY" in query. 1413 * If specified, the query will include 1414 * "ORDER BY $order". If omitted, no ordering will be 1415 * applied. 1416 * FIXME This parameter doesn't seem to make sense 1417 * @param integer[] $limit Page, rows per page??? @todo Document this parameter 1418 * FIXME This parameter doesn't seem to make sense 1419 * @param string $joins ??? @todo Document this parameter 1420 * @uses find_all() 1421 * @return mixed An object of the same class as this class and 1422 * matching $conditions, or null if none did. 1423 * @throws {@link ActiveRecordError} 1424 */ 1425 function find_first($conditions = null, $order = null, $limit = null, $joins = null) { 1426 if(is_array($conditions)) { 1427 $options = $conditions; 1428 } else { 1429 $options['conditions'] = $conditions; 1430 } 1431 if(!is_null($order)) $options['order'] = $order; 1432 if(!is_null($limit)) $options['limit'] = $limit; 1433 if(!is_null($joins)) $options['joins'] = $joins; 1434 1435 $result = $this->find_all($options); 1436 return @current($result); 1437 } 1438 1439 /** 1440 * Return all the rows selected by the SQL argument 1441 * 1442 * If no rows match, an empty array is returned. 1443 * @param string $sql SQL to use in the query. 1444 */ 1445 function find_by_sql($sql) { 1446 return $this->find_all($sql); 1447 } 1448 1449 /** 1450 * Reloads the attributes of this object from the database. 1451 * @uses get_primary_key_conditions() 1452 * @todo Document this API 1453 */ 1454 function reload($conditions = null) { 1455 if(is_null($conditions)) { 1456 $conditions = $this->get_primary_key_conditions(); 1457 } 1458 $object = $this->find($conditions); 1459 if(is_object($object)) { 1460 foreach($object as $key => $value) { 1461 $this->$key = $value; 1462 } 1463 return true; 1464 } 1465 return false; 1466 } 1467 1468 /** 1469 * Loads into current object values from the database. 1470 */ 1471 function load($conditions = null) { 1472 return $this->reload($conditions); 1473 } 1474 1475 /** 1476 * @todo Document this API. What's going on here? It appears to 1477 * either create a row with all empty values, or it tries 1478 * to recurse once for each attribute in $attributes. 1479 * Creates an object, instantly saves it as a record (if the validation permits it). 1480 * If the save fails under validations it returns false and $errors array gets set. 1481 */ 1482 function create($attributes, $dont_validate = false) { 1483 $class_name = $this->get_class_name(); 1484 $object = new $class_name(); 1485 $result = $object->save($attributes, $dont_validate); 1486 return ($result ? $object : false); 1487 } 1488 1489 /** 1490 * Finds the record from the passed id, instantly saves it with the passed attributes 1491 * (if the validation permits it). Returns true on success and false on error. 1492 * @todo Document this API 1493 */ 1494 function update($id, $attributes, $dont_validate = false) { 1495 if(is_array($id)) { 1496 foreach($id as $update_id) { 1497 $this->update($update_id, $attributes[$update_id], $dont_validate); 1498 } 1499 } else { 1500 $object = $this->find($id); 1501 return $object->save($attributes, $dont_validate); 1502 } 1503 } 1504 1505 /** 1506 * Updates all records with the SET-part of an SQL update statement in updates and 1507 * returns an integer with the number of rows updates. A subset of the records can 1508 * be selected by specifying conditions. 1509 * Example: 1510 * $model->update_all("category = 'cooldude', approved = 1", "author = 'John'"); 1511 * @uses is_error() 1512 * @uses query() 1513 * @throws {@link ActiveRecordError} 1514 * @todo Document this API 1515 */ 1516 function update_all($updates, $conditions = null) { 1517 $sql = "UPDATE $this->table_name SET $updates WHERE $conditions"; 1518 $result = $this->query($sql); 1519 if ($this->is_error($result)) { 1520 $this->raise($result->getMessage()); 1521 } else { 1522 return true; 1523 } 1524 } 1525 1526 /** 1527 * Save without valdiating anything. 1528 * @todo Document this API 1529 */ 1530 function save_without_validation($attributes = null) { 1531 return $this->save($attributes, true); 1532 } 1533 1534 /** 1535 * Create or update a row in the table with specified attributes 1536 * 1537 * @param string[] $attributes List of name => value pairs giving 1538 * name and value of attributes to set. 1539 * @param boolean $dont_validate true => Don't call validation 1540 * routines before saving the row. If false or omitted, all 1541 * applicable validation routines are called. 1542 * @uses add_record_or_update_record() 1543 * @uses update_attributes() 1544 * @uses valid() 1545 * @return boolean 1546 * <ul> 1547 * <li>true => row was updated or inserted successfully</li> 1548 * <li>false => insert failed</li> 1549 * </ul> 1550 */ 1551 function save($attributes = null, $dont_validate = false) { 1552 //error_log("ActiveRecord::save() \$attributes=" 1553 // . var_export($attributes,true)); 1554 $this->update_attributes($attributes); 1555 if($dont_validate || $this->valid()) { 1556 return $this->add_record_or_update_record(); 1557 } else { 1558 return false; 1559 } 1560 } 1561 1562 /** 1563 * Create or update a row in the table 1564 * 1565 * If this object represents a new row in the table, insert it. 1566 * Otherwise, update the exiting row. before_?() and after_?() 1567 * routines will be called depending on whether the row is new. 1568 * @uses add_record() 1569 * @uses after_create() 1570 * @uses after_update() 1571 * @uses before_create() 1572 * @uses before_save() 1573 * @uses $new_record 1574 * @uses update_record() 1575 * @return boolean 1576 * <ul> 1577 * <li>true => row was updated or inserted successfully</li> 1578 * <li>false => insert failed</li> 1579 * </ul> 1580 */ 1581 private function add_record_or_update_record() { 1582 //error_log('add_record_or_update_record()'); 1583 $this->before_save(); 1584 if($this->new_record) { 1585 $this->before_create(); 1586 $result = $this->add_record(); 1587 $this->after_create(); 1588 } else { 1589 $this->before_update(); 1590 $result = $this->update_record(); 1591 $this->after_update(); 1592 } 1593 $this->after_save(); 1594 return $result; 1595 } 1596 1597 /** 1598 * Insert a new row in the table associated with this object 1599 * 1600 * Build an SQL INSERT statement getting the table name from 1601 * {@link $table_name}, the column names from {@link 1602 * $content_columns} and the values from object variables. 1603 * Send the insert to the RDBMS. 1604 * @uses $auto_save_habtm 1605 * @uses add_habtm_records() 1606 * @uses before_create() 1607 * @uses get_insert_id() 1608 * @uses is_error() 1609 * @uses query() 1610 * @uses get_inserts() 1611 * @uses raise() 1612 * @uses $table_name 1613 * @return boolean 1614 * <ul> 1615 * <li>true => row was inserted successfully</li> 1616 * <li>false => insert failed</li> 1617 * </ul> 1618 * @throws {@link ActiveRecordError} 1619 */ 1620 private function add_record() { 1621 self::$db->loadModule('Extended', null, true); 1622 # $primary_key_value may either be a quoted integer or php null 1623 $primary_key_value = self::$db->getBeforeID($this->table_name, $this->primary_keys[0]); 1624 if($this->is_error($primary_key_value)) { 1625 $this->raise($primary_key_value->getMessage()); 1626 } 1627 $this->update_composite_attributes(); 1628 $attributes = $this->get_inserts(); 1629 $fields = @implode(', ', array_keys($attributes)); 1630 $values = @implode(', ', array_values($attributes)); 1631 $sql = "INSERT INTO {$this->table_name} ($fields) VALUES ($values)"; 1632 //echo "add_record: SQL: $sql<br>"; 1633 //error_log("add_record: SQL: $sql"); 1634 $result = $this->query($sql); 1635 1636 if($this->is_error($result)) { 1637 $this->raise($results->getMessage()); 1638 } else { 1639 $habtm_result = true; 1640 $primary_key = $this->primary_keys[0]; 1641 # $id is now equivalent to the value in the id field that was inserted 1642 $primary_key_value = self::$db->getAfterID($primary_key_value, $this->table_name, $this->primary_keys[0]); 1643 if($this->is_error($primary_key_value)) { 1644 $this->raise($primary_key_value->getMessage()); 1645 } 1646 $this->$primary_key = $primary_key_value; 1647 if($primary_key_value != '') { 1648 if($this->auto_save_habtm) { 1649 $habtm_result = $this->add_habtm_records($primary_key_value); 1650 } 1651 $this->save_associations(); 1652 } 1653 return ($result && $habtm_result); 1654 } 1655 } 1656 1657 /** 1658 * Update the row in the table described by this object 1659 * 1660 * The primary key attributes must exist and have appropriate 1661 * non-null values. If a column is listed in {@link 1662 * $content_columns} but no attribute of that name exists, the 1663 * column will be set to the null string ''. 1664 * @todo Describe habtm automatic update 1665 * @uses is_error() 1666 * @uses get_updates_sql() 1667 * @uses get_primary_key_conditions() 1668 * @uses query() 1669 * @uses raise() 1670 * @uses update_habtm_records() 1671 * @return boolean 1672 * <ul> 1673 * <li>true => row was updated successfully</li> 1674 * <li>false => update failed</li> 1675 * </ul> 1676 * @throws {@link ActiveRecordError} 1677 */ 1678 private function update_record() { 1679 //error_log('update_record()'); 1680 $this->update_composite_attributes(); 1681 $updates = $this->get_updates_sql(); 1682 $conditions = $this->get_primary_key_conditions(); 1683 $sql = "UPDATE {$this->table_name} SET {$updates} WHERE {$conditions}"; 1684 //echo "update_record:$sql<br>"; 1685 //error_log("update_record: SQL: $sql"); 1686 $result = $this->query($sql); 1687 if($this->is_error($result)) { 1688 $this->raise($results->getMessage()); 1689 } else { 1690 $habtm_result = true; 1691 $primary_key = $this->primary_keys[0]; 1692 $primary_key_value = $this->$primary_key; 1693 if($primary_key_value > 0) { 1694 if($this->auto_save_habtm) { 1695 $habtm_result = $this->update_habtm_records($primary_key_value); 1696 } 1697 $this->save_associations(); 1698 } 1699 return ($result && $habtm_result); 1700 } 1701 } 1702 1703 /** 1704 * Loads the model values into composite object 1705 * @todo Document this API 1706 */ 1707 private function get_composite_object($name) { 1708 $composite_object = null; 1709 $composite_attributes = array(); 1710 if(is_array($this->composed_of)) { 1711 if(array_key_exists($name, $this->composed_of)) { 1712 $class_name = Inflector::classify(($this->composed_of[$name]['class_name'] ? 1713 $this->composed_of[$name]['class_name'] : $name)); 1714 1715 $mappings = $this->composed_of[$name]['mapping']; 1716 if(is_array($mappings)) { 1717 foreach($mappings as $database_name => $composite_name) { 1718 $composite_attributes[$composite_name] = $this->$database_name; 1719 } 1720 } 1721 } 1722 } elseif($this->composed_of == $name) { 1723 $class_name = $name; 1724 $composite_attributes[$name] = $this->$name; 1725 } 1726 1727 if(class_exists($class_name)) { 1728 $composite_object = new $class_name; 1729 if($composite_object->auto_map_attributes !== false) { 1730 //echo "auto_map_attributes<br>"; 1731 foreach($composite_attributes as $name => $value) { 1732 $composite_object->$name = $value; 1733 } 1734 } 1735 if(method_exists($composite_object, '__construct')) { 1736 //echo "calling constructor<br>"; 1737 $composite_object->__construct($composite_attributes); 1738 } 1739 } 1740 return $composite_object; 1741 } 1742 1743 /** 1744 * returns the association type if defined in child class or null 1745 * @todo Document this API 1746 * @uses $belongs_to 1747 * @uses $has_and_belongs_to_many 1748 * @uses $has_many 1749 * @uses $has_one 1750 * @return mixed Association type, one of the following: 1751 * <ul> 1752 * <li>"belongs_to"</li> 1753 * <li>"has_and_belongs_to_many"</li> 1754 * <li>"has_many"</li> 1755 * <li>"has_one"</li> 1756 * </ul> 1757 * if an association exists, or null if no association 1758 */ 1759 function get_association_type($association_name) { 1760 $type = null; 1761 if(is_string($this->has_many)) { 1762 if(preg_match("/\b$association_name\b/", $this->has_many)) { 1763 $type = "has_many"; 1764 } 1765 } elseif(is_array($this->has_many)) { 1766 if(array_key_exists($association_name, $this->has_many)) { 1767 $type = "has_many"; 1768 } 1769 } 1770 if(is_string($this->has_one)) { 1771 if(preg_match("/\b$association_name\b/", $this->has_one)) { 1772 $type = "has_one"; 1773 } 1774 } elseif(is_array($this->has_one)) { 1775 if(array_key_exists($association_name, $this->has_one)) { 1776 $type = "has_one"; 1777 } 1778 } 1779 if(is_string($this->belongs_to)) { 1780 if(preg_match("/\b$association_name\b/", $this->belongs_to)) { 1781 $type = "belongs_to"; 1782 } 1783 } elseif(is_array($this->belongs_to)) { 1784 if(array_key_exists($association_name, $this->belongs_to)) { 1785 $type = "belongs_to"; 1786 } 1787 } 1788 if(is_string($this->has_and_belongs_to_many)) { 1789 if(preg_match("/\b$association_name\b/", $this->has_and_belongs_to_many)) { 1790 $type = "has_and_belongs_to_many"; 1791 } 1792 } elseif(is_array($this->has_and_belongs_to_many)) { 1793 if(array_key_exists($association_name, $this->has_and_belongs_to_many)) { 1794 $type = "has_and_belongs_to_many"; 1795 } 1796 } 1797 return $type; 1798 } 1799 1800 /** 1801 * Saves any associations objects assigned to this instance 1802 * @uses $auto_save_associations 1803 * @todo Document this API 1804 */ 1805 private function save_associations() { 1806 if(count($this->save_associations) && $this->auto_save_associations) { 1807 foreach(array_keys($this->save_associations) as $type) { 1808 if(count($this->save_associations[$type])) { 1809 foreach($this->save_associations[$type] as $object_or_array) { 1810 if(is_object($object_or_array)) { 1811 $this->save_association($object_or_array, $type); 1812 } elseif(is_array($object_or_array)) { 1813 foreach($object_or_array as $object) { 1814 $this->save_association($object, $type); 1815 } 1816 } 1817 } 1818 } 1819 } 1820 } 1821 } 1822 1823 /** 1824 * save the association to the database 1825 * @todo Document this API 1826 */ 1827 private function save_association($object, $type) { 1828 if(is_object($object) && get_parent_class($object) == __CLASS__ && $type) { 1829 //echo get_class($object)." - type:$type<br>"; 1830 switch($type) { 1831 case "has_many": 1832 case "has_one": 1833 $primary_key = $this->primary_keys[0]; 1834 $foreign_key = Inflector::singularize($this->table_name)."_".$primary_key; 1835 $object->$foreign_key = $this->$primary_key; 1836 //echo "fk:$foreign_key = ".$this->$primary_key."<br>"; 1837 break; 1838 } 1839 $object->save(); 1840 } 1841 } 1842 1843 /** 1844 * Deletes the record with the given $id or if you have done a 1845 * $model = $model->find($id), then $model->delete() it will delete 1846 * the record it just loaded from the find() without passing anything 1847 * to delete(). If an array of ids is provided, all ids in array are deleted. 1848 * @uses $errors 1849 * @todo Document this API 1850 */ 1851 function delete($id = null) { 1852 $deleted_ids = array(); 1853 $primary_key_value = null; 1854 $primary_key = $this->primary_keys[0]; 1855 if(is_null($id)) { 1856 # Primary key's where clause from already loaded values 1857 $conditions = $this->get_primary_key_conditions(); 1858 $deleted_ids[] = $this->$primary_key; 1859 } elseif(!is_array($id)) { 1860 $deleted_ids[] = $id; 1861 $id = $this->attribute_is_string($primary_key) ? "'".$id."'" : $id; 1862 $conditions = "{$primary_key} = {$id}"; 1863 } elseif(is_array($id)) { 1864 $deleted_ids = $id; 1865 $ids = ($this->attribute_is_string($primary_key)) ? 1866 "'".implode("','", $id)."'" : 1867 implode(',', $id); 1868 $conditions = "{$primary_key} IN ({$ids})"; 1869 } 1870 1871 if(is_null($conditions)) { 1872 $this->errors[] = "No conditions specified to delete on."; 1873 return false; 1874 } 1875 1876 $this->before_delete(); 1877 if($result = $this->delete_all($conditions)) { 1878 foreach($deleted_ids as $id) { 1879 if($this->auto_delete_habtm && $id != '') { 1880 if(is_string($this->has_and_belongs_to_many)) { 1881 $habtms = explode(",", $this->has_and_belongs_to_many); 1882 foreach($habtms as $other_table_name) { 1883 $this->delete_all_habtm_records(trim($other_table_name), $id); 1884 } 1885 } elseif(is_array($this->has_and_belongs_to_many)) { 1886 foreach($this->has_and_belongs_to_many as $other_table_name => $values) { 1887 $this->delete_all_habtm_records($other_table_name, $id); 1888 } 1889 } 1890 } 1891 } 1892 $this->after_delete(); 1893 } 1894 1895 return $result; 1896 } 1897 1898 /** 1899 * Delete from table all rows that match argument 1900 * 1901 * Delete the row(s), if any, matching the argument. 1902 * @param string $conditions SQL argument to "WHERE" describing 1903 * the rows to delete 1904 * @return boolean 1905 * <ul> 1906 * <li>true => One or more rows were deleted</li> 1907 * <li>false => $conditions was omitted</li> 1908 * </ul> 1909 * @uses is_error() 1910 * @uses $new_record 1911 * @uses $errors 1912 * @uses query() 1913 * @throws {@link ActiveRecordError} 1914 */ 1915 function delete_all($conditions = null) { 1916 if(is_null($conditions)) { 1917 $this->errors[] = "No conditions specified to delete on."; 1918 return false; 1919 } 1920 1921 # Delete the record(s) 1922 if($this->is_error($rs = $this->query("DELETE FROM $this->table_name WHERE $conditions"))) { 1923 $this->raise($rs->getMessage()); 1924 } 1925 1926 $this->new_record = true; 1927 return true; 1928 } 1929 1930 /** 1931 * @uses $has_and_belongs_to_many 1932 * @todo Document this API 1933 */ 1934 private function set_habtm_attributes($attributes) { 1935 if(is_array($attributes)) { 1936 $this->habtm_attributes = array(); 1937 foreach($attributes as $key => $habtm_array) { 1938 if(is_array($habtm_array)) { 1939 if(is_string($this->has_and_belongs_to_many)) { 1940 if(preg_match("/\b$key\b/", $this->has_and_belongs_to_many)) { 1941 $this->habtm_attributes[$key] = $habtm_array; 1942 } 1943 } elseif(is_array($this->has_and_belongs_to_many)) { 1944 if(array_key_exists($key, $this->has_and_belongs_to_many)) { 1945 $this->habtm_attributes[$key] = $habtm_array; 1946 } 1947 } 1948 } 1949 } 1950 } 1951 } 1952 1953 /** 1954 * 1955 * @todo Document this API 1956 */ 1957 private function update_habtm_records($this_foreign_value) { 1958 return $this->add_habtm_records($this_foreign_value); 1959 } 1960 1961 /** 1962 * 1963 * @uses is_error() 1964 * @uses query() 1965 * @throws {@link ActiveRecordError} 1966 * @todo Document this API 1967 */ 1968 private function add_habtm_records($this_foreign_value) { 1969 if($this_foreign_value > 0 && count($this->habtm_attributes) > 0) { 1970 if($this->delete_habtm_records($this_foreign_value)) { 1971 reset($this->habtm_attributes); 1972 foreach($this->habtm_attributes as $other_table_name => $other_foreign_values) { 1973 $table_name = $this->get_join_table_name($this->table_name,$other_table_name); 1974 $other_foreign_key = Inflector::singularize($other_table_name)."_id"; 1975 $this_foreign_key = Inflector::singularize($this->table_name)."_id"; 1976 foreach($other_foreign_values as $other_foreign_value) { 1977 unset($attributes); 1978 $attributes[$this_foreign_key] = $this_foreign_value; 1979 $attributes[$other_foreign_key] = $other_foreign_value; 1980 $attributes = $this->quoted_attributes($attributes); 1981 $fields = @implode(', ', array_keys($attributes)); 1982 $values = @implode(', ', array_values($attributes)); 1983 $sql = "INSERT INTO $table_name ($fields) VALUES ($values)"; 1984 //echo "add_habtm_records: SQL: $sql<br>"; 1985 $result = $this->query($sql); 1986 if ($this->is_error($result)) { 1987 $this->raise($result->getMessage()); 1988 } 1989 } 1990 } 1991 } 1992 } 1993 return true; 1994 } 1995 1996 /** 1997 * 1998 * @uses is_error() 1999 * @uses query() 2000 * @throws {@link ActiveRecordError} 2001 * @todo Document this API 2002 */ 2003 private function delete_habtm_records($this_foreign_value) { 2004 if($this_foreign_value > 0 && count($this->habtm_attributes) > 0) { 2005 reset($this->habtm_attributes); 2006 foreach($this->habtm_attributes as $other_table_name => $values) { 2007 $this->delete_all_habtm_records($other_table_name, $this_foreign_value); 2008 } 2009 } 2010 return true; 2011 } 2012 2013 private function delete_all_habtm_records($other_table_name, $this_foreign_value) { 2014 if($other_table_name && $this_foreign_value > 0) { 2015 $habtm_table_name = $this->get_join_table_name($this->table_name,$other_table_name); 2016 $this_foreign_key = Inflector::singularize($this->table_name)."_id"; 2017 $sql = "DELETE FROM {$habtm_table_name} WHERE {$this_foreign_key} = {$this_foreign_value}"; 2018 //echo "delete_all_habtm_records: SQL: $sql<br>"; 2019 $result = $this->query($sql); 2020 if($this->is_error($result)) { 2021 $this->raise($result->getMessage()); 2022 } 2023 } 2024 } 2025 2026 /** 2027 * Apply automatic timestamp updates 2028 * 2029 * If automatic timestamps are in effect (as indicated by 2030 * {@link $auto_timestamps} == true) and the column named in the 2031 * $field argument is of type "timestamp" and matches one of the 2032 * names in {@link auto_create_timestamps} or {@link 2033 * auto_update_timestamps}(as selected by {@link $new_record}), 2034 * then return the current date and time as a string formatted 2035 * to insert in the database. Otherwise return $value. 2036 * @uses $new_record 2037 * @uses $content_columns 2038 * @uses $auto_timestamps 2039 * @uses $auto_create_timestamps 2040 * @uses $auto_update_timestamps 2041 * @param string $field Name of a column in the table 2042 * @param mixed $value Value to return if $field is not an 2043 * automatic timestamp column 2044 * @return mixed Current date and time or $value 2045 */ 2046 private function check_datetime($field, $value) { 2047 if($this->auto_timestamps) { 2048 if(is_array($this->content_columns)) { 2049 foreach($this->content_columns as $field_info) { 2050 if(($field_info['name'] == $field) && stristr($field_info['type'], "date")) { 2051 $format = ($field_info['type'] == "date") ? $this->date_format : "{$this->date_format} {$this->time_format}"; 2052 if($this->new_record) { 2053 if(in_array($field, $this->auto_create_timestamps)) { 2054 return date($format); 2055 } elseif($this->preserve_null_dates && is_null($value) && !stristr($field_info['flags'], "not_null")) { 2056 return null; 2057 } 2058 } elseif(!$this->new_record) { 2059 if(in_array($field, $this->auto_update_timestamps)) { 2060 return date($format); 2061 } elseif($this->preserve_null_dates && is_null($value) && !stristr($field_info['flags'], "not_null")) { 2062 return null; 2063 } 2064 } 2065 } 2066 } 2067 } 2068 } 2069 return $value; 2070 } 2071 2072 /** 2073 * Update object attributes from list in argument 2074 * 2075 * The elements of $attributes are parsed and assigned to 2076 * attributes of the ActiveRecord object. Date/time fields are 2077 * treated according to the 2078 * {@tutorial PHPonTrax/naming.pkg#naming.naming_forms}. 2079 * @param string[] $attributes List of name => value pairs giving 2080 * name and value of attributes to set. 2081 * @uses $auto_save_associations 2082 * @todo Figure out and document how datetime fields work 2083 */ 2084 function update_attributes($attributes) { 2085 //error_log('update_attributes()'); 2086 2087 if(is_array($attributes)) { 2088 // Test each attribute to be updated 2089 // and process according to its type 2090 foreach($attributes as $field => $value) { 2091 # datetime / date parts check 2092 if(preg_match('/^\w+\(.*i\)$/i', $field)) { 2093 2094 // The name of this attribute ends in "(?i)" 2095 // indicating that it's part of a date or time 2096 2097 // Accumulate all the pieces of a date and time in 2098 // array $datetime_key. The keys in the array are 2099 // the names of date/time attributes with the final 2100 // "(?i)" stripped off. 2101 $datetime_key = substr($field, 0, strpos($field, "(")); 2102 if( !isset($old_datetime_key) 2103 || ($datetime_key != $old_datetime_key)) { 2104 2105 // This value of $datetime_key hasn't been seen 2106 // before, so remember it. 2107 $old_datetime_key = $datetime_key; 2108 2109 // $datetime_value accumulates the pieces of the 2110 // date/time attribute $datetime_key 2111 $datetime_value = ""; 2112 } 2113 2114 // Concatentate pieces of the attribute's value 2115 // FIXME: this only works if the array elements 2116 // are sorted by key. Is this guaranteed? 2117 if(strstr($field, "2i") || strstr($field, "3i")) { 2118 $datetime_value .= "-".$value; 2119 } elseif(strstr($field, "4i")) { 2120 $datetime_value .= " ".$value; 2121 } elseif(strstr($field, "5i")) { 2122 $datetime_value .= ":".$value; 2123 } else { 2124 $datetime_value .= $value; 2125 } 2126 $datetime_fields[$old_datetime_key] = $datetime_value; 2127 # this elseif checks if first its an object if its parent is ActiveRecord 2128 } elseif(is_object($value) && get_parent_class($value) == __CLASS__ && $this->auto_save_associations) { 2129 if($association_type = $this->get_association_type($field)) { 2130 $this->save_associations[$association_type][] = $value; 2131 if($association_type == "belongs_to") { 2132 $primary_key = $value->primary_keys[0]; 2133 $foreign_key = Inflector::singularize($value->table_name)."_".$primary_key; 2134 $this->$foreign_key = $value->$primary_key; 2135 } 2136 } 2137 # this elseif checks if its an array of objects and if its parent is ActiveRecord 2138 } elseif(is_array($value) && $this->auto_save_associations) { 2139 if($association_type = $this->get_association_type($field)) { 2140 $this->save_associations[$association_type][] = $value; 2141 } 2142 } else { 2143 2144 // Just a simple attribute, copy it 2145 $this->$field = $value; 2146 } 2147 } 2148 2149 // If any date/time fields were found, assign the 2150 // accumulated values to corresponding attributes 2151 if(isset($datetime_fields) 2152 && is_array($datetime_fields)) { 2153 foreach($datetime_fields as $field => $value) { 2154 //error_log("$field = $value"); 2155 $this->$field = $value; 2156 } 2157 } 2158 $this->set_habtm_attributes($attributes); 2159 } 2160 } 2161 2162 /** 2163 * If a composite object was specified via $composed_of, then its values 2164 * mapped to the model will overwrite the models values. 2165 * 2166 */ 2167 function update_composite_attributes() { 2168 if(is_array($this->composed_of)) { 2169 foreach($this->composed_of as $name => $options) { 2170 $composite_object = $this->$name; 2171 if(is_array($options) && is_object($composite_object)) { 2172 if(is_array($options['mapping'])) { 2173 foreach($options['mapping'] as $database_name => $composite_name) { 2174 $this->$database_name = $composite_object->$composite_name; 2175 } 2176 } 2177 } 2178 } 2179 } 2180 } 2181 2182 /** 2183 * Return pairs of column-name:column-value 2184 * 2185 * Return the contents of the object as an array of elements 2186 * where the key is the column name and the value is the column 2187 * value. Relies on a previous call to 2188 * {@link set_content_columns()} for information about the format 2189 * of a row in the table. 2190 * @uses $content_columns 2191 * @see set_content_columns 2192 * @see quoted_attributes() 2193 */ 2194 function get_attributes() { 2195 $attributes = array(); 2196 if(is_array($this->content_columns)) { 2197 foreach($this->content_columns as $column) { 2198 //echo "attribute: $info[name] -> {$this->$info[name]}<br>"; 2199 $attributes[$column['name']] = $this->$column['name']; 2200 } 2201 } 2202 return $attributes; 2203 } 2204 2205 /** 2206 * Return pairs of column-name:quoted-column-value 2207 * 2208 * Return pairs of column-name:quoted-column-value where the key 2209 * is the column name and the value is the column value with 2210 * automatic timestamp updating applied and characters special to 2211 * SQL quoted. 2212 * 2213 * If $attributes is null or omitted, return all columns as 2214 * currently stored in {@link content_columns()}. Otherwise, 2215 * return the name:value pairs in $attributes. 2216 * @param string[] $attributes Name:value pairs to return. 2217 * If null or omitted, return the column names and values 2218 * of the object as stored in $content_columns. 2219 * @return string[] 2220 * @uses get_attributes() 2221 * @see set_content_columns() 2222 */ 2223 function quoted_attributes($attributes = null) { 2224 if(is_null($attributes)) { 2225 $attributes = $this->get_attributes(); 2226 } 2227 $return = array(); 2228 foreach($attributes as $key => $value) { 2229 $value = $this->check_datetime($key, $value); 2230 $column = $this->column_for_attribute($key); 2231 $type = $this->attribute_is_string($key, $column) ? "Text" : "Integer"; 2232 $value = self::$db->quote($value, $type); 2233 if($value == 'NULL' && stristr($column['flags'], "not_null")) { 2234 $value = "''"; 2235 } 2236 $return[$key] = $value; 2237 } 2238 return $return; 2239 } 2240 2241 /** 2242 * Return column values for SQL insert statement 2243 * 2244 * Return an array containing the column names and values of this 2245 * object, filtering out the primary keys, which are not set. 2246 * 2247 * @uses $primary_keys 2248 * @uses quoted_attributes() 2249 */ 2250 function get_inserts() { 2251 $attributes = $this->quoted_attributes(); 2252 $inserts = array(); 2253 foreach($attributes as $key => $value) { 2254 if(!in_array($key, $this->primary_keys) || ($value != "''" && isset($value))) { 2255 $inserts[$key] = $value; 2256 } 2257 } 2258 return $inserts; 2259 } 2260 2261 /** 2262 * Return argument for a "WHERE" clause specifying this row 2263 * 2264 * Returns a string which specifies the column(s) and value(s) 2265 * which describe the primary key of this row of the associated 2266 * table. The primary key must be one or more attributes of the 2267 * object and must be listed in {@link $content_columns} as 2268 * columns in the row. 2269 * 2270 * Example: if $primary_keys = array("id", "ssn") and column "id" 2271 * has value "5" and column "ssn" has value "123-45-6789" then 2272 * the string "id = '5' AND ssn = '123-45-6789'" would be returned. 2273 * @uses $primary_keys 2274 * @uses quoted_attributes() 2275 * @return string Column name = 'value' [ AND name = 'value']... 2276 */ 2277 function get_primary_key_conditions() { 2278 $conditions = null; 2279 $attributes = $this->quoted_attributes(); 2280 if(count($attributes) > 0) { 2281 $conditions = array(); 2282 # run through our fields and join them with their values 2283 foreach($attributes as $key => $value) { 2284 if(in_array($key, $this->primary_keys) && isset($value) && $value != "''") { 2285 $conditions[] = "$key = $value"; 2286 } 2287 } 2288 $conditions = implode(" AND ", $conditions); 2289 } 2290 return $conditions; 2291 } 2292 2293 /** 2294 * Return column values of object formatted for SQL update statement 2295 * 2296 * Return a string containing the column names and values of this 2297 * object in a format ready to be inserted in a SQL UPDATE 2298 * statement. Automatic update has been applied to timestamps if 2299 * enabled and characters special to SQL have been quoted. 2300 * @uses quoted_attributes() 2301 * @return string Column name = 'value', ... for all attributes 2302 */ 2303 function get_updates_sql() { 2304 $updates = null; 2305 $attributes = $this->quoted_attributes(); 2306 if(count($attributes) > 0) { 2307 $updates = array(); 2308 # run through our fields and join them with their values 2309 foreach($attributes as $key => $value) { 2310 if($key && isset($value) && !in_array($key, $this->primary_keys)) { 2311 $updates[] = "$key = $value"; 2312 } 2313 } 2314 $updates = implode(", ", $updates); 2315 } 2316 return $updates; 2317 } 2318 2319 /** 2320 * Set {@link $table_name} from the class name of this object 2321 * 2322 * By convention, the name of the database table represented by 2323 * this object is derived from the name of the class. 2324 * @uses Inflector::tableize() 2325 */ 2326 function set_table_name_using_class_name() { 2327 if(!$this->table_name) { 2328 $class_name = $this->get_class_name(); 2329 $this->table_name = Inflector::tableize($class_name); 2330 } 2331 } 2332 2333 /** 2334 * Get class name of child object 2335 * 2336 * this will return the manually set name or get_class($this) 2337 * @return string child class name 2338 */ 2339 private function get_class_name() { 2340 if(!is_null($this->class_name)) { 2341 $class_name = $this->class_name; 2342 } else { 2343 $class_name = get_class($this); 2344 } 2345 return $class_name; 2346 } 2347 2348 /** 2349 * Populate object with information about the table it represents 2350 * 2351 * Call {@link 2352 * http://pear.php.net/manual/en/package.database.db.db-common.tableinfo.php 2353 * DB_common::tableInfo()} to get a description of the table and 2354 * store it in {@link $content_columns}. Add a more human 2355 * friendly name to the element for each column. 2356 * @uses $db 2357 * @uses $content_columns 2358 * @uses Inflector::humanize() 2359 * @see __set() 2360 * @param string $table_name Name of table to get information about 2361 */ 2362 function set_content_columns($table_name) { 2363 if(isset(self::$table_info[$table_name])) { 2364 $this->content_columns = self::$table_info[$table_name]; 2365 } else { 2366 self::$db->loadModule('Reverse', null, true); 2367 $this->content_columns = self::$db->reverse->tableInfo($table_name); 2368 if($this->is_error($this->content_columns)) { 2369 $this->raise($this->content_columns->getMessage()); 2370 } 2371 if(is_array($this->content_columns)) { 2372 $i = 0; 2373 foreach($this->content_columns as $column) { 2374 $this->content_columns[$i++]['human_name'] = Inflector::humanize($column['name']); 2375 } 2376 self::$table_info[$table_name] = $this->content_columns; 2377 } 2378 } 2379 } 2380 2381 /** 2382 * Returns the autogenerated id from the last insert query 2383 * 2384 * @uses $db 2385 * @uses is_error() 2386 * @uses raise() 2387 * @throws {@link ActiveRecordError} 2388 */ 2389 function get_insert_id() { 2390 // fetch the last inserted id via autoincrement or current value of a sequence 2391 if(self::$db->supports('auto_increment') === true) { 2392 $id = self::$db->lastInsertID($this->table_name, $this->primary_keys[0]); 2393 if($this->is_error($id)) { 2394 $this->raise($id->getMessage()); 2395 } 2396 return $id; 2397 } 2398 2399 return null; 2400 } 2401 2402 /** 2403 * Open a database connection if one is not currently open 2404 * 2405 * The name of the database normally comes from 2406 * $database_settings which is set in {@link 2407 * environment.php} by reading file config/database.ini. The 2408 * database name may be overridden by assigning a different name 2409 * to {@link $database_name}. 2410 * 2411 * If there is a connection now open, as indicated by the saved 2412 * value of a MDB2 object in $active_connections[$connection_name], and 2413 * {@link force_reconnect} is not true, then set the database 2414 * fetch mode and return. 2415 * 2416 * If there is no connection, open one and save a reference to 2417 * it in $active_connections[$connection_name]. 2418 * 2419 * @uses $db 2420 * @uses $database_name 2421 * @uses $force_reconnect 2422 * @uses $active_connections 2423 * @uses is_error() 2424 * @throws {@link ActiveRecordError} 2425 */ 2426 function establish_connection() { 2427 $connection =& self::$active_connections[$this->connection_name]; 2428 if(!is_object($connection) || $this->force_reconnect) { 2429 if(array_key_exists($this->connection_name, self::$database_settings)) { 2430 # Use a different custom sections settings ? 2431 if(array_key_exists("use", self::$database_settings[$this->connection_name])) { 2432 $connection_settings = self::$database_settings[self::$database_settings[$this->connection_name]['use']]; 2433 } else { 2434 # Custom defined db settings in database.ini 2435 $connection_settings = self::$database_settings[$this->connection_name]; 2436 } 2437 } else { 2438 # Just use the current TRAX_ENV's environment db settings 2439 # $this->connection_name's default value is TRAX_ENV so 2440 # if should never really get here unless override $this->connection_name 2441 # and you define a custom db section in database.ini and it can't find it. 2442 $connection_settings = self::$database_settings[TRAX_ENV]; 2443 } 2444 # Override database name if param is set 2445 if($this->database_name) { 2446 $connection_settings['database'] = $this->database_name; 2447 } 2448 # Set optional Pear parameters 2449 if(isset($connection_settings['persistent'])) { 2450 $connection_options['persistent'] = $connection_settings['persistent']; 2451 } 2452 # Connect to the database and throw an error if the connect fails. 2453 $connection =& MDB2::Connect($connection_settings, $connection_options); 2454 //static $connect_cnt; $connect_cnt++; error_log("connection #".$connect_cnt); 2455 } 2456 if(!$this->is_error($connection)) { 2457 self::$active_connections[$this->connection_name] =& $connection; 2458 self::$db =& $connection; 2459 } else { 2460 $this->raise($connection->getMessage()); 2461 } 2462 self::$db->setFetchMode($this->fetch_mode); 2463 return self::$db; 2464 } 2465 2466 /** 2467 * Test whether argument is a PEAR Error object or a MDB2 Error object. 2468 * 2469 * @param object $obj Object to test 2470 * @return boolean Whether object is one of these two errors 2471 */ 2472 function is_error($obj) { 2473 if((PEAR::isError($obj)) || (MDB2::isError($obj))) { 2474 return true; 2475 } else { 2476 return false; 2477 } 2478 } 2479 2480 /** 2481 * Throw an exception describing an error in this object 2482 * 2483 * @throws {@link ActiveRecordError} 2484 */ 2485 function raise($message) { 2486 $error_message = "Model Class: ".$this->get_class_name()."<br>"; 2487 $error_message .= "Error Message: ".$message; 2488 throw new ActiveRecordError($error_message, "ActiveRecord Error", "500"); 2489 } 2490 2491 /** 2492 * Add or overwrite description of an error to the list of errors 2493 * @param string $error Error message text 2494 * @param string $key Key to associate with the error (in the 2495 * simple case, column name). If omitted, numeric keys will be 2496 * assigned starting with 0. If specified and the key already 2497 * exists in $errors, the old error message will be overwritten 2498 * with the value of $error. 2499 * @uses $errors 2500 */ 2501 function add_error($error, $key = null) { 2502 if(!is_null($key)) { 2503 $this->errors[$key] = $error; 2504 } else { 2505 $this->errors[] = $error; 2506 } 2507 } 2508 2509 /** 2510 * Return description of non-fatal errors 2511 * 2512 * @uses $errors 2513 * @param boolean $return_string 2514 * <ul> 2515 * <li>true => Concatenate all error descriptions into a string 2516 * using $seperator between elements and return the 2517 * string</li> 2518 * <li>false => Return the error descriptions as an array</li> 2519 * </ul> 2520 * @param string $seperator String to concatenate between error 2521 * descriptions if $return_string == true 2522 * @return mixed Error description(s), if any 2523 */ 2524 function get_errors($return_string = false, $seperator = "<br>") { 2525 if($return_string && count($this->errors) > 0) { 2526 return implode($seperator, $this->errors); 2527 } else { 2528 return $this->errors; 2529 } 2530 } 2531 2532 /** 2533 * Return errors as a string. 2534 * 2535 * Concatenate all error descriptions into a stringusing 2536 * $seperator between elements and return the string. 2537 * @param string $seperator String to concatenate between error 2538 * descriptions 2539 * @return string Concatenated error description(s), if any 2540 */ 2541 function get_errors_as_string($seperator = "<br>") { 2542 return $this->get_errors(true, $seperator); 2543 } 2544 2545 /** 2546 * Determine if passed in attribute (table column) is a string 2547 * @param string $attribute Name of the table column 2548 * @uses column_for_attribute() 2549 */ 2550 function attribute_is_string($attribute, $column = null) { 2551 $column = is_null($column) ? $this->column_for_attribute($attribute) : $column; 2552 switch(strtolower($column['mdb2type'])) { 2553 case 'text': 2554 case 'timestamp': 2555 case 'date': 2556 case 'time': 2557 case 'blob': 2558 case 'clob': 2559 return true; 2560 } 2561 return false; 2562 } 2563 2564 /** 2565 * Determine if passed in name is a composite class or not 2566 * @param string $name Name of the composed_of mapping 2567 * @uses $composed_of 2568 */ 2569 private function is_composite($name) { 2570 if(is_array($this->composed_of)) { 2571 if(array_key_exists($name, $this->composed_of)) { 2572 return true; 2573 } 2574 } 2575 return false; 2576 } 2577 2578 /** 2579 * Runs validation routines for update or create 2580 * 2581 * @uses after_validation(); 2582 * @uses after_validation_on_create(); 2583 * @uses after_validation_on_update(); 2584 * @uses before_validation(); 2585 * @uses before_validation_on_create(); 2586 * @uses before_validation_on_update(); 2587 * @uses $errors 2588 * @uses $new_record 2589 * @uses validate(); 2590 * @uses validate_model_attributes(); 2591 * @uses validate_on_create(); 2592 * @return boolean 2593 * <ul> 2594 * <li>true => Valid, no errors found. 2595 * {@link $errors} is empty</li> 2596 * <li>false => Not valid, errors in {@link $errors}</li> 2597 * </ul> 2598 */ 2599 function valid() { 2600 # first clear the errors array 2601 $this->errors = array(); 2602 2603 if($this->new_record) { 2604 $this->before_validation(); 2605 $this->before_validation_on_create(); 2606 $this->validate(); 2607 $this->validate_model_attributes(); 2608 $this->after_validation(); 2609 $this->validate_on_create(); 2610 $this->after_validation_on_create(); 2611 } else { 2612 $this->before_validation(); 2613 $this->before_validation_on_update(); 2614 $this->validate(); 2615 $this->validate_model_attributes(); 2616 $this->after_validation(); 2617 $this->validate_on_update(); 2618 $this->after_validation_on_update(); 2619 } 2620 2621 return count($this->errors) ? false : true; 2622 } 2623 2624 /** 2625 * Call every method named "validate_*()" where * is a column name 2626 * 2627 * Find and call every method named "validate_something()" where 2628 * "something" is the name of a column. The "validate_something()" 2629 * functions are expected to return an array whose first element 2630 * is true or false (indicating whether or not the validation 2631 * succeeded), and whose second element is the error message to 2632 * display if the first element is false. 2633 * 2634 * @return boolean 2635 * <ul> 2636 * <li>true => Valid, no errors found. 2637 * {@link $errors} is empty</li> 2638 * <li>false => Not valid, errors in {@link $errors}. 2639 * $errors is an array whose keys are the names of columns, 2640 * and the value of each key is the error message returned 2641 * by the corresponding validate_*() method.</li> 2642 * </ul> 2643 * @uses $errors 2644 * @uses get_attributes() 2645 */ 2646 function validate_model_attributes() { 2647 $validated_ok = true; 2648 $attrs = $this->get_attributes(); 2649 $methods = get_class_methods($this->get_class_name()); 2650 foreach($methods as $method) { 2651 if(preg_match('/^validate_(.+)/', $method, $matches)) { 2652 # If we find, for example, a method named validate_name, then 2653 # we know that that function is validating the 'name' attribute 2654 # (as found in the (.+) part of the regular expression above). 2655 $validate_on_attribute = $matches[1]; 2656 # Check to see if the string found (e.g. 'name') really is 2657 # in the list of attributes for this object... 2658 if(array_key_exists($validate_on_attribute, $attrs)) { 2659 # ...if so, then call the method to see if it validates to true... 2660 $result = $this->$method(); 2661 if(is_array($result)) { 2662 # $result[0] is true if validation went ok, false otherwise 2663 # $result[1] is the error message if validation failed 2664 if($result[0] == false) { 2665 # ... and if not, then validation failed 2666 $validated_ok = false; 2667 # Mark the corresponding entry in the error array by 2668 # putting the error message in for the attribute, 2669 # e.g. $this->errors['name'] = "can't be empty" 2670 # when 'name' was an empty string. 2671 $this->errors[$validate_on_attribute] = $result[1]; 2672 } 2673 } 2674 } 2675 } 2676 } 2677 return $validated_ok; 2678 } 2679 2680 /** 2681 * Overwrite this method for validation checks on all saves and 2682 * use $this->errors[] = "My error message."; or 2683 * for invalid attributes $this->errors['attribute'] = "Attribute is invalid."; 2684 * @todo Document this API 2685 */ 2686 function validate() {} 2687 2688 /** 2689 * Override this method for validation checks used only on creation. 2690 * @todo Document this API 2691 */ 2692 function validate_on_create() {} 2693 2694 /** 2695 * Override this method for validation checks used only on updates. 2696 * @todo Document this API 2697 */ 2698 function validate_on_update() {} 2699 2700 /** 2701 * Is called before validate(). 2702 * @todo Document this API 2703 */ 2704 function before_validation() {} 2705 2706 /** 2707 * Is called after validate(). 2708 * @todo Document this API 2709 */ 2710 function after_validation() {} 2711 2712 /** 2713 * Is called before validate() on new objects that haven't been saved yet (no record exists). 2714 * @todo Document this API 2715 */ 2716 function before_validation_on_create() {} 2717 2718 /** 2719 * Is called after validate() on new objects that haven't been saved yet (no record exists). 2720 * @todo Document this API 2721 */ 2722 function after_validation_on_create() {} 2723 2724 /** 2725 * Is called before validate() on existing objects that has a record. 2726 * @todo Document this API 2727 */ 2728 function before_validation_on_update() {} 2729 2730 /** 2731 * Is called after validate() on existing objects that has a record. 2732 * @todo Document this API 2733 */ 2734 function after_validation_on_update() {} 2735 2736 /** 2737 * Is called before save() (regardless of whether its a create or update save) 2738 * @todo Document this API 2739 */ 2740 function before_save() {} 2741 2742 /** 2743 * Is called after save (regardless of whether its a create or update save). 2744 * @todo Document this API 2745 */ 2746 function after_save() {} 2747 2748 /** 2749 * Is called before save() on new objects that havent been saved yet (no record exists). 2750 * @todo Document this API 2751 */ 2752 function before_create() {} 2753 2754 /** 2755 * Is called after save() on new objects that havent been saved yet (no record exists). 2756 * @todo Document this API 2757 */ 2758 function after_create() {} 2759 2760 /** 2761 * Is called before save() on existing objects that has a record. 2762 * @todo Document this API 2763 */ 2764 function before_update() {} 2765 2766 /** 2767 * Is called after save() on existing objects that has a record. 2768 * @todo Document this API 2769 */ 2770 function after_update() {} 2771 2772 /** 2773 * Is called before delete(). 2774 * @todo Document this API 2775 */ 2776 function before_delete() {} 2777 2778 /** 2779 * Is called after delete(). 2780 * @todo Document this API 2781 */ 2782 function after_delete() {} 2783 2784 /** 2785 * Log SQL query in development mode 2786 * 2787 * If running in development mode, log the query to self::$query_log 2788 * @param string SQL to be logged 2789 */ 2790 function log_query($query) { 2791 if(TRAX_ENV == "development" && $query) { 2792 self::$query_log[] = $query; 2793 } 2794 } 2795 2796 } 2797 2798 // -- set Emacs parameters -- 2799 // Local variables: 2800 // tab-width: 4 2801 // c-basic-offset: 4 2802 // c-hanging-comment-ender-p: nil 2803 // indent-tabs-mode: nil 2804 // End: 2805 ?>
titre
Description
Corps
titre
Description
Corps
titre
Description
Corps
titre
Corps
| Généré le : Sun Feb 25 20:04:38 2007 | par Balluche grâce à PHPXref 0.7 |