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

Revision 337, 145.6 KB (checked in by john, 2 years 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
2651    /**
2652     *  Return pairs of column-name:quoted-column-value
2653     *
2654     *  Return pairs of column-name:quoted-column-value where the key
2655     *  is the column name and the value is the column value with
2656     *  automatic timestamp updating applied and characters special to
2657     *  SQL quoted.
2658     * 
2659     *  If $attributes is null or omitted, return all columns as
2660     *  currently stored in {@link content_columns()}.  Otherwise,
2661     *  return the name:value pairs in $attributes.
2662     *  @param string[] $attributes Name:value pairs to return.
2663     *    If null or omitted, return the column names and values
2664     *    of the object as stored in $content_columns.
2665     *  @return string[]
2666     *  @uses get_attributes()
2667     *  @see set_content_columns()
2668     */
2669    function quoted_attributes($attributes = null) {
2670        if(is_null($attributes)) {
2671            $attributes = $this->get_attributes();
2672        }
2673        $return = array();
2674        foreach($attributes as $name => $value) {           
2675            $return[$name] = $this->quote_attribute($name, $value);
2676        }
2677        return $return;
2678    }
2679
2680    /**
2681     *  Quotes a single attribute for use in an sql statement.
2682     *
2683     */   
2684    function quote_attribute($attribute, $value = null) {
2685        $value = is_null($value) ? $this->$attribute : $value;
2686        $value = $this->check_datetime($attribute, $value);       
2687        $column = $this->column_for_attribute($attribute);
2688        if(isset($column['mdb2type'])) {
2689            $type = $column['mdb2type'];
2690        } else {
2691            $type = $this->attribute_is_string($attribute, $column) ? 
2692                "text" : is_float($attribute) ? "float" : "integer"; 
2693        }           
2694        $value = self::$db->quote($value, $type);   
2695        if($value === 'NULL' && stristr($column['flags'], "not_null")) {
2696            $value = "''";   
2697        } 
2698        return $value;               
2699    }
2700
2701    /**
2702     *  Escapes a string for use in an sql statement.
2703     *
2704     */   
2705    function escape($string) {
2706        return(self::$db->escape($string));
2707    }   
2708
2709    /**
2710     *  Return argument for a "WHERE" clause specifying this row
2711     *
2712     *  Returns a string which specifies the column(s) and value(s)
2713     *  which describe the primary key of this row of the associated
2714     *  table.  The primary key must be one or more attributes of the
2715     *  object and must be listed in {@link $content_columns} as
2716     *  columns in the row.
2717     *
2718     *  Example: if $primary_keys = array("id", "ssn") and column "id"
2719     *  has value "5" and column "ssn" has value "123-45-6789" then
2720     *  the string "id = 5 AND ssn = '123-45-6789'" would be returned.
2721     *  @uses $primary_keys
2722     *  @uses quoted_attributes()
2723     *  @return string Column name = 'value' [ AND name = 'value']...
2724     */
2725    function get_primary_key_conditions($operator = "=") {
2726        $conditions = null;
2727        $attributes = $this->quoted_attributes();
2728        if(count($attributes) > 0) {
2729            $conditions = array();
2730            # run through our fields and join them with their values
2731            foreach($attributes as $key => $value) {
2732                if(in_array($key, $this->primary_keys) && isset($value) && $value != "''") {
2733                    $conditions[] = "{$key} {$operator} {$value}";   
2734                }
2735            }
2736            $conditions = implode(" AND ", $conditions);
2737        }
2738        return $conditions;
2739    }
2740
2741    /**
2742     *  Return column values for SQL insert statement
2743     *
2744     *  Return an array containing the column names and values of this
2745     *  object, filtering out the primary keys, which are not set.
2746     *
2747     *  @uses $primary_keys
2748     *  @uses quoted_attributes()
2749     */
2750    function get_inserts() {
2751        $attributes = $this->quoted_attributes();
2752        $inserts = array();
2753        foreach($attributes as $key => $value) {
2754            if(!in_array($key, $this->primary_keys) || ($value != "''" && isset($value))) {
2755                $inserts[$key] = $value;
2756            }
2757        }
2758        return $inserts;
2759    }
2760
2761    /**
2762     *  Return column values of object formatted for SQL update statement
2763     *
2764     *  Return a string containing the column names and values of this
2765     *  object in a format ready to be inserted in a SQL UPDATE
2766     *  statement.  Automatic update has been applied to timestamps if
2767     *  enabled and characters special to SQL have been quoted.
2768     *  @uses quoted_attributes()
2769     *  @return string Column name = 'value', ... for all attributes
2770     */
2771    function get_updates_sql() {
2772        $updates = null;
2773        $attributes = null;
2774        if($this->partial_updates && $this->changed) {
2775            foreach($this->changed_attributes as $key => $changes) {
2776                $attributes[$key] = $changes['modified'];
2777            }
2778            #error_log("modified attributes:".print_r($attributes, true));
2779            # make sure the auto timestamps are in there
2780            foreach($this->content_columns as $column_name => $column) {
2781                if(stristr($column['type'], "date") && in_array($column_name, $this->auto_update_timestamps) && !array_key_exists($column_name, $attributes)) {
2782                    $attributes[$column_name] = $this->$column_name;
2783                }
2784            }
2785            #error_log("modified attributes with dates:".print_r($attributes, true));                       
2786        }
2787        $attributes = $this->quoted_attributes($attributes);
2788        if(count($attributes) > 0) {
2789            $updates = array();
2790            # run through our fields and join them with their values
2791            foreach($attributes as $key => $value) {
2792                if($key && isset($value) && !in_array($key, $this->primary_keys)) {
2793                    $updates[] = "$key = $value";
2794                }
2795            }
2796            $updates = implode(", ", $updates);
2797        }
2798        return $updates;
2799    }
2800
2801    function human_attribute_name($attribute) {
2802        return Inflector::humanize($attribute);
2803    }
2804
2805    /**
2806     *  Set {@link $table_name} from the class name of this object
2807     *
2808     *  By convention, the name of the database table represented by
2809     *  this object is derived from the name of the class.
2810     *  @uses Inflector::tableize()
2811     */
2812    function set_table_name_using_class_name() {
2813        if(!$this->table_name) {
2814            $class_name = $this->get_class_name();
2815            $this->table_name = Inflector::tableize($class_name);
2816        }
2817    }
2818
2819    /**
2820     *  Get class name of child object
2821     *
2822     *  this will return the manually set name or get_class($this)
2823     *  @return string child class name
2824     */   
2825    private function get_class_name() {
2826        return !is_null($this->class_name) ? $this->class_name : get_class($this);               
2827    }
2828
2829    /**
2830     *  Populate object with information about the table it represents
2831     *
2832     *  Call {@link
2833     *  http://pear.php.net/manual/en/package.database.db.db-common.tableinfo.php
2834     *  DB_common::tableInfo()} to get a description of the table and
2835     *  store it in {@link $content_columns}.  Add a more human
2836     *  friendly name to the element for each column.
2837     *  @uses $db
2838     *  @uses $content_columns
2839     *  @uses human_attribute_name()
2840     *  @see __set()
2841     *  @param string $table_name  Name of table to get information about
2842     */
2843    function set_content_columns($table_name) {
2844        if(!is_null($this->table_prefix)) {
2845            $table_name = $this->table_prefix.$table_name;
2846        }
2847        if(isset(self::$table_info[$table_name])) {
2848            $this->content_columns = self::$table_info[$table_name];
2849            #error_log("using cached content_columns");
2850        } else {       
2851            $db =& $this->get_connection(true);                                     
2852            $db->loadModule('Reverse', null, true);
2853            $content_columns = $db->reverse->tableInfo($table_name);
2854            if($this->is_error($content_columns)) {
2855                $this->raise($content_columns->getMessage());       
2856            }
2857            if(is_array($content_columns)) {
2858                $this->content_columns = array();
2859                foreach($content_columns as $column) {
2860                    $column['human_name'] = $this->human_attribute_name($column['name']);
2861                    $this->content_columns[$column['name']] = $column;
2862                }       
2863                self::$table_info[$table_name] = $this->content_columns;
2864            }
2865        }
2866    }
2867
2868    /**
2869     *  Returns the autogenerated id from the last insert query
2870     *
2871     *  @uses $db
2872     *  @uses is_error()
2873     *  @uses raise()
2874     *  @throws {@link ActiveRecordError}
2875     */
2876    function get_insert_id() {
2877        // fetch the last inserted id via autoincrement or current value of a sequence
2878        $db =& $this->get_connection();
2879        if($db->supports('auto_increment') === true) {
2880            $id = $db->lastInsertID("{$this->table_prefix}{$this->table_name}", $this->primary_keys[0]);   
2881            if($this->is_error($id)) {
2882                $this->raise($id->getMessage());
2883            } 
2884            return $id;               
2885        }
2886
2887        return null;
2888    }
2889
2890    /**
2891     *  Open a database connection if one is not currently open
2892     *
2893     *  The name of the database normally comes from
2894     *  $database_settings which is set in {@link
2895     *  environment.php} by reading file config/database.ini. The
2896     *  database name may be overridden by assigning a different name
2897     *  to {@link $database_name}.
2898     * 
2899     *  If there is a connection now open, as indicated by the saved
2900     *  value of a MDB2 object in $connection_pool[$connection_name], and
2901     *  {@link force_reconnect} is not true, then set the database
2902     *  fetch mode and return.
2903     *
2904     *  If there is no connection, open one and save a reference to
2905     *  it in $connection_pool[$connection_name].
2906     *
2907     *  @uses $db
2908     *  @uses $database_name
2909     *  @uses $force_reconnect
2910     *  @uses $connection_pool
2911     *  @uses is_error()
2912     *  @throws {@link ActiveRecordError}
2913     */
2914    function establish_connection($connection_name = null, $read_only = false) { 
2915        #error_log("trying connection name:$connection_name");
2916        $connection_name = $this->get_connection_name($connection_name); 
2917        #error_log("got connection name:$connection_name read only:".($read_only ? 'true' : 'false'));
2918        if($read_only) { 
2919            $connection =& self::$connection_pool_read_only[$connection_name];
2920        } else {
2921            $connection =& self::$connection_pool[$connection_name];
2922        }
2923        if(!is_object($connection) || $this->force_reconnect) {
2924            $connection_settings = array();
2925            $connection_options = array();
2926            if(array_key_exists($connection_name, self::$database_settings)) {
2927                 # Use a different custom sections settings ?
2928                if(array_key_exists("use", self::$database_settings[$connection_name])) {
2929                    $connection_settings = self::$database_settings[self::$database_settings[$connection_name]['use']];
2930                } else {
2931                    # Custom defined db settings in database.ini
2932                    $connection_settings = self::$database_settings[$connection_name];
2933                }
2934            } else {
2935                # Just use the current environment's db settings
2936                # $connection_name's default value is 'development' so
2937                # it should never really get here unless you override $this->connection_name
2938                # and you define a custom db section in database.ini and it can't find it.
2939                $connection_settings = self::$database_settings[$connection_name];
2940            }
2941            # Override database name if param is set
2942            if($this->database_name) {
2943                $connection_settings['database'] = $this->database_name;               
2944            }           
2945            # Set optional Pear parameters
2946            if(isset($connection_settings['persistent'])) {
2947                $connection_options['persistent'] = $connection_settings['persistent'];
2948            }
2949            # Connect to the database and throw an error if the connect fails.
2950            $connection =& MDB2::Connect($connection_settings, $connection_options);
2951            #static $connect_cnt;  $connect_cnt++; error_log("establish_connection($connection_name, $read_only) #".$connect_cnt);
2952           
2953            # For Postgres schemas (http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html)
2954            if(isset($connection_settings['schema_search_path'])){
2955                if(!$this->is_error($connection)) {
2956                    # Set the schema search path to a string of comma-separated schema names.
2957                    # First strip out all the whitespace
2958                    $connection->query('SET search_path TO '.preg_replace('/\s+/', '', $connection_settings['schema_search_path']));
2959                }
2960            } 
2961        } 
2962        if(!$this->is_error($connection)) {
2963            $connection->setFetchMode($this->fetch_mode);
2964            if($read_only) {
2965                self::$connection_pool_read_only[$connection_name] =& $connection; 
2966                $pool_size = count(self::$connection_pool_read_only);
2967                $this->read_only_connection_name = $connection_name;
2968            } else {
2969                self::$connection_pool[$connection_name] =& $connection; 
2970                $pool_size = count(self::$connection_pool);
2971                $this->connection_name = $connection_name;
2972            } 
2973           
2974            if($pool_size > 1 || $this->database_name != '') {
2975                $dsn = $connection->getDSN('array', true);     
2976                #error_log("dsn:".print_r($dsn, true));
2977                if($this->database_name != '') { 
2978                    #$type = "database defined";
2979                    $database_name = $this->database_name; 
2980                } elseif($dsn['database'] != '') { 
2981                    #$type = "dsn";
2982                    $database_name = $dsn['database'];
2983                }
2984                if($database_name) {
2985                    #error_log("connect $type switch database to {$database_name}");
2986                    $connection->setDatabase($database_name);           
2987                }
2988            }                                   
2989        } else {   
2990            $this->raise($connection->getMessage());
2991        }     
2992        return $connection;
2993    }
2994
2995    /**
2996     *  Set the name of the database connection to use.
2997     */   
2998    function get_connection_name($connection_name = null) {
2999        if(is_null($connection_name)) {
3000            if(!is_null($this->connection_name)) {
3001                $connection_name = $this->connection_name;     
3002            } else {
3003                $connection_name = self::$environment ? self::$environment : 'development';
3004            } 
3005        }
3006        return $connection_name; 
3007    }
3008
3009    /**
3010     *  Gets the database connection whether its read only or read/write   
3011     */   
3012    function get_connection($read_only = false) { 
3013        if($read_only && $this->read_only_connection_name && 
3014           array_key_exists($this->read_only_connection_name, self::$connection_pool_read_only)) {
3015            $db =& self::$connection_pool_read_only[$this->read_only_connection_name]; 
3016            $pool_size = count(self::$connection_pool_read_only);
3017            #error_log("get_connection pool:$pool_size - using read only:".$this->read_only_connection_name." dbname:".$db->database_name." table_name:".$this->table_name);
3018        } elseif(array_key_exists($this->connection_name, self::$connection_pool)) {
3019            $db =& self::$connection_pool[$this->connection_name];   
3020            $pool_size = count(self::$connection_pool);
3021            #error_log("get_connection pool:$pool_size - using read/write from pool:".$this->connection_name." dbname:".$db->database_name." table_name:".$this->table_name);
3022        } else {
3023            $db =& self::$db;   
3024            $pool_size = 1;
3025            #error_log("get_connection pool:1 - using read/write default:".$this->connection_name." dbname:".$db->database_name." table_name:".$this->table_name);
3026        } 
3027           
3028        if($pool_size > 1 || $this->database_name != '') {
3029            $dsn = $db->getDSN('array', true);     
3030            #error_log("dsn:".print_r($dsn, true));
3031            if($this->database_name != '') { 
3032                $type = "database defined"; 
3033                $database_name = $this->database_name; 
3034            } elseif($dsn['database'] != '') { 
3035                $type = "dsn";
3036                $database_name = $dsn['database'];
3037            }
3038            if($database_name) {
3039                #error_log("get_connection $type switch database to {$database_name} table:".$this->table_name);
3040                $db->setDatabase($database_name);           
3041            }
3042        }
3043                   
3044        return $db;
3045    }   
3046
3047    /**
3048     *  Clears all database connections
3049     */   
3050    function clear_all_connections() {
3051        self::$connection_pool = array();
3052        self::$connection_pool_read_only = array(); 
3053    } 
3054
3055    /**
3056     *  Select a different database   
3057     *
3058     * @param   string  name of the database that should be selected
3059     * @return  string  name of the database previously connected to
3060     * @access  public     
3061     */   
3062    function set_database($database_name) {   
3063        $db =& $this->get_connection(true);
3064        if($database_name && is_object($db)) {
3065            return $db->setDatabase($database_name);
3066        }
3067    }
3068
3069    /**
3070     *  Determine if passed in attribute (table column) is a string
3071     *  @param string $attribute Name of the table column
3072     *  @uses column_for_attribute()
3073     */   
3074    function attribute_is_string($attribute, $column = null) {
3075        $column = is_null($column) ? $this->column_for_attribute($attribute) : $column;
3076        switch(strtolower($column['mdb2type'])) {
3077            case 'text':
3078            case 'timestamp':
3079            case 'date':
3080            case 'time':
3081            case 'blob':
3082            case 'clob':
3083                return true;       
3084        }
3085        return false;       
3086    }
3087
3088    /**
3089     *  Determine if passed in name is a composite class or not
3090     *  @param string $name Name of the composed_of mapping
3091     *  @uses $composed_of
3092     */   
3093    private function is_composite($name) {
3094        if(is_array($this->composed_of)) {
3095            if(array_key_exists($name, $this->composed_of)) {
3096                return true;     
3097            }
3098        }       
3099        return false;
3100    }   
3101
3102    /**
3103     *  Runs validation routines for update or create
3104     *
3105     *  @uses after_validation();
3106     *  @uses after_validation_on_create();
3107     *  @uses after_validation_on_update();
3108     *  @uses before_validation();
3109     *  @uses before_validation_on_create();
3110     *  @uses before_validation_on_update();
3111     *  @uses $errors
3112     *  @uses $new_record
3113     *  @uses validate();
3114     *  @uses validate_model_attributes();
3115     *  @uses validate_builtin();
3116     *  @uses validate_on_create();
3117     *  @return boolean
3118     *    <ul>
3119     *      <li>true => Valid, no errors found.
3120     *        {@link $errors} is empty</li>
3121     *      <li>false => Not valid, errors in {@link $errors}</li>
3122     *    </ul>
3123     */
3124    function valid() {
3125        # first clear the errors array
3126        $this->errors = array();
3127
3128        if($this->new_record) {
3129            $this->before_validation();
3130            $this->before_validation_on_create();
3131            $this->validate();
3132            $this->validate_model_attributes();
3133            $this->validate_builtin();           
3134            $this->after_validation();
3135            $this->validate_on_create(); 
3136            $this->after_validation_on_create();
3137        } else {
3138            $this->before_validation();
3139            $this->before_validation_on_update();
3140            $this->validate();
3141            $this->validate_model_attributes();
3142            $this->validate_builtin();
3143            $this->after_validation();
3144            $this->validate_on_update();
3145            $this->after_validation_on_update();
3146        }
3147
3148        return count($this->errors) ? false : true;
3149    }
3150
3151    /**
3152     *  Call every method named "validate_*()" where * is a column name
3153     *
3154     *  Find and call every method named "validate_something()" where
3155     *  "something" is the name of a column.  The "validate_something()"
3156     *  functions are expected to return an array whose first element
3157     *  is true or false (indicating whether or not the validation
3158     *  succeeded), and whose second element is the error message to
3159     *  display if the first element is false.
3160     *
3161     *  @return boolean
3162     *    <ul>
3163     *      <li>true => Valid, no errors found.
3164     *        {@link $errors} is empty</li>
3165     *      <li>false => Not valid, errors in {@link $errors}.
3166     *        $errors is an array whose keys are the names of columns,
3167     *        and the value of each key is the error message returned
3168     *        by the corresponding validate_*() method.</li>
3169     *    </ul>
3170     *  @uses $errors
3171     *  @uses get_attributes()
3172     */
3173    function validate_model_attributes() {
3174        $validated_ok = true;
3175        $attrs = $this->get_attributes();
3176        $methods = get_class_methods($this->get_class_name());
3177        foreach($methods as $method) {
3178            if(preg_match('/^validate_(.+)/', $method, $matches)) {
3179                # If we find, for example, a method named validate_name, then
3180                # we know that that function is validating the 'name' attribute
3181                # (as found in the (.+) part of the regular expression above).
3182                $validate_on_attribute = $matches[1];
3183                # Check to see if the string found (e.g. 'name') really is
3184                # in the list of attributes for this object...
3185                if(array_key_exists($validate_on_attribute, $attrs)) {
3186                    # ...if so, then call the method to see if it validates to true...
3187                    $result = $this->$method();
3188                    if(is_array($result)) {
3189                        # $result[0] is true if validation went ok, false otherwise
3190                        # $result[1] is the error message if validation failed
3191                        if($result[0] == false) {
3192                            # ... and if not, then validation failed
3193                            $validated_ok = false;
3194                            # Mark the corresponding entry in the error array by
3195                            # putting the error message in for the attribute,
3196                            #   e.g. $this->add_error("can't be empty", 'name');
3197                            #   when 'name' was an empty string.
3198                            $this->add_error($result[1], $validate_on_attribute);
3199                        }
3200                    }
3201                }
3202            }
3203        }
3204        return $validated_ok;
3205    } 
3206   
3207    /**
3208     *  Overwrite this method for validation checks on all saves and
3209     *  use $this->add_error("My error message.", 'attribute');
3210     *  @todo Document this API
3211     */
3212    function validate() {}
3213
3214    /**
3215     *  Override this method for validation checks used only on creation.
3216     *  @todo Document this API
3217     */
3218    function validate_on_create() {}
3219
3220    /**
3221     *  Override this method for validation checks used only on updates.
3222     *  @todo Document this API
3223     */
3224    function validate_on_update() {}
3225
3226    /**
3227     *  Is called before validate().
3228     *  @todo Document this API
3229     */
3230    function before_validation() {}
3231
3232    /**
3233     *  Is called after validate().
3234     *  @todo Document this API
3235     */
3236    function after_validation() {}
3237
3238    /**
3239     *  Is called before validate() on new objects that haven't been saved yet (no record exists).
3240     *  @todo Document this API
3241     */
3242    function before_validation_on_create() {}
3243
3244    /**
3245     *  Is called after validate() on new objects that haven't been saved yet (no record exists).
3246     *  @todo Document this API
3247     */
3248    function after_validation_on_create()  {}
3249
3250    /**
3251     *  Is called before validate() on existing objects that has a record.
3252     *  @todo Document this API
3253     */
3254    function before_validation_on_update() {}
3255
3256    /**
3257     *  Is called after validate() on existing objects that has a record.
3258     *  @todo Document this API
3259     */
3260    function after_validation_on_update()  {}
3261
3262    /**
3263     *  Is called before save() (regardless of whether its a create or update save)
3264     *  @todo Document this API
3265     */
3266    function before_save() {}
3267
3268    /**
3269     *  Is called after save (regardless of whether its a create or update save).
3270     *  @todo Document this API
3271     */
3272    function after_save() {}
3273
3274    /**
3275     *  Is called before save() on new objects that havent been saved yet (no record exists).
3276     *  @todo Document this API
3277     */
3278    function before_create() {}
3279
3280    /**
3281     *  Is called after save() on new objects that havent been saved yet (no record exists).
3282     *  @todo Document this API
3283     */
3284    function after_create() {}
3285
3286    /**
3287     *  Is called before save() on existing objects that has a record.
3288     *  @todo Document this API
3289     */
3290    function before_update() {}
3291
3292    /**
3293     *  Is called after save() on existing objects that has a record.
3294     *  @todo Document this API
3295     */
3296    function after_update() {}
3297
3298    /**
3299     *  Is called before delete().
3300     *  @todo Document this API
3301     */
3302    function before_delete() { return true; }
3303
3304    /**
3305     *  Is called after delete().
3306     *  @todo Document this API
3307     */
3308    function after_delete() {}
3309
3310
3311    /**
3312     *  Validates any builtin validates_* functions defined as
3313     *  class variables in child model class.
3314     *
3315     *  eg.
3316     *  public $validates_presence_of = array(
3317     *      'first_name' => array(
3318     *          'message' => "is not optional.",
3319     *          'on' => 'update'
3320     *      ),
3321     *      'last_name' => null,
3322     *      'password' => array(
3323     *          'on' => 'create'
3324     *      )
3325     *  );
3326     *
3327     *  @uses $builtin_validation_functions
3328     */
3329    function validate_builtin() {
3330        foreach($this->builtin_validation_functions as $method_name) {
3331            $validation_name = $this->$method_name;
3332            if(is_string($validation_name)) {
3333                $validation_name = explode(",", $validation_name);   
3334            }
3335            if(method_exists($this, $method_name) && is_array($validation_name)) {
3336                foreach($validation_name as $attribute_name => $options) {
3337                    if(is_string($options)) {
3338                        $attribute_name = $options;
3339                        $options = array();
3340                    } elseif(!is_array($options)) {
3341                        $options = array();
3342                    }               
3343                    $attribute_name = trim($attribute_name);     
3344                    $parameters = array();
3345                    $on = array_key_exists('on', $options) ? $options['on'] : 'save';
3346                    $message = array_key_exists('message', $options) ? $options['message'] : null;   
3347                    switch($method_name) {
3348                        case 'validates_acceptance_of':
3349                            $accept = array_key_exists('accept', $options) ? $options['accept'] : 1;
3350                            $parameters = array($attribute_name, $message, $accept);
3351                            break;
3352                        case 'validates_confirmation_of':
3353                            $parameters = array($attribute_name, $message);                           
3354                            break;
3355                        case 'validates_exclusion_of': 
3356                            $in = array_key_exists('in', $options) ? $options['in'] : array();
3357                            $parameters = array($attribute_name, $in, $message); 
3358                            break;     
3359                        case 'validates_format_of':
3360                            $with = array_key_exists('with', $options) ? $options['with'] : '';
3361                            $parameters = array($attribute_name, $with, $message);
3362                            break;
3363                        case 'validates_inclusion_of':   
3364                            $in = array_key_exists('in', $options) ? $options['in'] : array();
3365                            $parameters = array($attribute_name, $in, $message);   
3366                            break; 
3367                        case 'validates_length_of':
3368                            $parameters = array($attribute_name, $options);
3369                            break;
3370                        case 'validates_numericality_of': 
3371                            $only_integer = array_key_exists('only_integer', $options) ? 
3372                                $options['only_integer'] : false;
3373                            $allow_null = array_key_exists('allow_null', $options) ? 
3374                                $options['allow_null'] : false;                         
3375                            $parameters = array($attribute_name, $message, $only_integer, $allow_null);
3376                            break;     
3377                        case 'validates_presence_of':   
3378                            $parameters = array($attribute_name, $message); 
3379                            break;   
3380                        case 'validates_uniqueness_of': 
3381                            $parameters = array($attribute_name, $message);
3382                            break;                       
3383                    }
3384                    if(count($parameters)) { 
3385                        $call = false;
3386                        if($on == 'create' && $this->new_record) {
3387                            $call = true;
3388                        } elseif($on == 'update' && !$this->new_record) {
3389                            $call = true;
3390                        } elseif($on == 'save') {
3391                            $call = true;         
3392                        }       
3393                        if($call) {
3394                            # error_log("calling $method_name(".implode(",",$parameters).")");
3395                            call_user_func_array(array($this, $method_name), $parameters);       
3396                        }                             
3397                    }
3398                }
3399            }             
3400        }       
3401    }
3402
3403    /**
3404     * Validates that a checkbox is clicked.
3405     * eg. validates_acceptance_of('eula')
3406     *
3407     * @param string|array $attribute_names
3408     * @param string $message
3409     * @param string $accept
3410     */
3411    function validates_acceptance_of($attribute_names, $message = null, $accept = 1) {
3412        $message = $this->get_error_message_for_validation($message, 'acceptance');     
3413        foreach((array) $attribute_names as $attribute_name) {                 
3414            if($this->$attribute_name != $accept) {
3415                #$attribute_human = $this->human_attribute_name($attribute_name);
3416                #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3417                $this->add_error($message, $attribute_name);               
3418            }
3419        }
3420    }
3421
3422    /**
3423     * Validates that a field has the same value as its corresponding confirmation field.
3424     * eg. validates_confirmation_of('password')
3425     *
3426     * @param string|array $attribute_names
3427     * @param string $message
3428     */
3429    function validates_confirmation_of($attribute_names, $message = null) {
3430        $message = $this->get_error_message_for_validation($message, 'confirmation');
3431        foreach((array) $attribute_names as $attribute_name) {         
3432            $attribute_confirmation = $attribute_name . '_confirmation';
3433            if($this->$attribute_confirmation != $this->$attribute_name) {
3434                #$attribute_human = $this->human_attribute_name($attribute_name);
3435                #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3436                $this->add_error($message, $attribute_name);
3437            }
3438        }
3439    }
3440
3441    /**
3442     * Validates that specified attributes are NOT in an array of elements.
3443     * eg. validates_exclusion_of('age, 'in' => array(13, 19))
3444     *
3445     * @param string|array $attribute_names
3446     * @param mixed $in array(1,2,3,4,5) or string 1..5
3447     * @param string $message
3448     */
3449    function validates_exclusion_of($attribute_names, $in = array(), $message = null) {
3450        $message = $this->get_error_message_for_validation($message, 'exclusion');
3451        foreach((array) $attribute_names as $attribute_name) {                 
3452            if(is_string($in)) {
3453                list($minimum, $maximum) = explode('..', $in);
3454                if($this->$attribute_name >= $minimum && $this->$attribute_name <= $maximum) {
3455                    #$attribute_human = $this->human_attribute_name($attribute_name);
3456                    #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3457                    $this->add_error($message, $attribute_name);       
3458                }
3459            } elseif(is_array($in)) {
3460                if(in_array($this->$attribute_name, $in)) {
3461                    #$attribute_human = $this->human_attribute_name($attribute_name);
3462                    #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3463                    $this->add_error($message, $attribute_name);
3464                }
3465            }   
3466        }
3467    }
3468
3469    /**
3470     * Validates that specified attributes matches a regular expression
3471     * eg. validates_format_of('email', '/^(+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i')
3472     *
3473     * @param string|array $attribute_names
3474     * @param string $regex
3475     * @param string $message
3476     */
3477    function validates_format_of($attribute_names, $regex, $message = null) {
3478        $message = $this->get_error_message_for_validation($message, 'invalid');       
3479        foreach((array) $attribute_names as $attribute_name) {                             
3480            $value = $this->$attribute_name;       
3481            # Was there an error?
3482            if(!preg_match($regex, $value)) {
3483                #$attribute_human = $this->human_attribute_name($attribute_name);
3484                #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3485                $this->add_error($message, $attribute_name);
3486            }
3487        }
3488    }
3489
3490    /**
3491     * Validates that specified attributes are in an array of elements.
3492     * eg. validates_inclusion_of('gender', array('m', 'f'))
3493     *
3494     * @param string|array $attribute_names
3495     * @param mixed $in array(1,2,3,4,5) or string 1..5
3496     * @param string $message
3497     */
3498    function validates_inclusion_of($attribute_names, $in = array(), $message = null) {
3499        $message = $this->get_error_message_for_validation($message, 'inclusion');
3500        foreach((array) $attribute_names as $attribute_name) {                 
3501            if(is_string($in)) {
3502                list($minimum, $maximum) = explode('..', $in);
3503                if(!($this->$attribute_name >= $minimum && $this->$attribute_name <= $maximum)) {
3504                    #$attribute_human = $this->human_attribute_name($attribute_name);
3505                    #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3506                    $this->add_error($message, $attribute_name);     
3507                }
3508            } elseif(is_array($in)) {
3509                if(!in_array($this->$attribute_name, $in)) {
3510                    #$attribute_human = $this->human_attribute_name($attribute_name);
3511                    #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3512                    $this->add_error($message, $attribute_name);
3513                }
3514            } 
3515        }
3516    }
3517
3518    /**
3519     * Validates that specified attributes are of some length
3520     * eg. validates_length_of('password', array('minimum' => 8))
3521     *
3522     * @param string|array $attribute_names
3523     * @param array $options
3524     */ 
3525    function validates_length_of($attribute_names, $options = array(
3526        'too_short' => null, 'too_long' => null, 'wrong_length' => null, 'message' => null)) {                     
3527        # Convert 'in' to 'minimum' and 'maximum'
3528        if(isset($options['in'])) {
3529            list($options['minimum'], $options['maximum']) = explode('..', $options['in']);
3530        }
3531        # If 'message' is set see if we need to override other messages
3532        if(isset($options['message'])) {
3533            if(!isset($options['too_short'])) $options['too_short'] = $options['message'];
3534            if(!isset($options['too_long'])) $options['too_long'] = $options['message'];
3535            if(!isset($options['wrong_length'])) $options['wrong_length'] = $options['message'];       
3536        }
3537               
3538        foreach((array) $attribute_names as $attribute_name) {         
3539            # Attribute string length
3540            $len = strlen($this->$attribute_name);
3541            #$attribute_human = $this->human_attribute_name($attribute_name);
3542           
3543            # If you have set the min length option
3544            if(isset($options['minimum'])) {
3545                $message = $this->get_error_message_for_validation($options['too_short'], 'too_short', $options['minimum']);
3546                if($len < $options['minimum']) {
3547                    #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3548                    $this->add_error($message, $attribute_name);                   
3549                }
3550            }
3551           
3552            # If you have set the max length option
3553            if(isset($options['maximum'])) {
3554                $message = $this->get_error_message_for_validation($options['too_long'], 'too_long', $options['maximum']);
3555                if($len > $options['maximum']) {
3556                    #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3557                    $this->add_error($message, $attribute_name);                   
3558                }
3559            }
3560           
3561            # If you have set an exact length option
3562            if(isset($options['is'])) {
3563                $message = $this->get_error_message_for_validation($options['wrong_length'], 'wrong_length', $options['is']);
3564                if($len != $options['is']) {
3565                    #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3566                    $this->add_error($message, $attribute_name);                   
3567                }
3568            }
3569        }
3570    }
3571
3572    /**
3573     * Validates that specified attributes are numbers
3574     * eg. validates_numericality_of('value')
3575     *
3576     * @param string|array $attribute_names
3577     * @param string $message
3578     */
3579    function validates_numericality_of($attribute_names, $message = null, $only_integer = false, $allow_null = false) {     
3580        foreach((array) $attribute_names as $attribute_name) { 
3581            $value = $this->$attribute_name;               
3582            # Skip validation if you allow null
3583            if($allow_null && is_null($value)) {
3584                break;
3585            }           
3586            if($only_integer) {
3587                $message = $this->get_error_message_for_validation($message, 'not_an_integer');
3588                if(!is_integer($value)) {
3589                    #$attribute_human = $this->human_attribute_name($attribute_name);
3590                    #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3591                    $this->add_error($message, $attribute_name);
3592                }
3593            } else {
3594                $message = $this->get_error_message_for_validation($message, 'not_a_number');
3595                if(!is_numeric($value)) {
3596                    #$attribute_human = $this->human_attribute_name($attribute_name);
3597                    #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3598                    $this->add_error($message, $attribute_name);
3599                }
3600            }
3601        }
3602    }
3603       
3604    /**
3605     * Validates that specified attributes are not blank
3606     * eg. validates_presence_of(array('firstname', 'lastname'))
3607     *
3608     * @param string|array $attribute_names
3609     * @param string $message
3610     */
3611    function validates_presence_of($attribute_names, $message = null) {
3612        $message = $this->get_error_message_for_validation($message, 'empty');     
3613        foreach((array) $attribute_names as $attribute_name) {             
3614            if($this->$attribute_name === '' || is_null($this->$attribute_name)) {
3615                #$attribute_human = $this->human_attribute_name($attribute_name);
3616                #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3617                $this->add_error($message, $attribute_name);
3618            }
3619        }
3620    }
3621   
3622    /**
3623     * Validates that specified attributes are unique in the model database table
3624     * eg. validates_uniqueness_of('username')
3625     *
3626     * @param string|array $attribute_names
3627     * @param string $message
3628     */
3629    function validates_uniqueness_of($attribute_names, $message = null) {
3630        $message = $this->get_error_message_for_validation($message, 'taken');
3631        foreach((array) $attribute_names as $attribute_name) {                     
3632            $quoted_value = $this->quote_attribute($attribute_name);
3633            # Conditions for new and existing record
3634            if($this->new_record) {
3635                $conditions = sprintf("%s = %s", $attribute_name, $quoted_value);
3636            } else {
3637                $conditions = sprintf("%s = %s AND %s", $attribute_name, 
3638                    $quoted_value, $this->get_primary_key_conditions("!="));
3639            }   
3640            if($this->find_first($conditions)) {
3641                #$attribute_human = $this->human_attribute_name($attribute_name);
3642                #$this->add_error("{$attribute_human} {$message}", $attribute_name);
3643                $this->add_error($message, $attribute_name);               
3644            }
3645        }
3646    }
3647       
3648    /**
3649     * Return the error message for a validation function
3650     *
3651     * @param string $message
3652     * @param string $key
3653     * @param string $value
3654     * @return string
3655     */
3656    private function get_error_message_for_validation($message, $key, $value = null) {
3657        if(is_null($message)) {
3658            # Return default error message
3659            return sprintf($this->default_error_messages[$key], $value);
3660        } else { 
3661            # Return your custom error message
3662            return $message;
3663        }
3664    }
3665
3666    /**
3667     *  Test whether argument is a PEAR Error object or a MDB2 Error object.
3668     *
3669     *  @param object $obj Object to test
3670     *  @return boolean  Whether object is one of these two errors
3671     */
3672    function is_error($obj) {
3673        if((PEAR::isError($obj)) || (MDB2::isError($obj))) {
3674            return true;
3675        } else {
3676            return false;
3677        }
3678    }
3679
3680    /**
3681     *  Throw an exception describing an error in this object
3682     *
3683     *  @throws {@link ActiveRecordError}
3684     */
3685    function raise($message) {
3686        $error_message  = "Model Class: ".$this->get_class_name()."<br>Error Message:";
3687        if(is_object(self::$db)) {       
3688            list(,$error_code_db, $error_msg_db) = self::$db->errorInfo(); 
3689            $error_message .= " ({$error_code_db}) {$error_msg_db}";
3690        }   
3691        if(!$error_code_db) {
3692            $error_message .= " ".$message;
3693        }
3694        throw new ActiveRecordError($error_message, "ActiveRecord Error", "500");
3695    }
3696
3697    function errors_full_messages() {
3698        $full_messages = array();
3699        foreach((array)$this->errors as $attribute => $message) {
3700            if(is_null($message)) {
3701                continue;
3702            }
3703            $full_messages[] = $this->human_attribute_name($attribute) . " " . $message;
3704        }
3705        return $full_messages;
3706    }
3707   
3708    function errors_on($attribute) {
3709        $errors = isset($this->errors[$attribute]) ? $this->errors[$attribute] : null;
3710        return ((is_array($errors) && count($errors) == 1) ? current($errors) : $errors);
3711    }   
3712
3713    /**
3714     *  Add or overwrite description of an error to the list of errors
3715     *  @param string $error Error message text
3716     *  @param string $key Key to associate with the error (in the
3717     *    simple case, column name).  If omitted, numeric keys will be
3718     *    assigned starting with 0.  If specified and the key already
3719     *    exists in $errors, the old error message will be overwritten
3720     *    with the value of $error.
3721     *  @uses $errors
3722     */
3723    function add_error($error, $key = null) {
3724        if(!is_null($key)) {
3725            $this->errors[$key] = $error;
3726        } else {
3727            $this->errors[] = $error;
3728        }
3729    }
3730
3731    /**
3732     *  Return description of non-fatal errors
3733     *
3734     *  @uses $errors
3735     *  @param boolean $return_string
3736     *    <ul>
3737     *      <li>true => Concatenate all error descriptions into a string
3738     *        using $seperator between elements and return the
3739     *        string</li>
3740     *      <li>false => Return the error descriptions as an array</li>
3741     *    </ul>
3742     *  @param string $seperator  String to concatenate between error
3743     *    descriptions if $return_string == true
3744     *  @return mixed Error description(s), if any
3745     */
3746    function get_errors($return_string = false, $seperator = "<br>") {
3747        if($return_string && count($this->errors) > 0) {
3748            return implode($seperator, $this->errors);
3749        } else {
3750            return $this->errors;
3751        }
3752    }
3753
3754    /**
3755     *  Return errors as a string.
3756     *
3757     *  Concatenate all error descriptions into a stringusing
3758     *  $seperator between elements and return the string.
3759     *  @param string $seperator  String to concatenate between error
3760     *    descriptions
3761     *  @return string Concatenated error description(s), if any
3762     */
3763    function get_errors_as_string($seperator = "<br>") {
3764        return $this->get_errors(true, $seperator);
3765    } 
3766
3767    /**
3768     *  Log SQL query in development mode
3769     *
3770     *  If running in development mode, log the query to self::$query_log
3771     *  @param string SQL to be logged
3772     */
3773    function log_query($query) {   
3774        if((self::$environment != 'production' || self::$log_all) && $query) {
3775            self::$query_log[] = $query;       
3776        }   
3777    }
3778
3779    /**
3780     *  For debugging to see what the attributes for this object are.
3781     *
3782     *  echo User => Array([id] => 1, [name] => John)
3783     */   
3784    function __toString() {
3785        return print_r($this->get_attributes(), true);
3786    }
3787
3788}
3789
3790
3791
3792// -- set Emacs parameters --
3793// Local variables:
3794// tab-width: 4
3795// c-basic-offset: 4
3796// c-hanging-comment-ender-p: nil
3797// indent-tabs-mode: nil
3798// End:
3799?>
Note: See TracBrowser for help on using the browser.