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

Revision 337, 145.6 KB (checked in by john, 5 months ago)

added unset() and isset() to ActiveRecord?

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