[ Index ]
 

Code source de PHPonTrax 2.6.6-svn

Accédez au Source d'autres logiciels libresSoutenez Angelica Josefina !

title

Body

[fermer]

/vendor/trax/ -> active_record.php (source)

   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  ?>


Généré le : Sun Feb 25 20:04:38 2007 par Balluche grâce à PHPXref 0.7