PHP on T R A X
Rapid Application Development Made Easy

root/trunk/trax/vendor/trax/active_record.php

Revision 311, 135.9 kB (checked in by john, 2 months ago)

updates and bug fixes

  • Property svn:keywords set to Id
Line 
1 <?php
2 /**
3  *  File containing the ActiveRecord class
4  *
5  *  (PHP 5)
6  *
7  *  @package PHPonTrax
8  *  @version $Id$
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     protected 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/package/MDB2/docs/2.3.0/MDB2/MDB2_Driver_Reverse_Common.html#methodtableInfo
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 $connection_pool 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 = null;
157
158     /**
159      *  Index into the $connection_pool_read_only array
160      *
161      *  Name of the index to use to return or set the current db connection
162      *  Mainly used if you want to force all reads(SELECT's) to goto a
163      *  specific database server.
164      *  @var string
165      */   
166     public $read_only_connection_name = null;
167
168     /**
169      *  Index into the $connection_pool_read_only array
170      *
171      *  Same as $read_only_connection_name but set for all models globally.
172      *  @var string
173      */   
174     public static $global_read_only_connection_name = null;
175
176     /**
177      * What environment to run in.
178      */
179     public static $environment = 'development';
180
181     /**
182      * Stores the database settings
183      */
184     public static $database_settings = array();
185     
186     /**
187      * Stores the active read/write connections. Indexed on $connection_name.
188      */
189     public static $connection_pool = array();
190
191     /**
192      * Stores the active read only connections. Indexed on $connection_name.
193      */   
194     public static $connection_pool_read_only = array();
195
196     /**
197      *  Mode to use when fetching data from database
198      *
199      *  See {@link
200      *  http://pear.php.net/package/MDB2/docs/2.3.0/MDB2/MDB2_Driver_Common.html#methodsetFetchMode
201      *  the relevant PEAR MDB2 class documentation}
202      *  @var integer
203      */
204     public $fetch_mode = MDB2_FETCHMODE_ASSOC;
205
206     /**
207      *  Force reconnect to database every page load
208      *
209      *  @var boolean
210      */
211     public $force_reconnect = false;
212
213     /**
214      *  find_all() returns an array of objects,
215      *  each object index is off of this field
216      *
217      *  @var string
218      */   
219     public $index_on = "id";
220
221     /**
222      *  Not yet implemented (page 222 Rails books)
223      *
224      *  @var boolean
225      */   
226     public $lock_optimistically = true;
227     
228     /**
229      *  Composite custom user created objects
230      *  @var mixed
231      */   
232     public $composed_of = null;
233
234     # Table associations
235     /**
236      *  @todo Document this variable
237      *  @var string[]
238      */
239     protected $has_many = null;
240
241     /**
242      *  @todo Document this variable
243      *  @var string[]
244      */
245     protected $has_one = null;
246
247     /**
248      *  @todo Document this variable
249      *  @var string[]
250      */
251     protected $has_and_belongs_to_many = null;
252
253     /**
254      *  @todo Document this variable
255      *  @var string[]
256      */
257     protected $belongs_to = null;
258
259     /**
260      *  @todo Document this variable
261      *  @var string[]
262      */
263     protected $habtm_attributes = null;
264
265     /**
266      *  @todo Document this property
267      */
268     protected $save_associations = array();
269     
270     /**
271      *  Whether or not to auto save defined associations if set
272      *  @var boolean
273      */
274     public $auto_save_associations = true;
275
276     /**
277      *  Whether this object represents a new record
278      *
279      *  true => This object was created without reading a row from the
280      *          database, so use SQL 'INSERT' to put it in the database.
281      *  false => This object was a row read from the database, so use
282      *           SQL 'UPDATE' to update database with new values.
283      *  @var boolean
284      */
285     protected $new_record = true;
286
287     /**
288      *  Names of automatic update timestamp columns
289      *
290      *  When a row containing one of these columns is updated and
291      *  {@link $auto_timestamps} is true, update the contents of the
292      *  timestamp columns with the current date and time.
293      *  @see $auto_timestamps
294      *  @see $auto_create_timestamps
295      *  @var string[]
296      */
297     public $auto_update_timestamps = array("updated_at","updated_on");
298
299     /**
300      *  Names of automatic create timestamp columns
301      *
302      *  When a row containing one of these columns is created and
303      *  {@link $auto_timestamps} is true, store the current date and
304      *  time in the timestamp columns.
305      *  @see $auto_timestamps
306      *  @see $auto_update_timestamps
307      *  @var string[]
308      */
309     public $auto_create_timestamps = array("created_at","created_on");
310
311     /**
312      *  Date format for use with auto timestamping
313      *
314      *  The format for this should be compatiable with the php date() function.
315      *  http://www.php.net/date
316      *  @var string
317      */
318      public $date_format = "Y-m-d";
319
320     /**
321      *  Time format for use with auto timestamping
322      *
323      *  The format for this should be compatiable with the php date() function.
324      *  http://www.php.net/date
325      *  @var string
326      */   
327      public $time_format = "H:i:s";
328       
329     /**
330      *  Whether to keep date/datetime fields NULL if not set
331      *
332      *  true => If date field is not set it try to preserve NULL
333      *  false => Don't try to preserve NULL if field is already NULL
334      *  @var boolean
335      */       
336      protected $preserve_null_dates = true;
337
338     /**
339      *  SQL aggregate functions that may be applied to the associated
340      *  table.
341      *
342      *  SQL defines aggregate functions AVG, COUNT, MAX, MIN and SUM.
343      *  Not all of these functions are implemented by all DBMS's
344      *  @var string[]
345      */
346     protected $aggregations = array("count","sum","avg","max","min");
347               
348     /**
349      *  Primary key of the associated table
350      *
351      *  Array element(s) name the primary key column(s), as used to
352      *  specify the row to be updated or deleted.  To be a primary key
353      *  a column must be listed both here and in {@link
354      *  $content_columns}.  <b>NOTE:</b>This
355      *  field is maintained by hand.  It is not derived from the table
356      *  description read from the database.
357      *  @var string[]
358      *  @see $content_columns
359      *  @see find()
360      *  @see find_all()
361      *  @see find_first()
362      */
363     public $primary_keys = array("id");
364
365     /**
366      *  Default for how many rows to return from {@link find_all()}
367      *  @var integer
368      */
369     public static $rows_per_page_default = 20;
370
371     /**
372      *  Pagination how many numbers in the list << < 1 2 3 4 > >>
373      */
374     public $display = 10;
375
376     /**
377      *  @todo Document this variable
378      */   
379     public $pagination_count = 0;
380
381     /**
382      *  @todo Document this variable
383      */
384     public $page = 0;
385
386     /**
387      * Sets the default options for the model.
388      *
389      * class Person extends ActiveRecord {
390      *     public $default_scope = array(
391      *         'order' => 'last_name, first_name'
392      *     ));
393      * }
394      *
395      */       
396     public $default_scope = array();
397     
398     /**
399      * Adds a class method for retrieving and querying objects.
400      * A scope represents a narrowing of a database query, such as
401      * 'conditions' => "first_name = 'John'"
402      *
403      * class Person extends ActiveRecord {
404      *     public $named_scope = array(
405      *         'people_named_john' => array(
406      *             'conditions' => "first_name = 'John'",     
407      *             'order' => 'last_name, first_name'
408      *     ));
409      * }
410      *
411      * $person = new Person;
412      * $person->people_named_john; # an array of AR objects people first_name = 'John'
413      *
414      */
415     public $named_scope = array();
416     
417
418     /**
419      *  Description of non-fatal errors found
420      *
421      *  For every non-fatal error found, an element describing the
422      *  error is added to $errors.  Initialized to an empty array in
423      *  {@link valid()} before validating object.  When an error
424      *  message is associated with a particular attribute, the message
425      *  should be stored with the attribute name as its key.  If the
426      *  message is independent of attributes, store it with a numeric
427      *  key beginning with 0.
428      * 
429      *  @var string[]
430      *  @see add_error()
431      *  @see get_errors()
432      */
433     public $errors = array();
434
435     /**
436      * An array with all the default error messages.
437      */
438     public $default_error_messages = array(
439         'inclusion' => "is not included in the list",
440         'exclusion' => "is reserved",
441         'invalid' => "is invalid",
442         'confirmation' => "doesn't match confirmation",
443         'accepted ' => "must be accepted",
444         'empty' => "can't be empty",
445         'blank' => "can't be blank",
446         'too_long' => "is too long (max is %d characters)",
447         'too_short' => "is too short (min is %d characters)",
448         'wrong_length' => "is the wrong length (should be %d characters)",
449         'taken' => "has already been taken",
450         'not_a_number' => "is not a number",
451         'not_an_integer' => "is not an integer"
452     );
453
454     /**
455      * An array of all the builtin validation function calls.
456      */   
457     protected $builtin_validation_functions = array(
458         'validates_acceptance_of',
459         'validates_confirmation_of',
460         'validates_exclusion_of',       
461         'validates_format_of',
462         'validates_inclusion_of',       
463         'validates_length_of',
464         'validates_numericality_of',       
465         'validates_presence_of',       
466         'validates_uniqueness_of'
467     );
468
469     /**
470      *  Whether to automatically update timestamps in certain columns
471      *
472      *  @see $auto_create_timestamps
473      *  @see $auto_update_timestamps
474      *  @var boolean
475      */
476     public $auto_timestamps = true;
477
478     /**
479      *  Auto insert / update $has_and_belongs_to_many tables
480      */
481     public $auto_save_habtm = true;
482
483     /**
484      *  Auto delete $has_and_belongs_to_many associations
485      */   
486     public $auto_delete_habtm = true;
487
488     /**
489      *  Transactions (only use if your db supports it)
490      *  This is for transactions only to let query() know that a 'BEGIN' has been executed
491      */
492     private static $in_transaction = false;
493
494     /**
495      *  Transactions (only use if your db supports it)
496      *  This will issue a rollback command if any sql fails.
497      */
498     public static $auto_rollback = false;
499     
500     /**
501      *  Keep a log of queries executed if in development env
502      */   
503     public static $query_log = array();
504
505     /**
506      *  Construct an ActiveRecord object
507      *
508      *  <ol>
509      *    <li>Establish a connection to the database</li>
510      *    <li>Find the name of the table associated with this object</li>
511      *    <li>Read description of this table from the database</li>
512      *    <li>Optionally apply update information to column attributes</li>
513      *  </ol>
514      *  @param string[] $attributes Updates to column attributes
515      *  @uses establish_connection()
516      *  @uses set_content_columns()
517      *  @uses $table_name
518      *  @uses set_table_name_using_class_name()
519      *  @uses update_attributes()
520      */
521     function __construct($attributes = null) {
522         # Open the database connection for reads / writes
523         self::$db = $this->establish_connection();
524         if($this->read_only_connection_name) {
525             # Open database connection for all reads
526             $this->establish_connection($this->read_only_connection_name, true);
527         } elseif(self::$global_read_only_connection_name) {
528             # Open database connection for all reads
529             $this->establish_connection(self::$global_read_only_connection_name, true);           
530         }
531
532         # Set $table_name
533         if($this->table_name == null) {
534             $this->set_table_name_using_class_name();
535         }
536
537         # Set column info
538         if($this->table_name) {
539             $this->set_content_columns($this->table_name);
540         }
541
542         # If $attributes array is passed in update the class with its contents
543         if(!is_null($attributes)) {
544             $this->update_attributes($attributes);
545         }
546         
547         # If callback is defined in model run it.
548         # this could hurt performance...
549         if(method_exists($this, 'after_initialize')) {
550             $this->after_initialize();   
551         }       
552     }
553
554     /**
555      *  Override get() if they do $model->some_association->field_name
556      *  dynamically load the requested contents from the database.
557      *  @todo Document this API
558      *  @uses $belongs_to
559      *  @uses get_association_type()
560      *  @uses $has_and_belongs_to_many
561      *  @uses $has_many
562      *  @uses $has_one
563      *  @uses find_all_has_many()
564      *  @uses find_all_habtm()
565      *  @uses find_one_belongs_to()
566      *  @uses find_one_has_one()
567      */
568     function __get($key) {
569         if($association_type = $this->get_association_type($key)) {
570             //error_log("association_type:$association_type");
571             switch($association_type) {
572                 case "has_many":
573                     $parameters = is_array($this->has_many) ? $this->has_many[$key] : null;
574                     $this->$key = $this->find_all_has_many($key, $parameters);
575                     break;
576                 case "has_one":
577                     $parameters = is_array($this->has_one) ? $this->has_one[$key] : null;
578                     $this->$key = $this->find_one_has_one($key, $parameters);
579                     if(is_null($this->$key)) unset($this->$key);                     
580                     break;
581                 case "belongs_to":
582                     $parameters = is_array($this->belongs_to) ? $this->belongs_to[$key] : null;
583                     $this->$key = $this->find_one_belongs_to($key, $parameters);
584                     if(is_null($this->$key)) unset($this->$key);                     
585                     break;
586                 case "has_and_belongs_to_many"
587                     $parameters = is_array($this->has_and_belongs_to_many) ? $this->has_and_belongs_to_many[$key] : null;
588                     $this->$key = $this->find_all_habtm($key, $parameters);
589                     break;           
590             }       
591         } elseif(array_key_exists($key, $this->named_scope) && is_array($this->named_scope[$key])) {
592             $this->$key = $this->find_all($this->named_scope[$key]);
593         } elseif($this->is_composite($key)) {           
594             $composite_object = $this->get_composite_object($key);
595             if(is_object($composite_object)) {
596                 $this->$key = $composite_object;   
597             }                               
598         }
599         //echo "<pre>getting: $key = ".$this->$key."<br></pre>";
600         return $this->$key;
601     }
602
603     /**
604      *  Store column value or description of the table format
605      *
606      *  If called with key 'table_name', $value is stored as the
607      *  description of the table format in $content_columns.
608      *  Any other key causes an object variable with the same name to
609      *  be created and stored into.  If the value of $key matches the
610      *  name of a column in content_columns, the corresponding object
611      *  variable becomes the content of the column in this row.
612      *  @uses $auto_save_associations
613      *  @uses get_association_type()
614      *  @uses set_content_columns()
615      */
616     function __set($key, $value) {
617         //echo "setting: $key = $value<br>";
618         if($key == "table_name") {
619             $this->set_content_columns($value);           
620           # this elseif checks if first its an object if its parent is ActiveRecord
621         } elseif(is_object($value) && get_parent_class($value) == __CLASS__ && $this->auto_save_associations) {
622             if($association_type = $this->get_association_type($key)) {
623                 $this->save_associations[$association_type][] = $value;
624                 if($association_type == "belongs_to") {
625                     $primary_key = $value->primary_keys[0];
626                     $foreign_key = Inflector::singularize($value->table_name)."_".$primary_key;
627                     $this->$foreign_key = $value->$primary_key;
628                 }
629             }
630             # this elseif checks if its an array of objects and if its parent is ActiveRecord               
631         } elseif(is_array($value) && $this->auto_save_associations) {
632             if($association_type = $this->get_association_type($key)) {
633                 $this->save_associations[$association_type][] = $value;
634             }
635         }       
636         
637         //  Assignment to something else, do it
638         $this->$key = $value;
639     }
640
641     /**
642      *  Override call() to dynamically call the database associations
643      *  @todo Document this API
644      *  @uses $aggregations
645      *  @uses aggregate_all()
646      *  @uses get_association_type()
647      *  @uses $belongs_to
648      *  @uses $has_one
649      *  @uses $has_and_belongs_to_many
650      *  @uses $has_many
651      *  @uses find_all_by()
652      *  @uses find_by()
653      */
654     function __call($method_name, $parameters) {
655         if(method_exists($this, $method_name)) {
656             # If the method exists, just call it
657             $result = call_user_func_array(array($this, $method_name), $parameters);
658         } else {
659             # ... otherwise, check to see if the method call is one of our
660             # special Trax methods ...
661             # ... first check for method names that match any of our explicitly
662             # declared associations for this model ( e.g. public $has_many = "movies" ) ...
663             if(is_array($parameters[0])) {
664                 $parameters = $parameters[0];   
665             }
666             $association_type = $this->get_association_type($method_name);
667             switch($association_type) {
668                 case "has_many":
669                     $parameters = is_array($this->has_many) && @array_key_exists($method_name, $this->has_many) ?
670                         array_merge($this->has_many[$method_name], $parameters) : $parameters;
671                     $result = $this->find_all_has_many($method_name, $parameters);
672                     break;
673                 case "has_one":
674                     $parameters = is_array($this->has_one) && @array_key_exists($method_name, $this->has_one) ?
675                         array_merge($this->has_one[$method_name], $parameters) : $parameters;
676                     $result = $this->find_one_has_one($method_name, $parameters);
677                     break;
678                 case "belongs_to":
679                     $parameters = is_array($this->belongs_to) && @array_key_exists($method_name, $this->belongs_to) ?
680                         array_merge($this->belongs_to[$method_name], $parameters) : $parameters;
681                     $result = $this->find_one_belongs_to($method_name, $parameters);
682                     break;
683                 case "has_and_belongs_to_many"
684                     $parameters = is_array($this->has_and_belongs_to_many) && @array_key_exists($method_name, $this->has_and_belongs_to_many) ?
685                         array_merge($this->has_and_belongs_to_many[$method_name], $parameters) : $parameters;
686                     $result = $this->find_all_habtm($method_name, $parameters);
687                     break;           
688             }
689
690             # check for the [count,sum,avg,etc...]_all magic functions
691             if(substr($method_name, -4) == "_all" && in_array(substr($method_name, 0, -4), $this->aggregations)) {
692                 //echo "calling method: $method_name<br>";
693                 $result = $this->aggregate_all($method_name, $parameters);
694             }
695             # check for the named scopes being called as a function
696             elseif(array_key_exists($method_name, $this->named_scope) && is_array($this->named_scope[$method_name])) {
697                 $result = $this->find_all(array_merge($this->named_scope[$method_name], (array)$parameters));
698             }           
699             # check for the find_all_by_* magic functions
700             elseif(strlen($method_name) > 11 && substr($method_name, 0, 11) == "find_all_by") {
701                 //echo "calling method: $method_name<br>";
702                 $result = $this->find_by($method_name, $parameters, "all");
703             }
704             # check for the find_by_* magic functions
705             elseif(strlen($method_name) > 7 && substr($method_name, 0, 7) == "find_by") {
706                 //echo "calling method: $method_name<br>";
707                 $result = $this->find_by($method_name, $parameters);
708             }
709             # check for find_or_create_by_* magic functions
710             elseif(strlen($method_name) > 17 && substr($method_name, 0, 17) == "find_or_create_by") {
711                 $result = $this->find_by($method_name, $parameters, "find_or_create");       
712             }
713         }
714         return $result;
715     }
716     
717     /**
718      *  Find all records using a "has_and_belongs_to_many" relationship
719      * (many-to-many with a join table in between).  Note that you can also
720      *  specify an optional "paging limit" by setting the corresponding "limit"
721      *  instance variable.  For example, if you want to return 10 movies from the
722      *  5th movie on, you could set $this->movies_limit = "10, 5"
723      *
724      *  Parameters: $this_table_name:  The name of the database table that has the
725      *                                 one row you are interested in.  E.g. genres
726      *              $other_table_name: The name of the database table that has the
727      *                                 many rows you are interested in.  E.g. movies
728      *  Returns: An array of ActiveRecord objects. (e.g. Movie objects)
729      *  @todo Document this API
730      */
731     private function find_all_habtm($other_table_name, $parameters = null) {
732         $additional_conditions = $additional_joins = null;
733         $options = array();
734         # Use any passed-in parameters
735         if(!is_null($parameters)) { 
736             if(@array_key_exists("conditions", $parameters)) {
737                 $additional_conditions = " AND (".$parameters['conditions'].")";
738             } elseif($parameters[0] != "") {
739                 $additional_conditions = " AND (".$parameters[0].")";
740             }
741             if(@array_key_exists("order", $parameters)) {
742                 $options['order'] = $parameters['order'];
743             } elseif($parameters[1] != "") {
744                 $options['order'] = $parameters[1];
745             }
746             if(@array_key_exists("limit", $parameters)) {
747                 $options['limit'] = $parameters['limit'];
748             } elseif($parameters[2] != "") {
749                 $options['limit'] = $parameters[2];
750             }
751             if(@array_key_exists("joins", $parameters)) {
752                 $additional_joins = $parameters['joins'];
753             } elseif($parameters[3] != "") {
754                 $additional_joins = $parameters[3];
755             }
756             if(@array_key_exists("page", $parameters)) {
757                 $options['page'] = $parameters['page'];
758             }
759             if(@array_key_exists("per_page", $parameters)) {
760                 $options['per_page'] = $parameters['per_page'];
761             }
762             if(@array_key_exists("class_name", $parameters)) {
763                 $other_object_name = $parameters['class_name'];
764             }           
765             if(@array_key_exists("join_table", $parameters)) {
766                 $join_table = $parameters['join_table'];
767             }
768             if(@array_key_exists("foreign_key", $parameters)) {
769                 $this_foreign_key = $parameters['foreign_key'];
770             }
771             if(@array_key_exists("association_foreign_key", $parameters)) {
772                 $other_foreign_key = $parameters['association_foreign_key'];
773             }           
774             if(@array_key_exists("finder_sql", $parameters)) {
775                 $finder_sql = $parameters['finder_sql'];
776             }   
777         }
778         
779         if(!is_null($other_object_name)) {
780             $other_class_name = Inflector::camelize($other_object_name);   
781             $other_table_name = Inflector::tableize($other_object_name);   
782         } else {
783             $other_class_name = Inflector::classify($other_table_name);
784         }
785         
786         # Instantiate an object to access find_all
787         $other_class_object = new $other_class_name();
788
789         # If finder_sql is specified just use it instead of determining the joins/sql
790         if(!is_null($finder_sql)) {
791             $conditions = $finder_sql;   
792         } else {
793             # Prepare the join table name primary keys (fields) to do the join on
794             if(is_null($join_table)) {
795                 $join_table = $this->get_join_table_name($this->table_name, $other_table_name);
796             }
797             
798             # Primary keys
799             $this_primary_key  = $this->primary_keys[0];
800             $other_primary_key = $other_class_object->primary_keys[0];
801             
802             # Foreign keys
803             if(is_null($this_foreign_key)) {
804                 $this_foreign_key = Inflector::singularize($this->table_name)."_".$this_primary_key;
805             }
806             if(is_null($other_foreign_key)) {
807                 $other_foreign_key = Inflector::singularize($other_table_name)."_".$other_primary_key;
808             }
809             
810             # Primary key value
811             if($this->attribute_is_string($this_primary_key)) {
812                 $this_primary_key_value = "'".$this->$this_primary_key."'";                   
813             } elseif(is_numeric($this->$this_primary_key)) {
814                 $this_primary_key_value = $this->$this_primary_key;
815             } else {
816                 #$this_primary_key_value = 0;
817                 # no primary key value so just return empty array same as find_all()
818                 return array();
819             }
820
821             if($this->habtm_sort_field) {
822                 $options['order'] = (isset($options['order']) ? $options['order'].',':'')."{$join_table}.{$this->habtm_sort_field}";
823             }
824             
825             # Set up the SQL segments
826             $conditions = "{$join_table}.{$this_foreign_key} = {$this_primary_key_value}".$additional_conditions;
827             $options['joins'] = "LEFT JOIN {$join_table} ON {$other_table_name}.{$other_primary_key} = {$join_table}.{$other_foreign_key}".$additional_joins;
828         }
829         $options['conditions'] = $conditions;
830         
831         # Get the list of other_class_name objects
832         return $other_class_object->find_all($options);
833     }
834
835     /**
836      *  Find all records using a "has_many" relationship (one-to-many)
837      *
838      *  Parameters: $other_table_name: The name of the other table that contains
839      *                                 many rows relating to this object's id.
840      *  Returns: An array of ActiveRecord objects. (e.g. Contact objects)
841      *  @todo Document this API
842      */
843     private function find_all_has_many($other_table_name, $parameters = null) {
844         $additional_conditions = $order = $limit = null;
845         # Use any passed-in parameters
846         if(is_array($parameters)) {
847             if(@array_key_exists("conditions", $parameters)) {
848                 $additional_conditions = " AND (".$parameters['conditions'].")";
849             } elseif($parameters[0] != "") {
850                 $additional_conditions = " AND (".$parameters[0].")";
851             }
852             if(@array_key_exists("order", $parameters)) {
853                 $options['order'] = $parameters['order'];
854             } elseif($parameters[1] != "") {
855                 $options['order'] = $parameters[1];
856             }
857             if(@array_key_exists("limit", $parameters)) {
858                 $options['limit'] = $parameters['limit'];
859             } elseif($parameters[2] != "") {
860                 $options['limit'] = $parameters[2];
861             }
862             if(@array_key_exists("joins", $parameters)) {
863                 $options['joins'] = $parameters['joins'];
864             } elseif($parameters[3] != "") {
865                 $options['joins'] = $parameters[3];
866             }
867             if(@array_key_exists("page", $parameters)) {
868                 $options['page'] = $parameters['page'];
869             }
870             if(@array_key_exists("per_page", $parameters)) {
871                 $options['per_page'] = $parameters['per_page'];
872             }
873             if(@array_key_exists("foreign_key", $parameters)) {
874                 $foreign_key = $parameters['foreign_key'];
875             }   
876             if(@array_key_exists("primary_key", $parameters)) {
877                 $this_primary_key = $parameters['primary_key'];
878             }                     
879             if(@array_key_exists("class_name", $parameters)) {
880                 $other_object_name = $parameters['class_name'];
881             } 
882             if(@array_key_exists("finder_sql", $parameters)) {
883                 $finder_sql = $parameters['finder_sql'];
884             }           
885         }
886
887         if(!is_null($other_object_name)) {
888             $other_class_name = Inflector::camelize($other_object_name);   
889         } else {
890             $other_class_name = Inflector::classify($other_table_name);
891         }
892
893         # Instantiate an object to access find_all
894         $other_class_object = new $other_class_name();
895         
896         # If finder_sql is specified just use it instead of determining the association
897         if(!is_null($finder_sql)) {
898             $conditions = $finder_sql
899         } else {         
900             # This class primary key
901             if(!$this_primary_key) {
902                 $this_primary_key = $this->primary_keys[0];
903             }
904                 
905             if(!$foreign_key) {
906                 # this should end up being like user_id or account_id but if you specified
907                 # a primaray key other than 'id' it will be like user_field
908                 $foreign_key = Inflector::singularize($this->table_name)."_".$this_primary_key;
909             }
910             
911             $foreign_key_value = $this->$this_primary_key;
912             if($other_class_object->attribute_is_string($foreign_key)) {
913                 $conditions = "{$foreign_key} = '{$foreign_key_value}'";                   
914             } elseif(is_numeric($foreign_key_value)) {
915                 $conditions = "{$foreign_key} = {$foreign_key_value}";
916             } else {
917                 #$conditions = "{$foreign_key} = 0";
918                 # no primary key value so just return empty array same as find_all()
919                 return array();               
920             }           
921             $conditions .= $additional_conditions;
922         }
923         $options['conditions'] = $conditions;
924         #error_log("has_many:".print_r($options, true));
925         # Get the list of other_class_name objects
926         return $other_class_object->find_all($options);
927     }
928
929     /**
930      *  Find all records using a "has_one" relationship (one-to-one)
931      *  (the foreign key being in the other table)
932      *  Parameters: $other_table_name: The name of the other table that contains
933      *                                 many rows relating to this object's id.
934      *  Returns: An array of ActiveRecord objects. (e.g. Contact objects)
935      *  @todo Document this API
936      */
937     private function find_one_has_one($other_object_name, $parameters = null) {       
938         $additional_conditions = null;
939         # Use any passed-in parameters
940         if(is_array($parameters)) {
941             //echo "<pre>";print_r($parameters);
942             if(@array_key_exists("conditions", $parameters)) {
943                 $additional_conditions = " AND (".$parameters['conditions'].")";
944             } elseif($parameters[0] != "") {
945                 $additional_conditions = " AND (".$parameters[0].")";
946             }
947             if(@array_key_exists("order", $parameters)) {
948                 $order = $parameters['order'];
949             } elseif($parameters[1] != "") {
950                 $order = $parameters[1];
951             }
952             if(@array_key_exists("foreign_key", $parameters)) {
953                 $foreign_key = $parameters['foreign_key'];
954             }
955             if(@array_key_exists("primary_key", $parameters)) {
956                 $this_primary_key = $parameters['primary_key'];
957             }                     
958             if(@array_key_exists("class_name", $parameters)) {
959                 $other_object_name = $parameters['class_name'];
960             } 
961         }
962         
963         $other_class_name = Inflector::camelize($other_object_name);
964         
965         # Instantiate an object to access find_all
966         $other_class_object = new $other_class_name();
967
968         # This class primary key
969         if(!$this_primary_key) {
970             $this_primary_key = $this->primary_keys[0];
971         }
972         
973         if(!$foreign_key) {
974             $foreign_key = Inflector::singularize($this->table_name)."_".$this_primary_key;
975         }
976
977         $foreign_key_value = $this->$this_primary_key;
978         if($other_class_object->attribute_is_string($foreign_key)) {
979             $conditions = "{$foreign_key} = '{$foreign_key_value}'";                   
980         } elseif(is_numeric($foreign_key_value)) {
981             $conditions = "{$foreign_key} = {$foreign_key_value}";
982         } else {
983             #$conditions = "{$foreign_key} = 0";
984             return null;
985         }
986
987         $conditions .= $additional_conditions;
988         
989         # Get the list of other_class_name objects
990         return $other_class_object->find_first($conditions, $order);
991     }
992
993     /**
994      *  Find all records using a "belongs_to" relationship (one-to-one)
995      *  (the foreign key being in the table itself)
996      *  Parameters: $other_object_name: The singularized version of a table name.
997      *                                  E.g. If the Contact class belongs_to the
998      *                                  Customer class, then $other_object_name
999      *                                  will be "customer".
1000      *  @todo Document this API
1001      */
1002     private function find_one_belongs_to($other_object_name, $parameters = null) {
1003
1004         $additional_conditions = null;
1005         # Use any passed-in parameters
1006         if(is_array($parameters)) {
1007             //echo "<pre>";print_r($parameters);
1008             if(@array_key_exists("conditions", $parameters)) {
1009                 $additional_conditions = " AND (".$parameters['conditions'].")";
1010             } elseif($parameters[0] != "") {
1011                 $additional_conditions = " AND (".$parameters[0].")";
1012             }
1013             if(@array_key_exists("order", $parameters)) {
1014                 $order = $parameters['order'];
1015             } elseif($parameters[1] != "") {
1016                 $order = $parameters[1];
1017             }
1018             if(@array_key_exists("foreign_key", $parameters)) {
1019                 $foreign_key = $parameters['foreign_key'];
1020             }         
1021             if(@array_key_exists("class_name", $parameters)) {
1022                 $other_object_name = $parameters['class_name'];
1023             } 
1024         }
1025         
1026         $other_class_name = Inflector::camelize($other_object_name);
1027     
1028         # Instantiate an object to access find_all
1029         $other_class_object = new $other_class_name();
1030
1031         # This class primary key
1032         $other_primary_key = $other_class_object->primary_keys[0];
1033
1034         if(!$foreign_key) {
1035             $foreign_key = $other_object_name."_".$other_primary_key;
1036         }
1037         
1038         $other_primary_key_value = $this->$foreign_key;
1039         if($other_class_object->attribute_is_string($other_primary_key)) {
1040             $conditions = "{$other_primary_key} = '{$other_primary_key_value}'";                   
1041         } elseif(is_numeric($other_primary_key_value)) {
1042             $conditions = "{$other_primary_key} = {$other_primary_key_value}";
1043         } else {
1044             #$conditions = "{$other_primary_key} = 0";
1045             return null;
1046         }
1047         $conditions .= $additional_conditions;
1048         
1049         # Get the list of other_class_name objects
1050         return $other_class_object->find_first($conditions, $order);
1051     }
1052
1053     /**
1054      *  Implement *_all() functions (SQL aggregate functions)
1055      *
1056      *  Apply one of the SQL aggregate functions to a column of the
1057      *  table associated with this object.  The SQL aggregate
1058      *  functions are AVG, COUNT, MAX, MIN and SUM.  Not all DBMS's
1059      *  implement all of these functions.
1060      *  @param string $agrregrate_type SQL aggregate function to
1061      *    apply, suffixed '_all'.  The aggregate function is one of
1062      *  the strings in {@link $aggregations}.
1063      *  @param string[] $parameters  Conditions to apply to the
1064      *    aggregate function.  If present, must be an array of three
1065      *    strings:<ol>
1066      *     <li>$parameters[0]: If present, expression to apply
1067      *       the aggregate function to.  Otherwise, '*' will be used.
1068      *       <b>NOTE:</b>SQL uses '*' only for the COUNT() function,
1069      *       where it means "including rows with NULL in this column".</li>
1070      *     <li>$parameters[1]: argument to WHERE clause</li>
1071      *     <li>$parameters[2]: joins??? @todo Document this parameter</li>
1072      *    </ol>
1073      *  @throws {@link ActiveRecordError}
1074      *  @uses query()
1075      *  @uses is_error()
1076      */
1077     private function aggregate_all($aggregate_type, $parameters = null) {
1078         $aggregate_type = strtoupper(substr($aggregate_type, 0, -4));
1079         $distinct = strtolower($aggregate_type) == 'count' ? 'DISTINCT ' : '';
1080         #($parameters[0]) ? $field = $parameters[0] : $field = "*";
1081         $field = (stristr($parameters[0], ".") ? $parameters[0] : "{$this->table_prefix}{$this->table_name}.".$parameters[0]);
1082         $sql = "SELECT {$aggregate_type}({$distinct}{$field}) AS agg_result FROM {$this->table_prefix}{$this->table_name} ";       
1083         # Use any passed-in parameters
1084         if(is_array($parameters[1])) {
1085             extract($parameters[1]);   
1086         } elseif(!is_null($parameters)) {
1087             $conditions = $parameters[1];
1088             $joins = $parameters[2];
1089         }
1090         if(!empty($joins)) $sql .= " $joins ";
1091         if(!empty($conditions)) $sql .= " WHERE $conditions ";
1092         # echo "$aggregate_type sql:$sql<br>";
1093         //print_r($parameters[0]);
1094         //echo $sql;
1095         $rs = $this->query($sql, true);
1096         $row = $rs->fetchRow();
1097         if($row["agg_result"]) {
1098             return $row["agg_result"];   
1099         }
1100         return 0;
1101     }
1102
1103     /**
1104      *  Returns a the name of the join table that would be used for the two
1105      *  tables.  The join table name is decided from the alphabetical order
1106      *  of the two tables.  e.g. "genres_movies" because "g" comes before "m"
1107      *
1108      *  Parameters: $first_table, $second_table: the names of two database tables,
1109      *   e.g. "movies" and "genres"
1110      *  @todo Document this API
1111      */
1112     public function get_join_table_name($first_table, $second_table) {
1113         $tables = array($first_table, $second_table);
1114         @sort($tables);
1115         return $this->table_prefix.@implode("_", $tables);
1116     }
1117
1118     /**
1119      *  Test whether this object represents a new record
1120      *  @uses $new_record
1121      *  @return boolean Whether this object represents a new record
1122      */
1123    function is_new_record() {
1124         return $this->new_record;
1125     }
1126
1127    /**
1128     *  get the attributes for a specific column.
1129     *  @uses $content_columns
1130     *  @todo Document this API
1131     */
1132     function column_for_attribute($attribute) {
1133         if(is_array($this->content_columns)) {
1134             foreach($this->content_columns as $column) {
1135                 if($column['name'] == $attribute) {
1136                     return $column;
1137                 }
1138             }
1139         }
1140         return null;
1141     }
1142
1143    /**
1144     *  get the columns  data type.
1145     *  @uses column_for_attribute()
1146     *  @todo Document this API
1147     */   
1148     function column_type($attribute) {
1149         $column = $this->column_for_attribute($attribute);
1150         if(isset($column['type'])) {
1151             return $column['type'];   
1152         }           
1153         return null;
1154     }
1155     
1156     /**
1157      *  Check whether a column exists in the associated table
1158      *
1159      *  When called, {@link $content_columns} lists the columns in
1160      *  the table described by this object.
1161      *  @param string Name of the column
1162      *  @return boolean true=>the column exists; false=>it doesn't
1163      *  @uses content_columns
1164      */
1165     function column_attribute_exists($attribute) {
1166         if(is_array($this->content_columns)) {
1167             foreach($this->content_columns as $column) {
1168                 if($column['name'] == $attribute) {
1169                     return true;
1170                 }
1171             }
1172         }
1173         return false;     
1174     }
1175
1176     /**
1177      *  Get contents of one column of record selected by id and table
1178      *
1179      *  When called, {@link $id} identifies one record in the table
1180      *  identified by {@link $table}.  Fetch from the database the
1181      *  contents of column $column of this record.
1182      *  @param string Name of column to retrieve
1183      *  @uses $db
1184      *  @uses column_attribute_exists()
1185      *  @throws {@link ActiveRecordError}
1186      *  @uses is_error()
1187      */
1188     function send($column) {
1189         if($this->column_attribute_exists($column) && ($conditions = $this->get_primary_key_conditions())) {
1190             # Run the query to grab a specific columns value.
1191             $sql = "SELECT {$column} FROM {$this->table_prefix}{$this->table_name} WHERE {$conditions} LIMIT 1";
1192             $this->log_query($sql);
1193             $db =& $this->get_connection(true);
1194             $result = $db->queryOne($sql);
1195             if($this->is_error($result)) {
1196                 $this->raise($result->getMessage());
1197             }
1198         }
1199         return $result;
1200     }
1201
1202     /**
1203      * Only used if you want to do transactions and your db supports transactions
1204      *
1205      *  @uses $db
1206      *  @todo Document this API
1207      */
1208     function begin() {
1209         # check if transaction are supported by this driver
1210         if(self::$db->supports('transactions')) {       
1211             $rs = self::$db->beginTransaction();
1212             if($this->is_error($rs)) {
1213                 $this->raise($rs->getMessage());
1214             }     
1215             self::$in_transaction = true;
1216         }
1217     }
1218
1219     /**
1220      * Only used if you want to do transactions and your db supports transactions
1221      *
1222      *  @uses $db
1223      *  @todo Document this API
1224      */   
1225     function save_point($save_point) {
1226         if(!is_null($save_point)) {
1227             # check if transaction are supported by this driver
1228             if(self::$db->supports('transactions')) {           
1229                 # check if we are inside a transaction and if savepoints are supported
1230                 if(self::$db->inTransaction() && self::$db->supports('savepoints')) {
1231                     # Set a savepoint
1232                     $rs = self::$db->beginTransaction($save_point);
1233                     if($this->is_error($rs)) {
1234                         $this->raise($rs->getMessage());
1235                     }               
1236                 }
1237             }         
1238         }       
1239     }
1240
1241     /**
1242      *  Only used if you want to do transactions and your db supports transactions
1243      *
1244      *  @uses $db
1245      *  @todo Document this API
1246      */
1247     function commit() {     
1248         # check if transaction are supported by this driver
1249         if(self::$db->supports('transactions')) {
1250             # check if we are inside a transaction
1251             if(self::$db->inTransaction()) {
1252                 $rs = self::$db->commit();
1253                 if($this->is_error($rs)) {
1254                     $this->raise($rs->getMessage());
1255                 }       
1256                 self::$in_transaction = false;
1257             }
1258         }
1259     }
1260
1261     /**
1262      *  Only used if you want to do transactions and your db supports transactions
1263      *
1264      *  @uses $db
1265      *  @todo Document this API
1266      */   
1267     function rollback() {
1268         # check if transaction are supported by this driver
1269         if(self::$db->supports('transactions')) {
1270             $rs = self::$db->rollback();
1271             if($this->is_error($rs)) {
1272                 $this->raise($rs->getMessage());
1273             }
1274             self::$in_transaction = false;
1275         }               
1276     }
1277
1278     /**
1279      *  Perform an SQL query and return the results
1280      *
1281      *  @param string $sql  SQL for the query command
1282      *  @return $mdb2->query {@link http://pear.php.net/manual/en/package.database.mdb2.intro-query.php}
1283      *    Result set from query
1284      *  @uses $db
1285      *  @uses is_error()
1286      *  @uses log_query()
1287      *  @throws {@link ActiveRecordError}
1288      */
1289     function query($sql, $read_only = false) {
1290         # Run the query
1291         $this->log_query($sql);
1292         $db =& $this->get_connection($read_only);
1293         $rs =& $db->query($sql);
1294         if ($this->is_error($rs)) {
1295             if(self::$auto_rollback && self::$in_transaction) {
1296                 $this->rollback();
1297             }
1298             $this->raise($rs->getMessage());
1299         }
1300         return $rs;
1301     }
1302
1303     /**
1304      *  Implement find_by_*() and =_* methods
1305      * 
1306      *  Converts a method name beginning 'find_by_' or 'find_all_by_'
1307      *  into a query for rows matching the rest of the method name and
1308      *  the arguments to the function.  The part of the method name
1309      *  after '_by' is parsed for columns and logical relationships
1310      *  (AND and OR) to match.  For example, the call
1311      *    find_by_fname('Ben')
1312      *  is converted to
1313      *    SELECT * ... WHERE fname='Ben'
1314      *  and the call
1315      *    find_by_fname_and_lname('Ben','Dover')
1316      *  is converted to
1317      *    SELECT * ... WHERE fname='Ben' AND lname='Dover'
1318      * 
1319      *  @uses find_all()
1320      *  @uses find_first()
1321      */
1322     private function find_by($method_name, $parameters, $find_type = null) {
1323         if($find_type == "find_or_create") {
1324             $explode_len = 18;
1325         } elseif($find_type == "all") {
1326             $explode_len = 12;
1327         } else {
1328             $explode_len = 8;     
1329         }
1330         $method_name = substr(strtolower($method_name), $explode_len);
1331         $method_parts = explode("|", str_replace("_and_", "|AND|", $method_name));
1332         if(count($method_parts)) {
1333             $conditions = null;
1334             $options = array();
1335             $create_fields = array();
1336             $param_index = 0;
1337             foreach($method_parts as $part) {
1338                 if($part == "AND") {
1339                     $conditions .= " AND ";
1340                     $param_index++;
1341                 } else {
1342                     $value = $this->attribute_is_string($part) ?
1343                         "'".$parameters[$param_index]."'" :
1344                         $parameters[$param_index];                   
1345                     $create_fields[$part] = $parameters[$param_index]; 
1346                     $conditions .= "{$part} = {$value}";
1347                 }
1348             }
1349             # If last param exists and is a string set it as the ORDER BY clause           
1350             # or if the last param is an array set it as the $options
1351             ++$param_index;
1352             if(isset($parameters[$param_index]) && ($last_param = $parameters[$param_index])) {
1353                 if(is_string($last_param)) {
1354                     $options['order'] = $last_param;       
1355                 } elseif(is_array($last_param)) {
1356                     $options = $last_param;   
1357                 }
1358             } 
1359             # Set the conditions
1360             if(isset($options['conditions']) && $conditions) {
1361                 $options['conditions'] = "(".$options['conditions'].") AND (".$conditions.")";   
1362             } else {
1363                 $options['conditions'] = $conditions;   
1364             }
1365
1366             # Now do the actual find with condtions from above
1367             if($find_type == "find_or_create") {
1368                 # see if we can find a record with specified parameters
1369                 $object = $this->find($options);
1370                 if(is_object($object)) {
1371                     # we found a record with the specified parameters so return it
1372                     return $object;   
1373                 } elseif(count($create_fields)) {
1374                     # can't find a record with specified parameters so create a new record
1375                     # and return new object       
1376                     foreach($create_fields as $field => $value) {
1377                         $this->$field = $value;   
1378                     }
1379                     $this->save();
1380                     return $this->find($options);
1381                 }
1382             } elseif($find_type == "all") {
1383                 return $this->find_all($options);
1384             } else {
1385                 return $this->find($options);
1386             }
1387         }
1388     }
1389
1390     /**
1391      *  Builds a sql statement.
1392      * 
1393      *  @uses $rows_per_page_default
1394      *  @uses $rows_per_page
1395      *  @uses $offset
1396      *  @uses $page
1397      *
1398      */
1399     function build_sql($conditions = null, $order = null, $limit = null, $joins = null) {
1400         
1401         $offset = null;
1402         $page = null;
1403         $per_page = null;
1404         $select = null;
1405         $paginate = false;
1406
1407         # this is if they passed in an associative array to emulate
1408         # named parameters.
1409         if(is_array($conditions)) {
1410             if(@array_key_exists("per_page", $conditions) && !is_numeric($conditions['per_page'])) {
1411                 extract($conditions);
1412                 $per_page = 0;   
1413             } else {
1414                 extract($conditions);     
1415             }
1416             # If conditions wasn't in the array set it to null
1417             if(is_array($conditions)) {
1418                 $conditions = null;   
1419             } 
1420         }
1421
1422         # Test source of SQL for query
1423         if(stristr($conditions, "SELECT")) {
1424             # SQL completely specified in argument so use it as is
1425             $sql = $conditions;
1426         } else {
1427
1428             # If select fields not specified just do a SELECT *
1429             if(is_null($select)) {
1430                 $select = "*";
1431             }
1432
1433             # SQL will be built from specifications in argument
1434             $sql  = "SELECT {$select} FROM {$this->table_prefix}{$this->table_name} ";         
1435             
1436             # If join specified, include it
1437             if(!is_null($joins)) {
1438                 $sql .= " $joins ";
1439             }
1440
1441             # If conditions specified, include them
1442             if(!is_null($conditions)) {
1443                 if(array_key_exists('conditions', $this->default_scope)
1444                    && !is_null($this->default_scope['conditions'])) {
1445                     $conditions = " ({$conditions}) AND (".$this->default_scope['conditions'].") ";
1446                 }
1447                 $sql .= "WHERE $conditions ";
1448             } elseif(array_key_exists('conditions', $this->default_scope)
1449                      && !is_null($this->default_scope['conditions'])) {
1450                 $sql .= "WHERE ".$this->default_scope['conditions']." ";
1451             }
1452             
1453             # If GROUP BY was specified
1454             if(!is_null($group)) {
1455                 $sql .= "GROUP BY {$group} ";
1456             }
1457             
1458             # If HAVING clause is specified
1459             if(!is_null($having)) {
1460                 $sql .= "HAVING {$having} ";
1461             }
1462
1463             # If ordering specified, include it
1464             if(!is_null($order)) {
1465                 if(array_key_exists('order', $this->default_scope)
1466                    && !is_null($this->default_scope['order'])) {
1467                     $order = " {$order},".$this->default_scope['order']." ";
1468                 }               
1469                 $sql .= "ORDER BY $order ";
1470             } elseif(array_key_exists('order', $this->default_scope)
1471                      && !is_null($this->default_scope['order'])) {
1472                 $sql .= "ORDER BY ".$this->default_scope['order']." ";
1473             }
1474
1475             # Is output to be generated in pages?
1476             if(is_numeric($limit) || is_numeric($offset) || is_numeric($per_page) || is_numeric($page)) {
1477                 #error_log("limit:$limit offset:$offset per_page:$per_page page:$page");
1478
1479                 if(is_numeric($limit)) {   
1480                     $this->rows_per_page = (int)$limit;       
1481                 }
1482                 if(is_numeric($per_page)) {
1483                     $this->rows_per_page = (int)$per_page;
1484                        $paginate = true;
1485                 }
1486                 # Default for rows_per_page:
1487                 if ($this->rows_per_page <= 0) {
1488                     $this->rows_per_page = (int)self::$rows_per_page_default;
1489                 }
1490                 
1491                 # Only use request's page if you are calling from find_all_with_pagination() and if it is int
1492                 #if(isset($_REQUEST['page']) && strval(intval($_REQUEST['page'])) == $_REQUEST['page']) {
1493                     #$this->page = $_REQUEST['page'];
1494                 #}
1495                 if(!is_null($page)) {
1496                     $this->page = (int)$page;
1497                     $paginate = true;
1498                 }
1499                 
1500                 if($this->page <= 0) {
1501                     $this->page = 1;
1502                 }
1503                                 
1504                 # Set the LIMIT string segment for the SQL
1505                 if(is_null($offset)) {
1506                     $offset = ($this->page - 1) * $this->rows_per_page;
1507                 }
1508
1509                 $sql .= "LIMIT {$this->rows_per_page} OFFSET {$offset}";
1510                 # $sql .= "LIMIT $offset, $this->rows_per_page";
1511                 
1512                 if($paginate) {
1513                     #error_log("I am going to paginate.");
1514                     # Set number of total pages in result set
1515                     if($count = $this->count_all($this->primary_keys[0], $conditions, $joins)) {
1516                         $this->pagination_count = $count;
1517                         $this->pages = (($count % $this->rows_per_page) == 0)
1518                             ? $count / $this->rows_per_page
1519                             : floor($count / $this->rows_per_page) + 1;
1520                     }
1521                 }
1522             }
1523         }
1524         
1525         return $sql;
1526     }
1527     
1528     /**
1529      *  Returns same as find_all
1530      *
1531      */
1532     function paginate($page = 1, $per_page = 0, $options = array()) {
1533         if(is_array($page)) {
1534             $options = $page;
1535         } else {
1536             $options['page'] = (int)($page > 0 ? $page : 1);
1537             $options['per_page'] = (int)($per_page > 0 ? $per_page : self::$rows_per_page_default);
1538         }
1539         $options['paginate'] = true;
1540         return $this->find_all($options);
1541     }   
1542
1543     /**
1544      *  Return rows selected by $conditions
1545      *
1546      *  If no rows match, an empty array is returned.
1547      *  @param string SQL to use in the query.  If
1548      *    $conditions contains "SELECT", then $order, $limit and
1549      *    $joins are ignored and the query is completely specified by
1550      *    $conditions.  If $conditions is omitted or does not contain
1551      *    "SELECT", "SELECT * FROM" will be used.  If $conditions is
1552      *    specified and does not contain "SELECT", the query will
1553      *    include "WHERE $conditions".  If $conditions is null, the
1554      *    entire table is returned.
1555      *  @param string Argument to "ORDER BY" in query.
1556      *    If specified, the query will include
1557      *    "ORDER BY $order". If omitted, no ordering will be
1558      *    applied. 
1559      *  @param integer[] Page, rows per page???
1560      *  @param string ???
1561      *  @todo Document the $limit and $joins parameters
1562      *  @uses is_error()
1563      *  @uses $new_record
1564      *  @uses query()
1565      *  @return object[] Array of objects of the same class as this
1566      *    object, one object for each row returned by the query.
1567      *    If the column 'id' was in the results, it is used as the key
1568      *    for that object in the array.
1569      *  @throws {@link ActiveRecordError}
1570      */
1571     function find_all($conditions = null, $order = null, $limit = null, $joins = null) {
1572         //error_log("find_all(".(is_null($conditions)?'null':$conditions)
1573         //          .', ' . (is_null($order)?'null':$order)
1574         //          .', ' . (is_null($limit)?'null':var_export($limit,true))
1575         //          .', ' . (is_null($joins)?'null':$joins).')');
1576
1577         # Placed the sql building code in a separate function
1578         $sql = $this->build_sql($conditions, $order, $limit, $joins);
1579
1580         # echo "ActiveRecord::find_all() - sql: $sql\n<br>";
1581         # echo "query: $sql\n";
1582         # error_log("ActiveRecord::find_all -> $sql");
1583         $rs = $this->query($sql, true);
1584         
1585         $objects = array();
1586         $class_name = $this->get_class_name();
1587         while($row = $rs->fetchRow()) {   
1588             $object = new $class_name();
1589             $object->new_record = false;
1590             $objects_key = null;
1591             foreach($row as $field => $value) {
1592                 $object->$field = $value;
1593                 if($field == $this->index_on) {
1594                     $objects_key = $value;
1595                 }
1596             }
1597             if(is_null($objects_key)) {
1598                 $objects[] = $object;
1599             } else {
1600                 $objects[$objects_key] = $object;   
1601             }
1602             # If callback is defined in model run it.
1603             # this will probably hurt performance...
1604             if(method_exists($object, 'after_find')) {
1605                 $object->after_find();   
1606             }
1607             unset($object);
1608         }
1609         return $objects;
1610     }
1611
1612     /**
1613      *  Find row(s) with specified value(s)
1614      *
1615      *  Find all the rows in the table which match the argument $id.
1616      *  Return zero or more objects of the same class as this
1617      *  class representing the rows that matched the argument.
1618      *  @param mixed[] $id  If $id is an array then a query will be
1619      *    generated selecting all of the array values in column "id".
1620      *    If $id is a string containing "=" then the string value of
1621      *    $id will be inserted in a WHERE clause in the query.  If $id
1622      *    is a scalar not containing "=" then a query will be generated
1623      *    selecting the first row WHERE id = '$id'.
1624      *    <b>NOTE</b> The column name "id" is used regardless of the
1625      *    value of {@link $primary_keys}.  Therefore if you need to
1626      *    select based on some column other than "id", you must pass a
1627      *    string argument ready to insert in the SQL SELECT.
1628      *  @param string $order Argument to "ORDER BY" in query.
1629      *    If specified, the query will include "ORDER BY
1630      *    $order". If omitted, no ordering will be applied.
1631      *  @param integer[] $limit Page, rows per page???
1632      *  @param string $joins ???
1633      *  @todo Document the $limit and $joins parameters
1634      *  @uses find_all()
1635      *  @uses find_first()
1636      *  @return mixed Results of query.  If $id was a scalar then the
1637      *    result is an object of the same class as this class and
1638      *    matching $id conditions, or if no row matched the result is
1639      *    null.
1640      *
1641      *    If $id was an array then the result is an array containing
1642      *    objects of the same class as this class and matching the
1643      *    conditions set by $id.  If no rows matched, the array is
1644      *    empty.
1645      *  @throws {@link ActiveRecordError}
1646      */
1647     function find($id, $order = null, $limit = null, $joins = null) {
1648         $find_all = false;
1649         if(is_array($id)) {
1650             if($id[0]) {
1651                 # passed in array of numbers array(1,2,4,23)
1652                 $primary_key = $this->primary_keys[0];
1653                 $primary_key_values = $this->attribute_is_string($primary_key) ?
1654                     "'".implode("','", $id)."'" :
1655                     implode(",", $id);
1656                 $options['conditions'] = "{$primary_key} IN({$primary_key_values})";
1657                 $find_all = true;
1658             } else {
1659                 # passed in an options array
1660                 $options = $id;   
1661             }
1662         } elseif(stristr($id, "=")) {
1663             # has an "=" so must be a WHERE clause
1664             $options['conditions'] = $id;
1665         } else {
1666             # find an single record with id = $id
1667             $primary_key = $this->primary_keys[0];
1668             $primary_key_value = $this->attribute_is_string($primary_key) ? "'".$id."'" : $id ;
1669             $options['conditions'] = "{$primary_key} = {$primary_key_value}";
1670         }
1671         if(!is_null($order)) $options['order'] = $order;
1672         if(!is_null($limit)) $options['limit'] = $limit;
1673         if(!is_null($joins)) $options['joins'] = $joins;
1674
1675
1676         if($find_all) {
1677             return $this->find_all($options);
1678         } else {
1679             return $this->find_first($options);
1680         }
1681     }
1682
1683     /**
1684      *  Return first row selected by $conditions
1685      *
1686      *  If no rows match, null is returned.
1687      *  @param string $conditions SQL to use in the query.  If
1688      *    $conditions contains "SELECT", then $order, $limit and
1689      *    $joins are ignored and the query is completely specified by
1690      *    $conditions.  If $conditions is omitted or does not contain
1691      *    "SELECT", "SELECT * FROM" will be used.  If $conditions is
1692      *    specified and does not contain "SELECT", the query will
1693      *    include "WHERE $conditions".  If $conditions is null, the
1694      *    entire table is returned.
1695      *  @param string $order Argument to "ORDER BY" in query.
1696      *    If specified, the query will include
1697      *    "ORDER BY $order". If omitted, no ordering will be
1698      *    applied. 
1699      *  FIXME This parameter doesn't seem to make sense
1700      *  @param integer[] $limit Page, rows per page??? @todo Document this parameter
1701      *  FIXME This parameter doesn't seem to make sense
1702      *  @param string $joins ??? @todo Document this parameter
1703      *  @uses find_all()
1704      *  @return mixed An object of the same class as this class and
1705      *    matching $conditions, or null if none did.
1706      *  @throws {@link ActiveRecordError}
1707      */
1708     function find_first($conditions = null, $order = null, $limit = 1, $joins = null) {
1709         if(is_array($conditions)) {
1710             $options = $conditions;   
1711         } else {
1712             $options['conditions'] = $conditions;   
1713         }
1714         if(!is_null($order)) $options['order'] = $order;
1715         if(!is_null($limit)) $options['limit'] = $limit;
1716         if(!is_null($joins)) $options['joins'] = $joins;
1717
1718         $result = @current($this->find_all($options));
1719         return (is_object($result) ? $result : null);       
1720     }
1721
1722     /**
1723      *  Return all the rows selected by the SQL argument
1724      *
1725      *  If no rows match, an empty array is returned.
1726      *  @param string $sql SQL to use in the query.
1727      */
1728     function find_by_sql($sql) {
1729         return $this->find_all($sql);
1730     }
1731
1732     /**
1733      *  Reloads the attributes of this object from the database.
1734      *  @uses get_primary_key_conditions()
1735      *  @todo Document this API
1736      */
1737     function reload($conditions = null) {
1738         if(is_null($conditions)) {
1739             $conditions = $this->get_primary_key_conditions();
1740         }
1741         $object = $this->find($conditions);
1742         if(is_object($object)) {
1743             foreach($object as $key => $value) {
1744                 $this->$key = $value;
1745             }
1746             return true;
1747         }
1748         return false;
1749     }
1750
1751     /**
1752      *  Loads into current object values from the database.
1753      */
1754     function load($conditions = null) {
1755         return $this->reload($conditions);       
1756     }
1757
1758     /**
1759      *  @todo Document this API.  What's going on here?  It appears to
1760      *        either create a row with all empty values, or it tries
1761      *        to recurse once for each attribute in $attributes.
1762      *  Creates an object, instantly saves it as a record (if the validation permits it).
1763      *  If the save fails under validations it returns false and $errors array gets set.
1764      */
1765     function create($attributes, $dont_validate = false) {
1766         $class_name = $this->get_class_name();
1767         $object = new $class_name();
1768         $result = $object->save($attributes, $dont_validate);
1769         return ($result ? $object : false);
1770     }
1771
1772     /**
1773      *  Finds the record from the passed id, instantly saves it with the passed attributes
1774      *  (if the validation permits it). Returns true on success and false on error.
1775      *  @todo Document this API
1776      */
1777     function update($id, $attributes, $dont_validate = false) {
1778         if(is_array($id)) {
1779             foreach($id as $update_id) {
1780                 $this->update($update_id, $attributes[$update_id], $dont_validate);
1781             }
1782         } else {
1783             $object = $this->find($id);
1784             return $object->save($attributes, $dont_validate);
1785         }
1786     }
1787
1788     /**
1789      *  Updates all records with the SET-part of an SQL update statement in updates and
1790      *  returns an integer with the number of rows updates. A subset of the records can
1791      *  be selected by specifying conditions.
1792      *  Example:
1793      *    $model->update_all("category = 'cooldude', approved = 1", "author = 'John'");
1794      *  @uses is_error()
1795      *  @uses query()
1796      *  @throws {@link ActiveRecordError}
1797      *  @todo Document this API
1798      */
1799     function update_all($updates, $conditions = null) {
1800         $sql = "UPDATE {$this->table_prefix}{$this->table_name} SET {$updates} WHERE {$conditions}";
1801         $this->query($sql);
1802         return true;
1803     }
1804
1805     /**
1806      *  Save without valdiating anything.
1807      *  @todo Document this API
1808      */
1809     function save_without_validation($attributes = null) {
1810         return $this->save($attributes, true);
1811     }
1812
1813     /**
1814      *  Create or update a row in the table with specified attributes
1815      *
1816      *  @param string[] $attributes List of name => value pairs giving
1817      *    name and value of attributes to set.
1818      *  @param boolean $dont_validate true => Don't call validation
1819      *    routines before saving the row.  If false or omitted, all
1820      *    applicable validation routines are called.
1821      *  @uses add_record_or_update_record()
1822      *  @uses update_attributes()
1823      *  @uses valid()
1824      *  @return boolean
1825      *          <ul>
1826      *            <li>true => row was updated or inserted successfully</li>
1827      *            <li>false => insert failed</li>
1828      *          </ul>
1829      */
1830     function save($attributes = null, $dont_validate = false) {
1831         //error_log("ActiveRecord::save() \$attributes="
1832         //          . var_export($attributes,true));
1833         $this->update_attributes($attributes);
1834         if($dont_validate || $this->valid()) {
1835             return $this->add_record_or_update_record();
1836         } else {
1837             return false;
1838         }
1839     }
1840
1841     /**
1842      *  Create or update a row in the table
1843      *
1844      *  If this object represents a new row in the table, insert it.
1845      *  Otherwise, update the exiting row.  before_?() and after_?()
1846      *  routines will be called depending on whether the row is new.
1847      *  @uses add_record()
1848      *  @uses after_create()
1849      *  @uses after_update()
1850      *  @uses before_create()
1851      *  @uses before_save()
1852      *  @uses $new_record
1853      *  @uses update_record()
1854      *  @return boolean
1855      *          <ul>
1856      *            <li>true => row was updated or inserted successfully</li>
1857      *            <li>false => insert failed</li>
1858      *          </ul>
1859      */
1860     private function add_record_or_update_record() {
1861         //error_log('add_record_or_update_record()');
1862         $this->before_save();
1863         if($this->new_record) {
1864             $this->before_create();
1865             $result = $this->add_record();   
1866             $this->after_create();
1867         } else {
1868             $this->before_update();
1869             $result = $this->update_record();
1870             $this->after_update();
1871         }
1872         $this->after_save();
1873         return $result;
1874     }
1875
1876     /**
1877      *  Insert a new row in the table associated with this object
1878      *
1879      *  Build an SQL INSERT statement getting the table name from
1880      *  {@link $table_name}, the column names from {@link
1881      *  $content_columns} and the values from object variables.
1882      *  Send the insert to the RDBMS.
1883      *  @uses $auto_save_habtm
1884      *  @uses add_habtm_records()
1885      *  @uses before_create()
1886      *  @uses get_insert_id()
1887      *  @uses is_error()
1888      *  @uses query()
1889      *  @uses get_inserts()
1890      *  @uses raise()
1891      *  @uses $table_name
1892      *  @return boolean
1893      *          <ul>
1894      *            <li>true => row was inserted successfully</li>
1895      *            <li>false => insert failed</li>
1896      *          </ul>
1897      *  @throws {@link ActiveRecordError}
1898      */
1899     private function add_record() {
1900         self::$db->loadModule('Extended', null, true);               
1901         # $primary_key_value may either be a quoted integer or php null
1902         $primary_key_value = self::$db->getBeforeID("{$this->table_prefix}{$this->table_name}", $this->primary_keys[0]);
1903         if($this->is_error($primary_key_value)) {
1904             $this->raise($primary_key_value->getMessage());
1905         }
1906         $this->update_composite_attributes();
1907         $attributes = $this->get_inserts();
1908         $fields = @implode(', ', array_keys($attributes));
1909         $values = @implode(', ', array_values($attributes));
1910         $sql = "INSERT INTO {$this->table_prefix}{$this->table_name} ({$fields}) VALUES ({$values})";
1911         //echo "add_record: SQL: $sql<br>";
1912         //error_log("add_record: SQL: $sql");
1913         $result = $this->query($sql); 
1914         $habtm_result = true;
1915         $primary_key = $this->primary_keys[0];
1916         # $id is now equivalent to the value in the id field that was inserted
1917         $primary_key_value = self::$db->getAfterID($primary_key_value, "{$this->table_prefix}{$this->table_name}", $this->primary_keys[0]);
1918         if($this->is_error($primary_key_value)) {
1919             $this->raise($primary_key_value->getMessage());
1920         }           
1921         $this->$primary_key = $primary_key_value;
1922         if($primary_key_value != '') {
1923             if($this->auto_save_habtm) {
1924                 $habtm_result = $this->add_habtm_records($primary_key_value);
1925             }
1926             $this->save_associations();
1927         }         
1928         return ($result && $habtm_result);
1929     }
1930
1931     /**
1932      *  Update the row in the table described by this object
1933      *
1934      *  The primary key attributes must exist and have appropriate
1935      *  non-null values.  If a column is listed in {@link
1936      *  $content_columns} but no attribute of that name exists, the
1937      *  column will be set to the null string ''.
1938      *  @todo Describe habtm automatic update
1939      *  @uses is_error()
1940      *  @uses get_updates_sql()
1941      *  @uses get_primary_key_conditions()
1942      *  @uses query()
1943      *  @uses raise()
1944      *  @uses update_habtm_records()
1945      *  @return boolean
1946      *          <ul>
1947      *            <li>true => row was updated successfully</li>
1948      *            <li>false => update failed</li>
1949      *          </ul>
1950      *  @throws {@link ActiveRecordError}
1951      */
1952     private function update_record() {
1953         //error_log('update_record()');
1954         $this->update_composite_attributes();
1955         $updates = $this->get_updates_sql();
1956         $conditions = $this->get_primary_key_conditions();
1957         $sql = "UPDATE {$this->table_prefix}{$this->table_name} SET {$updates} WHERE {$conditions}";
1958         //echo "update_record:$sql<br>";
1959         //error_log("update_record: SQL: $sql");
1960         $result = $this->query($sql);
1961         $habtm_result = true;
1962         $primary_key = $this->primary_keys[0];
1963         $primary_key_value = $this->$primary_key;
1964         if($primary_key_value > 0) {
1965             if($this->auto_save_habtm) {
1966                 $habtm_result = $this->update_habtm_records($primary_key_value);
1967             }
1968             $this->save_associations();
1969         }         
1970         return ($result && $habtm_result);
1971     }
1972
1973     /**
1974      *  Loads the model values into composite object
1975      *  @todo Document this API
1976      */   
1977     private function get_composite_object($name) {
1978         $composite_object = null;
1979         $composite_attributes = array();
1980         if(is_array($this->composed_of)) {
1981             if(array_key_exists($name, $this->composed_of)) {
1982                 $class_name = Inflector::classify(($this->composed_of[$name]['class_name'] ?
1983                     $this->composed_of[$name]['class_name'] : $name));           
1984
1985                 $mappings = $this->composed_of[$name]['mapping'];
1986                 if(is_array($mappings)) {
1987                     foreach($mappings as $database_name => $composite_name) {
1988                         $composite_attributes[$composite_name] = $this->$database_name;                     
1989                     }   
1990                 }   
1991             }   
1992         } elseif($this->composed_of == $name) {
1993             $class_name = $name;
1994             $composite_attributes[$name] = $this->$name;       
1995         }
1996         
1997         if(class_exists($class_name)) {                     
1998             $composite_object = new $class_name;       
1999             if($composite_object->auto_map_attributes !== false) {
2000                 //echo "auto_map_attributes<br>";
2001                 foreach($composite_attributes as $name => $value) {
2002                     $composite_object->$name = $value;   
2003                 }                                     
2004             }           
2005             if(method_exists($composite_object, '__construct')) {
2006                 //echo "calling constructor<br>";
2007                 $composite_object->__construct($composite_attributes);       
2008             }         
2009         }
2010         return $composite_object;
2011     }
2012     
2013     /**
2014      *  returns the association type if defined in child class or null
2015      *  @todo Document this API
2016      *  @uses $belongs_to
2017      *  @uses $has_and_belongs_to_many
2018      *  @uses $has_many
2019      *  @uses $has_one
2020      *  @return mixed Association type, one of the following:
2021      *  <ul>
2022      *    <li>"belongs_to"</li>
2023      *    <li>"has_and_belongs_to_many"</li>
2024      *    <li>"has_many"</li>
2025      *    <li>"has_one"</li>
2026      *  </ul>
2027      *  if an association exists, or null if no association
2028      */
2029     function get_association_type($association_name) {
2030         $type = null;
2031         if(is_string($this->has_many)) {
2032             if(preg_match("/\b$association_name\b/", $this->has_many)) {
2033                 $type = "has_many";   
2034             }
2035         } elseif(is_array($this->has_many)) {
2036             if(array_key_exists($association_name, $this->has_many)) {
2037                 $type = "has_many";     
2038             }
2039         }
2040         if(is_string($this->has_one)) {
2041             if(preg_match("/\b$association_name\b/", $this->has_one)) {
2042                 $type = "has_one";     
2043             }
2044         } elseif(is_array($this->has_one)) {
2045             if(array_key_exists($association_name, $this->has_one)) {
2046                 $type = "has_one";     
2047             }
2048         }
2049         if(is_string($this->belongs_to)) {
2050             if(preg_match("/\b$association_name\b/", $this->belongs_to)) {
2051                 $type = "belongs_to";     
2052             }
2053         } elseif(is_array($this->belongs_to)) {
2054             if(array_key_exists($association_name, $this->belongs_to)) {
2055                 $type = "belongs_to";     
2056             }
2057         }
2058         if(is_string($this->has_and_belongs_to_many)) {
2059             if(preg_match("/\b$association_name\b/", $this->has_and_belongs_to_many)) {
2060                 $type = "has_and_belongs_to_many";     
2061             }
2062         } elseif(is_array($this->has_and_belongs_to_many)) {
2063             if(array_key_exists($association_name, $this->has_and_belongs_to_many)) {
2064                 $type = "has_and_belongs_to_many";     
2065             }
2066         }   
2067         return $type;   
2068     }
2069     
2070     /**
2071      *  Saves any associations objects assigned to this instance
2072      *  @uses $auto_save_associations
2073      *  @todo Document this API
2074      */
2075     private function save_associations() {     
2076         if(count($this->save_associations) && $this->auto_save_associations) {
2077             foreach(array_keys($this->save_associations) as $type) {
2078                 if(count($this->save_associations[$type])) {
2079                     foreach($this->save_associations[$type] as $object_or_array) {
2080                         if(is_object($object_or_array)) {
2081                             $this->save_association($object_or_array, $type);     
2082                         } elseif(is_array($object_or_array)) {
2083                             foreach($object_or_array as $object) {
2084                                 $this->save_association($object, $type);   
2085                             }   
2086                         }
2087                     }
2088                 }
2089             }   
2090         }       
2091     }
2092     
2093     /**
2094      *  save the association to the database
2095      *  @todo Document this API
2096      */
2097     private function save_association($object, $type) {
2098         if(is_object($object) && get_parent_class($object) == __CLASS__ && $type) {
2099             //echo get_class($object)." - type:$type<br>";
2100             switch($type) {
2101                 case "has_many":
2102                 case "has_one":
2103                     $primary_key = $this->primary_keys[0];
2104                     $foreign_key = Inflector::singularize($this->table_name)."_".$primary_key;
2105                     $object->$foreign_key = $this->$primary_key;
2106                     //echo "fk:$foreign_key = ".$this->$primary_key."<br>";
2107                     break;
2108             }
2109             $object->save();       
2110         }           
2111     }
2112
2113     /**
2114      *  Deletes the record with the given $id or if you have done a
2115      *  $model = $model->find($id), then $model->delete() it will delete
2116      *  the record it just loaded from the find() without passing anything
2117      *  to delete(). If an array of ids is provided, all ids in array are deleted.
2118      *  @uses $errors
2119      *  @todo Document this API
2120      */
2121     function delete($id = null) {
2122         $deleted_ids = array();
2123         $primary_key_value = null;
2124         $primary_key = $this->primary_keys[0];
2125         if(is_null($id)) {
2126             # Primary key's where clause from already loaded values
2127             $conditions = $this->get_primary_key_conditions();
2128             $deleted_ids[] = $this->$primary_key;
2129         } elseif(!is_array($id)) {         
2130             $deleted_ids[] = $id;
2131             $id = $this->attribute_is_string($primary_key) ? "'".$id."'" : $id;
2132             $conditions = "{$primary_key} = {$id}";
2133         } elseif(is_array($id)) {
2134             $deleted_ids = $id;
2135             $ids = ($this->attribute_is_string($primary_key)) ?
2136                 "'".implode("','", $id)."'" :
2137                 implode(',', $id);
2138             $conditions = "{$primary_key} IN ({$ids})";
2139         }
2140
2141         if(is_null($conditions)) {
2142             $this->add_error("No conditions specified to delete on.");
2143             return false;
2144         }
2145
2146         if($this->before_delete()) {
2147             if($result = $this->delete_all($conditions)) {
2148                 foreach($deleted_ids as $id) {
2149                     if(<