PHP on T R A X
Rapid Application Development Made Easy

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

Revision 329, 141.7 kB (checked in by john, 5 months ago)

changed default environment back to development in AR

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