[Solar-svn] Revision 2798

pmjones at solarphp.com pmjones at solarphp.com
Fri Sep 28 21:32:48 CDT 2007


branch: Solar_Sql_Model and related

(Breaks, breaks, and more breaks.  The horror ... the horror ...)


Solar_Sql_Model_Catalog
-----------------------

BALEETED! Now using adapter-level caching of table lists and table column
definitions, and re-building related properties as needed, instead of keeping
a catalog of model definitions. Slower but less memory intensive, and lends
itself to better testing (in theory).


Solar_Sql_Model_Related*
------------------------

Definitions for querying related models, as well as Select creation, are now
stored in these classes. One class each for BelongsTo, HasOne, and HasMany
(including has many "through").

**Of note, the 'foreign_model' key is now called 'foreign_class', to distinguish
the class name from the foreign model instance.**

Throughout the Model classes, where we used to refer to $related as an array,
we now refer to $related as an object.


Solar_Sql_Model_Record
----------------------

* [CHG] New property $_lazy_load is used to turn lazy-loading of related
  records off and on. We turn it off at save() time, for example, so that the
  filter mechanism does not lazy-load the related properties, thus causing
  lots of SQL hits (and incidentally, sometimes, infinite recursion).

* [CHG] In method filter(), after filtering is complete, we now call __destruct()
  on the filter and unset it to reclaim memory.


Solar_Sql_Model
---------------

* [ADD] Method __destruct() now unsets related definition objects. It's not
  enough to `unset($model)` when you're done with it. To fully reclaim memory
  from all internal objects, you need to issue `$model->__destruct()` and then
  `unset($model)`.

* [FIX] Method __get() now correctly returns empty properties.

* [CHG] Protected method _fixSelectParams() is now public fixSelectParams().

* [ADD] Added support methods _fetchAll(), _fetchAssoc() and _fetchOne() to
  automate adding of eager related records for extended classes needing custom
  fetch*() methods.

* [DEL] Method newSelectRelated() is now Solar_Sql_Model_Related::newSelect().

* [ADD] Method getRelated() now lazy-loads and returns the related definition
  controller object.

* [ADD] Several _fix*() methods previously from Solar_Sql_Catalog, to "fix"
  critical user-defined aspects of the Model instance.




Modified: branches/orm/Solar/Sql/Model/Collection.php
===================================================================
--- branches/orm/Solar/Sql/Model/Collection.php	2007-09-29 02:04:13 UTC (rev 2797)
+++ branches/orm/Solar/Sql/Model/Collection.php	2007-09-29 02:32:47 UTC (rev 2798)
@@ -96,8 +96,8 @@
      */
     public function loadRelated($name, $data)
     {
-        $opts = $this->_model->related[$name];
-        if ($opts['type'] == 'has_many') {
+        $related = $this->_model->getRelated($name);
+        if ($related->type == 'has_many') {
             foreach ($data as $item) {
                 $id = array_shift($item);
                 $this->_related[$name][$id][] = $item;
@@ -196,16 +196,7 @@
     //
     // -----------------------------------------------------------------
     
-    /**
-     * 
-     * ArrayAccess: get a key value.
-     * 
-     * @param string $key The requested key.
-     * 
-     * @return mixed
-     * 
-     */
-    public function offsetGet($key)
+    public function __get($key)
     {
         // convert array to record object
         // honors single-table inheritance
@@ -257,6 +248,6 @@
             }
         }
         
-        return $this->_data[$key] = $val;
+        return $this->__set($key, $val);
     }
 }
\ No newline at end of file

Modified: branches/orm/Solar/Sql/Model/Record.php
===================================================================
--- branches/orm/Solar/Sql/Model/Record.php	2007-09-29 02:04:13 UTC (rev 2797)
+++ branches/orm/Solar/Sql/Model/Record.php	2007-09-29 02:32:47 UTC (rev 2798)
@@ -103,6 +103,17 @@
     
     /**
      * 
+     * Tells whether or not __get() should lazy-load relateds.
+     * 
+     * We need this so that when saving, we don't load every related record.
+     * 
+     * @var bool
+     * 
+     */
+    protected $_lazy_load = true;
+    
+    /**
+     * 
      * Magic getter for record properties; automatically calls __getColName()
      * methods when they exist.
      * 
@@ -117,12 +128,14 @@
         $this->_checkDeleted();
         
         // do we need to load relationship data?
-        $load_related = empty($this->_data[$key]) &&
+        $load_related = $this->_lazy_load &&
+                        empty($this->_data[$key]) &&
                         ! empty($this->_model->related[$key]);
         
         if ($load_related) {
             // the key was for a relation that has no data yet.
-            // load the data.
+            // load the data.  don't return at this point, look
+            // for accessor methods later.
             $this->_data[$key] = $this->_model->fetchRelatedObject(
                 $this,
                 $key,
@@ -130,9 +143,6 @@
             );
         }
         
-        // default value
-        $result = null;
-        
         // if an accessor method exists, use it
         if (! empty($this->_access_methods['get'][$key])) {
             // use accessor method
@@ -287,14 +297,17 @@
         }
         
         // load related data as records and collections
-        foreach ($this->_model->related as $name => $opts) {
+        $list = array_keys($this->_model->related);
+        foreach ($list as $name) {
             
+            $related = $this->_model->getRelated($name);
+            
             // is this a "to-one" association with data already in place?
-            $type = $opts['type'];
+            $type = $related->type;
             if (($type == 'has_one' || $type == 'belongs_to') && ! empty($this->_data[$name])) {
                     
                 // create a record object from the related model
-                $model = Solar::factory($opts['foreign_model'], array(
+                $model = Solar::factory($related->foreign_class, array(
                     'sql' => $this->_model->sql
                 ));
                 $this->_data[$name] = $model->newRecord($this->_data[$name]);
@@ -302,7 +315,7 @@
             } elseif ($type == 'has_many' && ! empty($this->_data[$name])) {
                 
                 // create a collection object from the related model
-                $model = Solar::factory($opts['foreign_model'], array(
+                $model = Solar::factory($related->foreign_class, array(
                     'sql' => $this->_model->sql
                 ));
                 $this->_data[$name] = $model->newCollection($this->_data[$name]);
@@ -312,8 +325,8 @@
                 $this->_data[$name] = null;
             }
             
-            // either way, set the default related page
-            $this->_related_page[$name] = $opts['page'];
+            // by default get all related records
+            $this->_related_page[$name] = 0;
         }
     }
     
@@ -414,9 +427,11 @@
             
             if ($empty_related) {
                 
+                $related = $this->_model->getRelated($key);
+                
                 // do not fetch related just for array convertion, this leads
                 // to some deep (perhaps infinite?) recurstion
-                if ($this->_model->related[$key]['type'] == 'has_many') {
+                if ($related->type == 'has_many') {
                     $val = array();
                 } else {
                     $val = null;
@@ -452,17 +467,6 @@
      * 
      * Saves this record to the database, inserting or updating as needed.
      * 
-     * Does not currently save related values, but expected behavior will be:
-     * 
-     * - *Do not* save belongsTo() relationships
-     * - *Do* save hasOne() relationships
-     * 
-     * Some questions:
-     * 
-     * - How to handle one-to-many?
-     * - How to handle many-to-many?
-     * - Use a transaction so we can roll back on failures in related records?
-     * 
      * Hook methods:
      * 
      * 1. `_preSave()` runs before all save operations.
@@ -476,12 +480,15 @@
      * 5. `_postSave()` runs after all save operations, but before related
      *    records are saved.
      * 
+     * 
+     * After _postSave(), we save each related Record and Collection object.
+     * 
      * @param array $data An associative array of data to merge with existing
      * record data.
      * 
      * @return bool
      * 
-     * @todo How to tell when related saves are invalid?  Throw an exception?
+     * @todo Automatic connection of related IDs to each other?
      * 
      */
     public function save($data = null)
@@ -500,6 +507,9 @@
             // pre-save routine
             $this->_preSave();
             
+            // turn off lazy loading
+            $this->_lazy_load = false;
+            
             // insert or update based on primary key value
             $primary = $this->_model->primary_col;
             if (empty($this->$primary)) {
@@ -508,6 +518,9 @@
                 $this->_update();
             }
             
+            // turn on lazy-loading for post-save routines
+            $this->_lazy_load = true;
+            
             // post-save routine
             $this->_postSave();
         }
@@ -821,11 +834,18 @@
         // tell the filter to use the model for locale strings
         $filter->setChainLocaleObject($this->_model);
         
-        // apply filters
+        // apply filters and retain invalids
         $valid = $filter->applyChain($this);
+        $invalid = $filter->getChainInvalid();
+        
+        // reclaim memory
+        $filter->__destruct();
+        unset($filter);
+        
+        // was it valid?
         if (! $valid) {
             $this->_status = 'invalid';
-            $this->_invalid = $filter->getChainInvalid();
+            $this->_invalid = $invalid;
             throw $this->_exception('ERR_INVALID', array($this->_invalid));
         }
         

Added: branches/orm/Solar/Sql/Model/Related/BelongsTo.php
===================================================================
--- branches/orm/Solar/Sql/Model/Related/BelongsTo.php	                        (rev 0)
+++ branches/orm/Solar/Sql/Model/Related/BelongsTo.php	2007-09-29 02:32:47 UTC (rev 2798)
@@ -0,0 +1,52 @@
+<?php
+class Solar_Sql_Model_Related_BelongsTo extends Solar_Sql_Model_Related {
+    
+    protected function _setType()
+    {
+        $this->type = 'belongs_to';
+    }
+    
+    // foreign key is stored in the native model
+    protected function _fixRelatedCol(&$opts)
+    {
+        $opts['native_col'] = $opts['foreign_key'];
+    }
+    
+    /**
+     * 
+     * A support method for _fixRelated() to handle belongs-to relationships.
+     * 
+     * @param array &$opts The relationship options; these are modified in-
+     * place.
+     * 
+     * @param StdClass $foreign The catalog entry for the foreign model.
+     * 
+     * @return void
+     * 
+     */
+    protected function _setRelated($opts)
+    {
+        // the foreign column
+        if (empty($opts['foreign_col'])) {
+            // named by foreign primary key
+            $this->foreign_col = $this->_foreign_model->primary_col;
+        } else {
+            $this->foreign_col = $opts['foreign_col'];
+        }
+        
+        // the native column
+        if (empty($opts['native_col'])) {
+            // named by foreign table's suggested foreign_col name
+            $this->native_col = $this->_foreign_model->foreign_col;
+        } else {
+            $this->native_col = $opts['native_col'];
+        }
+        
+        // the fetch type
+        if (empty($opts['fetch'])) {
+            $this->fetch = 'one';
+        } else {
+            $this->fetch = $opts['fetch'];
+        }
+    }
+}
\ No newline at end of file

Added: branches/orm/Solar/Sql/Model/Related/HasMany.php
===================================================================
--- branches/orm/Solar/Sql/Model/Related/HasMany.php	                        (rev 0)
+++ branches/orm/Solar/Sql/Model/Related/HasMany.php	2007-09-29 02:32:47 UTC (rev 2798)
@@ -0,0 +1,187 @@
+<?php
+class Solar_Sql_Model_Related_HasMany extends Solar_Sql_Model_Related {
+    
+    protected function _setType()
+    {
+        $this->type = 'has_many';
+    }
+    
+    // foreign key is stored in the foreign model
+    protected function _fixRelatedCol(&$opts)
+    {
+        $opts['foreign_col'] = $opts['foreign_key'];
+    }
+    
+    /**
+     * 
+     * A support method for _fixRelated() to handle has-many relationships.
+     * 
+     * @param array &$opts The relationship options; these are modified in-
+     * place.
+     * 
+     * @param StdClass $foreign The catalog entry for the foreign model.
+     * 
+     * @return void
+     * 
+     */
+    protected function _setRelated($opts)
+    {
+        // are we working through another relationship?
+        if (! empty($opts['through'])) {
+            // through another relationship, hand off to another method
+            return $this->_setRelatedThrough($opts);
+        }
+        
+        // the foreign column
+        if (empty($opts['foreign_col'])) {
+            // named by native table's suggested foreign_col name
+            $this->foreign_col = $this->_native_model->foreign_col;
+        } else {
+            $this->foreign_col = $opts['foreign_col'];
+        }
+        
+        // the native column
+        if (empty($opts['native_col'])) {
+            // named by native primary key
+            $this->native_col = $this->_native_model->primary_col;
+        } else {
+            $this->native_col = $opts['native_col'];
+        }
+        
+        // the fetch type
+        if (empty($opts['fetch'])) {
+            $this->fetch = 'all';
+        } else {
+            $this->fetch = $opts['fetch'];
+        }
+    }
+    
+    /**
+     * 
+     * A support method for _fixRelatedHasMany() to handle "through"
+     * relationships.
+     * 
+     * @param array &$opts The relationship options; these are modified in-
+     * place.
+     * 
+     * @param StdClass $foreign The catalog entry for the foreign model.
+     * 
+     * @return void
+     * 
+     */
+    protected function _setRelatedThrough($opts)
+    {
+        // get the "through" relationship control
+        $through = $this->_native_model->getRelated($opts['through']);
+        $this->through = $opts['through'];
+        
+        // the foreign column
+        if (empty($opts['foreign_col'])) {
+            // named by foreign primary key (e.g., foreign.id)
+            $this->foreign_col = $this->_foreign_model->primary_col;
+        } else {
+            $this->foreign_col = $opts['foreign_col'];
+        }
+        
+        // the native column
+        if (empty($opts['native_col'])) {
+            // named by native primary key (e.g., native.id)
+            $this->native_col = $this->_native_model->primary_col;
+        } else {
+            $this->native_col = $opts['native_col'];
+        }
+        
+        // get the through-table
+        if (empty($opts['through_table'])) {
+            $this->through_table = $through->foreign_table;
+        } else {
+            $this->through_table = $opts['through_table'];
+        }
+        
+        // get the through-alias
+        if (empty($opts['through_alias'])) {
+            $this->through_alias = $through->foreign_alias;
+        } else {
+            $this->through_alias = $opts['through_alias'];
+        }
+        
+        // a little magic
+        if (empty($opts['through_native_col']) &&
+            empty($opts['through_foreign_col']) &&
+            ! empty($opts['through_key'])) {
+            // pre-define through_foreign_col
+            $opts['through_foreign_col'] = $opts['through_key'];
+        }
+        
+        // what's the native model key in the through table?
+        if (empty($opts['through_native_col'])) {
+            $this->through_native_col = $through->foreign_col;
+        } else {
+            $this->through_native_col = $opts['through_native_col'];
+        }
+        
+        // what's the foreign model key in the through table?
+        if (empty($opts['through_foreign_col'])) {
+            $this->through_foreign_col = $this->_foreign_model->foreign_col;
+        } else {
+            $this->through_foreign_col = $opts['through_foreign_col'];
+        }
+        
+        // the fetch type
+        if (empty($opts['fetch'])) {
+            $this->fetch = 'all';
+        } else {
+            $this->fetch = $opts['fetch'];
+        }
+    }
+    
+    protected function _modSelect($select, $spec)
+    {
+        // for non-through relationship, go with the parent method
+        if (! $this->through) {
+            return parent::_modSelect($select, $spec);
+        }
+        
+        // more-complex 'has_many through' relationship.
+        // join through the mapping table.
+        $join_table = "{$this->through_table} AS {$this->through_alias}";
+        $join_where = "{$this->foreign_alias}.{$this->foreign_col} = "
+                    . "{$this->through_alias}.{$this->through_foreign_col}";
+        
+        $select->leftJoin($join_table, $join_where);
+        
+        // how to filter rows?
+        if ($spec instanceof Solar_Sql_Model_Record) {
+            // restrict to the related native column value in the "through" table
+            $select->where(
+                "{$this->through_alias}.{$this->through_native_col} = ?",
+                $spec->{$this->native_col} // this is where we set the filtering clause
+            );
+        } else {
+            // $spec is a Select object. restrict to a sub-query of IDs
+            // from the native table as an inner join.
+            $inner = str_replace("\n", "\n\t", $spec->fetchSql());
+            
+            // add the native table ID at the top through a join
+            $select->innerJoin(
+                "($inner) AS {$this->native_alias}",
+                "{$this->through_alias}.{$this->through_native_col} = {$this->native_alias}.{$this->native_col}",
+                "{$this->native_col} AS {$this->native_alias}__{$this->native_col}"
+            );
+        }
+        
+        // select from the foreign table.
+        $select->from(
+            "{$this->foreign_table} AS {$this->foreign_alias}",
+            $this->cols
+        );
+        
+        // honor foreign inheritance
+        if ($this->foreign_inherit_col) {
+            $select->where(
+                "{$this->foreign_alias}.{$this->foreign_inherit_col} = ?",
+                $this->foreign_inherit_val
+            );
+        }
+    }
+}
\ No newline at end of file

Added: branches/orm/Solar/Sql/Model/Related/HasOne.php
===================================================================
--- branches/orm/Solar/Sql/Model/Related/HasOne.php	                        (rev 0)
+++ branches/orm/Solar/Sql/Model/Related/HasOne.php	2007-09-29 02:32:47 UTC (rev 2798)
@@ -0,0 +1,52 @@
+<?php
+class Solar_Sql_Model_Related_HasOne extends Solar_Sql_Model_Related {
+    
+    protected function _setType()
+    {
+        $this->type = 'has_one';
+    }
+    
+    // foreign key is stored in the foreign model
+    protected function _fixRelatedCol(&$opts)
+    {
+        $opts['foreign_col'] = $opts['foreign_key'];
+    }
+    
+    /**
+     * 
+     * A support method for _fixRelated() to handle has-one relationships.
+     * 
+     * @param array &$opts The relationship options; these are modified in-
+     * place.
+     * 
+     * @param StdClass $foreign The catalog entry for the foreign model.
+     * 
+     * @return void
+     * 
+     */
+    protected function _setRelated($opts)
+    {
+        // the foreign column
+        if (empty($opts['foreign_col'])) {
+            // named by native table's suggested foreign_col name
+            $this->foreign_col = $this->_native_model->foreign_col;
+        } else {
+            $this->foreign_col = $opts['foreign_col'];
+        }
+        
+        // the native column
+        if (empty($opts['native_col'])) {
+            // named by native primary key
+            $this->native_col = $this->_native_model->primary_col;
+        } else {
+            $this->native_col = $opts['native_col'];
+        }
+        
+        // the fetch type
+        if (empty($opts['fetch'])) {
+            $this->fetch = 'one';
+        } else {
+            $this->fetch = $opts['fetch'];
+        }
+    }
+}
\ No newline at end of file

Added: branches/orm/Solar/Sql/Model/Related.php
===================================================================
--- branches/orm/Solar/Sql/Model/Related.php	                        (rev 0)
+++ branches/orm/Solar/Sql/Model/Related.php	2007-09-29 02:32:47 UTC (rev 2798)
@@ -0,0 +1,558 @@
+<?php
+// <http://ar.rubyonrails.com/classes/ActiveRecord/Associations/ClassMethods.html>
+abstract class Solar_Sql_Model_Related extends Solar_Base {
+    
+    public $name;
+    
+    public $type;
+    
+    /**
+     * 
+     * The class of the native model.
+     * 
+     * @var string
+     * 
+     */
+    public $native_class;
+    
+    /**
+     * 
+     * The name of the native table.
+     * 
+     * @var string
+     * 
+     */
+    public $native_table;
+    
+    /**
+     * 
+     * The alias for the native table.
+     * 
+     * @var string
+     * 
+     */
+    public $native_alias;
+    
+    /**
+     * 
+     * The native column to match against the foreign primary column.
+     * 
+     * @var string
+     * 
+     */
+    public $native_col;
+    
+    /**
+     * 
+     * `foreign_class`
+     * : (string) The class name of the foreign model. Default is the first
+     *   matching class for the relationship name, as loaded from the parent
+     *   class stack. Automatically honors single-table inheritance.
+     * 
+     * 
+     * @var string
+     * 
+     */
+    public $foreign_class;
+    
+    /**
+     * 
+     * `foreign_table`
+     * : (string) The name of the table for the foreign model. Default is the
+     *   table specified by the foreign model.
+     * 
+     * @var string
+     * 
+     */
+    public $foreign_table;
+    
+    /**
+     * 
+     * `foreign_alias`
+     * : (string) Aliases the foreign table to this name. Default is the
+     *   relationship name.
+     * 
+     * @var string
+     * 
+     */
+    public $foreign_alias;
+    
+    /**
+     * 
+     * `foreign_col`
+     * : (string) The name of the column to join with in the *foreign* table.
+     *   This forms one-half of the relationship.  Default is per association
+     *   type.
+     * 
+     * @var string
+     * 
+     */
+    public $foreign_col;
+    
+    /**
+     * 
+     * The name of the foreign primary column.
+     * 
+     * @var string
+     * 
+     */
+    public $foreign_primary_col;
+    
+    /**
+     * 
+     * `foreign_inherit_col`
+     * : (string) If the foreign model uses single-table inheritance, this is
+     *   the column where the inheritance value is stored.
+     * 
+     * 
+     * @var string
+     * 
+     */
+    public $foreign_inherit_col;
+    
+    /**
+     * 
+     * `foreign_inherit_val`
+     * : (string) If the foreign model has an inheritance type, the value of
+     *   that inheritance type (as stored in foreign_inherit_col).
+     * 
+     * @var string
+     * 
+     */
+    public $foreign_inherit_val;
+    
+    /**
+     * 
+     * The relationship name through which we find foreign records.
+     * 
+     * @var string
+     * 
+     */
+    public $through;
+    
+    /**
+     * 
+     * The "through" table name.
+     * 
+     * @var string
+     * 
+     */
+    public $through_table;
+    
+    /**
+     * 
+     * The "through" table alias.
+     * 
+     * @var string
+     * 
+     */
+    public $through_alias;
+    
+    /**
+     * 
+     * In the "through" table, the column that has the matching native value.
+     * 
+     * @var string
+     * 
+     */
+    public $through_native_col;
+    
+    /**
+     * 
+     * In the "through" table, the column that has the matching foreign value.
+     * 
+     * @var string
+     * 
+     */
+    public $through_foreign_col;
+    
+    /**
+     * 
+     * When fetching records, use DISTINCT ?
+     * 
+     * @var bool
+     * 
+     */
+    public $distinct;
+    
+    /**
+     * 
+     * Fetch these columns for the related records.
+     * 
+     * @var string|array
+     * 
+     */
+    public $cols;
+    
+    /**
+     * 
+     * Additional WHERE clauses when fetching records.
+     * 
+     * @var string|array
+     * 
+     */
+    public $where;
+    
+    /**
+     * 
+     * Additional GROUP clauses when fetching records.
+     * 
+     * @var string|array
+     * 
+     */
+    public $group;
+    
+    /**
+     * 
+     * Additional HAVING clauses when fetching records.
+     * 
+     * @var string|array
+     * 
+     */
+    public $having;
+    
+    /**
+     * 
+     * Additional ORDER clauses when fetching records.
+     * 
+     * @var string|array
+     * 
+     */
+    public $order;
+    
+    /**
+     * 
+     * When fetching records, use this many records per page of results.
+     * 
+     * @var int
+     * 
+     */
+    public $paging;
+    
+    /**
+     * 
+     * The fetch type to use: 'one', 'all', 'assoc', etc.
+     * 
+     * @var string
+     * 
+     */
+    public $fetch;
+    
+    /**
+     * 
+     * There is a virtual element called `foreign_key` that automatically
+     * populates the `native_col` or `foreign_col` value for you, based on the
+     * association type.  This will be used **only** when `native_col` **and**
+     * `foreign_col` are not set.
+     * 
+     * @var string
+     * 
+     */
+    // public $foreign_key;
+    
+    /**
+     * 
+     * There is a virtual element called `through_key` that automatically 
+     * populates the 'through_foreign_col' value for you.
+     * 
+     * @var string.
+     * 
+     */
+    // public $through_key;
+    
+    protected $_native_model;
+    
+    protected $_foreign_model;
+    
+    public function setNativeModel($model)
+    {
+        $this->_native_model = $model;
+        $this->native_class = $this->_native_model->class;
+        $this->native_table = $this->_native_model->table_name;
+        $this->native_alias = $this->_native_model->model_name;
+    }
+    
+    // gets the *related* model, not the native model
+    public function getModel()
+    {
+        return $this->_foreign_model;
+    }
+    
+    public function toArray()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $val) {
+            if ($key[0] == '_') {
+                unset($vars[$key]);
+            }
+        }
+        return $vars;
+    }
+    
+    /**
+     * 
+     * Corrects the relationship definitions.
+     * 
+     * @return void
+     * 
+     */
+    public function load($opts)
+    {
+        $this->name = $opts['name'];
+        $this->_setType();
+        $this->_setForeignModel($opts);
+        $this->_setCols($opts);
+        $this->_setSelect($opts);
+        
+        // if the user has specified *neither* a foreign_col *nor* a native_col,
+        // but *has* specified a foreign_key, use the foreign_key to define 
+        // the foreign_col or native col (depending on relation type). 
+        if (empty($opts['native_col']) &&
+            empty($opts['foreign_col']) &&
+            ! empty($opts['foreign_key'])) {
+            // redefine based on the "virtual" foreign_key value
+            $this->_fixRelatedCol($opts);
+        }
+        
+        $this->_setRelated($opts);
+    }
+    
+    public function newSelect($spec)
+    {
+        // specification must be a record, or params for a select
+        if (! ($spec instanceof Solar_Sql_Model_Record) && ! is_array($spec)) {
+            throw $this->_exception('ERR_RELATED_SPEC', array(
+                'spec' => $spec
+            ));
+        }
+        
+        // convert $spec array to a Select object for the native column ID list
+        if (is_array($spec)) {
+            // build the select
+            $params = $spec;
+            $spec = $this->_native_model->newSelect();
+            $spec->distinct($params['distinct'])
+                 ->from("{$this->native_table} AS {$this->native_alias}", $this->native_col)
+                 ->multiWhere($params['where'])
+                 ->group($params['group'])
+                 ->having($params['having'])
+                 ->order($params['order'])
+                 ->setPaging($params['paging'])
+                 ->limitPage($params['page']);
+        }
+        
+        // get a select object for the related rows
+        $select = Solar::factory(
+            $this->_native_model->select_class,
+            array('sql' => $this->_native_model->sql)
+        );
+        
+        // modify the select per-relationship. only has-many-through uses
+        // non-standard modification.
+        $this->_modSelect($select, $spec);
+        
+        // add remaining clauses
+        $select->distinct($this->distinct)
+               ->multiWhere($this->where)
+               ->group($this->group)
+               ->having($this->having)
+               ->order($this->order)
+               ->setPaging($this->paging);
+        
+        // done
+        return $select;
+    }
+    
+    protected function _modSelect($select, $spec)
+    {
+        // simple belongs_to, has_one, or has_many.
+        if ($spec instanceof Solar_Sql_Model_Record) {
+            // restrict to the related native column value in the foreign table
+            $select->where(
+                "{$this->foreign_alias}.{$this->foreign_col} = ?",
+                $spec->{$this->native_col}
+            );
+        } else {
+            // $spec is a Select object
+            // restrict to a sub-select of IDs from the native table
+            $inner = str_replace("\n", "\n\t\t", $spec->fetchSql());
+            // add the native table ID at the top through a join
+            $select->innerJoin(
+                "($inner) AS {$this->native_alias}",
+                "{$this->foreign_alias}.{$this->foreign_col} = {$this->native_alias}.{$this->native_col}",
+                "{$this->native_col} AS {$this->native_alias}__{$this->native_col}"
+            );
+        }
+        
+        // select columns from the foreign table.
+        $select->from(
+            "{$this->foreign_table} AS {$this->foreign_alias}",
+            $this->cols
+        );
+        
+        // honor foreign inheritance
+        if ($this->foreign_inherit_col) {
+            $select->where(
+                "{$this->foreign_alias}.{$this->foreign_inherit_col} = ?",
+                $this->foreign_inherit_val
+            );
+        }
+    }
+    
+    protected function _setForeignModel($opts)
+    {
+        // make sure we have at least a base class name
+        if (empty($opts['foreign_class'])) {
+            $this->foreign_class = $opts['name'];
+        } else {
+            $this->foreign_class = $opts['foreign_class'];
+        }
+        
+        // can we load a related model class from the hierarchy stack?
+        $class = $this->_native_model->stack->load($this->foreign_class, false);
+        
+        // did we find it?
+        if (! $class) {
+            // look for a "parallel" class name, based on where the word
+            // "Model" is in the current class name. this lets you pull
+            // model classes from the same level, not from the inheritance
+            // stack.
+            $pos = strrpos($this->native_class, 'Model_');
+            if ($pos !== false) {
+                $pos += 6; // "Model_"
+                $tmp = substr($this->native_class, 0, $pos) . ucfirst($this->foreign_class);
+                try {
+                    Solar::loadClass($tmp);
+                    // if no exception, $class gets set
+                    $class = $tmp;
+                } catch (Exception $e) {
+                    // do nothing
+                }
+            }
+        }
+        
+        // last chance: do we *still* need a class name?
+        if (! $class) {
+            // not in the hierarchy, and no parallel class name. look for the
+            // model class literally. this will throw an exception if the
+            // class cannot be found anywhere.
+            try {
+                Solar::loadClass($this->foreign_class);
+                // if no exception, $class gets set
+                $class = $this->foreign_class;
+            } catch (Solar_Exception $e) {
+                throw $this->_exception('ERR_LOAD_FOREIGN_MODEL', array(
+                    'native_model' => $this->_native_model->class,
+                    'related_name' => $opts['name'],
+                    'foreign_class' => $this->foreign_class,
+                ));
+            }
+        }
+        
+        // finally we have a class name, keep it as the foreign model class
+        $this->foreign_class = $class;
+        
+        // create a foreign model instance
+        $this->_foreign_model = Solar::factory( $this->foreign_class, array(
+            'sql' => $this->_native_model->sql
+        ));
+        
+        // get its table name
+        $this->foreign_table = $this->_foreign_model->table_name;
+        
+        // and its primary column
+        $this->foreign_primary_col = $this->_foreign_model->primary_col;
+        
+        // set the foreign alias based on the relationship name
+        $this->foreign_alias = $opts['name'];
+    }
+    
+    
+    protected function _setCols($opts)
+    {
+        // the list of foreign table cols to retrieve
+        if (empty($opts['cols'])) {
+            $this->cols = $this->_foreign_model->fetch_cols;
+        } elseif (is_string($opts['cols'])) {
+            $this->cols = explode(',', $opts['cols']);
+        } else {
+            $this->cols = (array) $opts['cols'];
+        }
+        
+        // make sure we always retrieve the foreign primary key value,
+        // if there is one.
+        $primary = $this->_foreign_model->primary_col;
+        if ($primary && ! in_array($primary, $this->cols)) {
+            $this->cols[] = $primary;
+        }
+        
+        // if inheritance is turned on for the foreign model,
+        // make sure we always retrieve the foreign inheritance value.
+        $inherit = $this->_foreign_model->inherit_col;
+        if ($inherit && ! in_array($inherit, $this->cols)) {
+            $this->cols[] = $inherit;
+        }
+        
+        // if inheritance is turned on, force the foreign_inherit
+        // column and value
+        if ($this->_foreign_model->inherit_col && $this->_foreign_model->inherit_model) {
+            $this->foreign_inherit_col = $this->_foreign_model->inherit_col;
+            $this->foreign_inherit_val = $this->_foreign_model->inherit_model;
+        } else {
+            $this->foreign_inherit_col = null;
+            $this->foreign_inherit_val = null;
+        }
+    }
+    
+    protected function _setSelect($opts)
+    {
+        // distinct
+        if (empty($opts['distinct'])) {
+            $this->distinct = false;
+        } else {
+            $this->distinct = (bool) $opts['distinct'];
+        }
+        
+        // where
+        if (empty($opts['where'])) {
+            $this->where = null;
+        } else {
+            $this->where = (array) $opts['where'];
+        }
+        
+        // group
+        if (empty($opts['group'])) {
+            $this->group = null;
+        } else {
+            $this->group = (array) $opts['group'];
+        }
+        
+        // having
+        if (empty($opts['having'])) {
+            $this->having = null;
+        } else {
+            $this->having = (array) $opts['having'];
+        }
+        
+        // order
+        if (empty($opts['order'])) {
+            // default to the foreign primary key
+            $this->order = array("{$this->foreign_alias}.{$this->_foreign_model->primary_col}");
+        } else {
+            $this->order = (array) $opts['order'];
+        }
+        
+        // paging from the foreign model
+        if (empty($opts['paging'])) {
+            $this->paging = $this->_foreign_model->paging;
+        } else {
+            $this->paging = (int) $opts['paging'];
+        }
+    }
+    
+    abstract protected function _setType();
+    
+    abstract protected function _fixRelatedCol(&$opts);
+    
+    abstract protected function _setRelated($opts);
+}
\ No newline at end of file

Modified: branches/orm/Solar/Sql/Model.php
===================================================================
--- branches/orm/Solar/Sql/Model.php	2007-09-29 02:04:13 UTC (rev 2797)
+++ branches/orm/Solar/Sql/Model.php	2007-09-29 02:32:47 UTC (rev 2798)
@@ -456,29 +456,35 @@
         // connect to the database
         $this->_sql = Solar::dependency('Solar_Sql', $this->_config['sql']);
         
-        // attach to the model catalog. be sure to use the same SQL
-        // connection for the catalog as in the model.
-        $catalog = Solar::factory(
-            'Solar_Sql_Model_Catalog',
-            array('sql' => $this->_sql)
-        );
-        
         // our class name so that we don't call get_class() all the time
         $this->_class = get_class($this);
         
-        // do we have a catalog entry for this model class yet?
-        if (! $catalog->exists($this->_class)) {
-            // call user-defined setup
-            $this->_setup();
-            // set and fix properties in the catalog.
-            $catalog->set($this->_class, get_object_vars($this));
+        // user-defined setup
+        $this->_setup();
+    
+        // follow-on cleanup of critical user-defined values
+        $this->_fixStack();
+        $this->_fixTableName();
+        $this->_fixIndex();
+        $this->_fixTableCols(); // also creates table if needed
+        $this->_fixModelName();
+        $this->_fixOrder();
+        $this->_fixPropertyCols();
+        $this->_fixFilters(); // including filter class
+    }
+    
+    /**
+     * 
+     * Call this before you 
+     * 
+     * @param array $config User-provided configuration values.
+     * 
+     */
+    public function __destruct()
+    {
+        foreach ($this->_related as $key => $val) {
+            unset($this->_related[$key]);
         }
-        
-        // get fixed properties back from the catalog
-        $vars = $catalog->get($this->_class);
-        foreach ($vars as $key => $val) {
-            $this->$key = $val;
-        }
     }
     
     /**
@@ -509,10 +515,11 @@
     public function __get($key)
     {
         $var = "_$key";
-        if (isset($this->$var)) {
+        if (property_exists($this, $var)) {
             return $this->$var;
         } else {
             throw $this->_exception('ERR_PROPERTY_NOT_DEFINED', array(
+                'class' => get_class($this),
                 'key' => $key,
                 'var' => $var,
             ));
@@ -694,7 +701,7 @@
     public function fetchAll($params = array())
     {
         // setup
-        $params = $this->_fixSelectParams($params);
+        $params = $this->fixSelectParams($params);
         $select = $this->newSelect($params['eager']);
         
         // build
@@ -718,9 +725,10 @@
         if ($data) {
             $coll = $this->newCollection($data);
             foreach ((array) $params['eager'] as $name) {
-                if ($this->_related[$name]['type'] == 'has_many') {
-                    $related = $this->fetchRelatedArray($params, $name);
-                    $coll->loadRelated($name, $related);
+                $related = $this->getRelated($name);
+                if ($related->type == 'has_many') {
+                    $data = $this->fetchRelatedArray($params, $name);
+                    $coll->loadRelated($name, $data);
                 }
             }
             return $coll;
@@ -771,7 +779,7 @@
     public function fetchAssoc($params = array())
     {
         // setup
-        $params = $this->_fixSelectParams($params);
+        $params = $this->fixSelectParams($params);
         $select = $this->newSelect($params['eager']);
         
         // build
@@ -786,9 +794,22 @@
                ->bind($params['bind']);
         
         // fetch
+        return $this->_fetchAssoc($select, $params);
+    }
+    
+    protected function _fetchAssoc($select, $params)
+    {
         $data = $select->fetchAssoc();
         if ($data) {
-            return $this->newCollection($data);
+            $coll = $this->newCollection($data);
+            foreach ((array) $params['eager'] as $name) {
+                $related = $this->getRelated($name);
+                if ($related->type == 'has_many') {
+                    $data = $this->fetchRelatedArray($params, $name);
+                    $coll->loadRelated($name, $data);
+                }
+            }
+            return $coll;
         } else {
             return array();
         }
@@ -831,7 +852,7 @@
     public function fetchOne($params = array())
     {
         // setup
-        $params = $this->_fixSelectParams($params);
+        $params = $this->fixSelectParams($params);
         $select = $this->newSelect($params['eager']);
         
         // build
@@ -853,11 +874,10 @@
         $record = $this->newRecord($data);
         
         // get related data from each eager has_many relationship
-        $list = (array) $params['eager'];
-        foreach ($this->_related as $name => $opts) {
-            $eager = in_array($name, $list);
-            if ($eager && $opts['type'] == 'has_many') {
-                $record->$name = $this->fetchRelatedObject($record, $name, $opts['page']);
+        foreach ((array) $params['eager'] as $name) {
+            $related = $this->getRelated($name);
+            if ($related->type == 'has_many') {
+                $record->$name = $this->fetchRelatedObject($record, $name);
             }
         }
         
@@ -907,7 +927,7 @@
     public function fetchCol($params = array())
     {
         // setup
-        $params = $this->_fixSelectParams($params);
+        $params = $this->fixSelectParams($params);
         $select = $this->newSelect($params['eager']);
         
         // build
@@ -972,7 +992,7 @@
     public function fetchPairs($params = array())
     {
         // setup
-        $params = $this->_fixSelectParams($params);
+        $params = $this->fixSelectParams($params);
         $select = $this->newSelect($params['eager']);
         
         // build
@@ -1037,7 +1057,7 @@
     public function fetchValue($params = array())
     {
         // setup
-        $params = $this->_fixSelectParams($params);
+        $params = $this->fixSelectParams($params);
         $select = $this->newSelect($params['eager']);
         $col = current($params['cols']);
         
@@ -1092,8 +1112,9 @@
         }
         
         // set placeholders for relateds.
-        foreach ($this->_related as $key => $val) {
-            $data[$key] = null;
+        $names = array_keys($this->_related);
+        foreach ($names as $name) {
+            $data[$name] = null;
         }
         
         // done, return the proper record object
@@ -1115,7 +1136,7 @@
      */
     public function countPages($params = null)
     {
-        $params = $this->_fixSelectParams($params);
+        $params = $this->fixSelectParams($params);
         
         $select = $this->newSelect();
         $select->distinct($params['distinct'])
@@ -1146,17 +1167,18 @@
      */
     public function countPagesRelated($record, $name, $params = null)
     {
-        $params = $this->_fixSelectParams($params);
+        $related = $this->getRelated($name);
+        $params = $this->fixSelectParams($params);
+        $select = $related->newSelect($record);
         
-        $select = $this->newSelectRelated($record, $name);
         $select->multiWhere($params['where'])
                ->group($params['group'])
                ->having($params['having'])
                ->setPaging($params['paging'])
                ->bind($params['bind']);
         
-        $opts = $this->related[$name];
-        $col  = "{$opts['foreign_alias']}.{$opts['foreign_primary_col']}";
+        $col = "{$related->foreign_alias}.{$related->foreign_primary_col}";
+        
         return $select->countPages($col);
     }
     
@@ -1176,7 +1198,7 @@
      * @return array A normalized set of clause params.
      * 
      */
-    protected function _fixSelectParams($params)
+    public function fixSelectParams($params)
     {
         settype($params, 'array');
         
@@ -1186,8 +1208,6 @@
         
         if (empty($params['cols'])) {
             $params['cols'] = array_keys($this->_table_cols);
-        } else {
-            // add primary and inherit cols?
         }
         
         if (empty($params['eager'])) {
@@ -1247,49 +1267,44 @@
         // add eager has_one/belongs_to joins
         foreach ((array) $eager as $name) {
             
-            if (empty($this->_related[$name])) {
-                // skip unrecognized relationship names
-                continue;
-            }
-            
             // get the relationship options
-            $opts = $this->_related[$name];
+            $related = $this->getRelated($name);
             
             // only process eager to-one associations at this point
-            if ($opts['type'] == 'has_many') {
+            if ($related->type == 'has_many') {
                 continue;
             }
             
             // build column names as "name__col" so that we can extract the
             // the related data later.
             $cols = array();
-            foreach ($opts['cols'] as $col) {
+            foreach ($related->cols as $col) {
                 $cols[] = "$col AS {$name}__$col";
             }
             
             // primary-key join condition on foreign table
             // local.id = foreign_alias.local_id
-            $cond = "{$this->_model_name}.{$opts['native_col']} = "
-                  . "{$opts['foreign_alias']}.{$opts['foreign_col']}";
+            $cond = "{$this->_model_name}.{$related->native_col} = "
+                  . "{$related->foreign_alias}.{$related->foreign_col}";
             
             // add the join
             $select->leftJoin(
-                "{$opts['foreign_table']} AS {$opts['foreign_alias']}",
+                "{$related->foreign_table} AS {$related->foreign_alias}",
                 $cond,
                 $cols
             );
             
             // inheritance for foreign model
-            if ($opts['foreign_inherit_col']) {
+            if ($related->foreign_inherit_col) {
                 $select->where(
-                    "{$opts['foreign_alias']}.{$opts['foreign_inherit_col']} = ?",
-                    $opts['foreign_inherit_val']
+                    "{$related->foreign_alias}.{$related->foreign_inherit_col} = ?",
+                    $related->foreign_inherit_val
                 );
             
             }
             
             // added where conditions for the join
-            $select->multiWhere($opts['where']);
+            $select->multiWhere($related->where);
         }
         
         // inheritance for native model
@@ -1306,163 +1321,6 @@
     
     /**
      * 
-     * Returns a new Solar_Sql_Select tool for selecting related records.
-     * 
-     * @param string $name The relationship name.
-     * 
-     * @return Solar_Sql_Select
-     * 
-     * @todo Make $record into $spec, allow for a Select object (in which case
-     * we use an inner IN(SELECT...) clause) or a Record (in which case we use
-     * the primary key value).
-     * 
-     */
-    public function newSelectRelated($spec, $name)
-    {
-        // specification must be a record, or params for a select
-        if (! ($spec instanceof Solar_Sql_Model_Record) && ! is_array($spec)) {
-            throw $this->_exception('ERR_RELATED_SPEC', array(
-                'spec' => $spec
-            ));
-        }
-        
-        // must have a related name
-        if (! array_key_exists($name, $this->_related)) {
-            throw $this->_exception('ERR_RELATED_NOT_EXIST', array(
-                'name' => $name,
-            ));
-        }
-        
-        // get the options for this relationship
-        $opts = $this->_related[$name];
-        
-        // convert $spec array to a Select object for the native column ID list
-        if (is_array($spec)) {
-            // build the params
-            $params = $this->_fixSelectParams($spec);
-            unset($params['eager']);
-            // build the select
-            $spec = $this->newSelect();
-            $spec->distinct($params['distinct'])
-                 ->from("{$opts['native_table']} AS {$opts['native_alias']}", $opts['native_col'])
-                 ->multiWhere($params['where'])
-                 ->group($params['group'])
-                 ->having($params['having'])
-                 ->order($params['order'])
-                 ->setPaging($params['paging'])
-                 ->limitPage($params['page']);
-        }
-        
-        // get a select object for the related rows
-        $select = Solar::factory(
-            $this->_select_class,
-            array('sql' => $this->_sql)
-        );
-        
-        // what type of relationship?
-        if ($opts['type'] == 'has_many' && $opts['through']) {
-            
-            // more-complex 'has_many through' relationship.
-            // join through the mapping table.
-            $join_table = "{$opts['through_table']} AS {$opts['through_alias']}";
-            $join_where = "{$opts['foreign_alias']}.{$opts['foreign_col']} = "
-                        . "{$opts['through_alias']}.{$opts['through_foreign_col']}";
-            
-            $select->leftJoin($join_table, $join_where);
-            
-            // how to filter rows?
-            if ($spec instanceof Solar_Sql_Model_Record) {
-                // restrict to the related native column value in the "through" table
-                $select->where(
-                    "{$opts['through_alias']}.{$opts['through_native_col']} = ?",
-                    $spec->{$opts['native_col']} // this is where we set the filtering clause
-                );
-            } else {
-                // $spec is a Select object. restrict to a sub-query of IDs
-                // from the native table as an inner join.
-                $inner = str_replace("\n", "\n\t", $spec->fetchSql());
-                
-                // add the native table ID at the top through a join
-                $select->innerJoin(
-                    "($inner) AS {$opts['native_alias']}",
-                    "{$opts['through_alias']}.{$opts['through_native_col']} = {$opts['native_alias']}.{$opts['native_col']}",
-                    "{$opts['native_col']} AS {$opts['native_alias']}__{$opts['native_col']}"
-                );
-            }
-            
-            // select from the foreign table.
-            $select->from(
-                "{$opts['foreign_table']} AS {$opts['foreign_alias']}",
-                $opts['cols']
-            );
-            
-            // honor foreign inheritance
-            if ($opts['foreign_inherit_col']) {
-                $select->where(
-                    "{$opts['foreign_alias']}.{$opts['foreign_inherit_col']} = ?",
-                    $opts['foreign_inherit_val']
-                );
-            }
-        
-        } else {
-            
-            // simple belongs_to, has_one, or has_many.
-            if ($spec instanceof Solar_Sql_Model_Record) {
-                // restrict to the related native column value in the foreign table
-                $select->where(
-                    "{$opts['foreign_alias']}.{$opts['foreign_col']} = ?",
-                    $spec->{$opts['native_col']}
-                );
-            } else {
-                // $spec is a Select object
-                // restrict to a sub-select of IDs from the native table
-                $inner = str_replace("\n", "\n\t\t", $spec->fetchSql());
-                // add the native table ID at the top through a join
-                $select->innerJoin(
-                    "($inner) AS {$opts['native_alias']}",
-                    "{$opts['foreign_alias']}.{$opts['foreign_col']} = {$opts['native_alias']}.{$opts['native_col']}",
-                    "{$opts['native_col']} AS {$opts['native_alias']}__{$opts['native_col']}"
-                );
-            }
-            
-            // select columns from the foreign table.
-            $select->from(
-                "{$opts['foreign_table']} AS {$opts['foreign_alias']}",
-                $opts['cols']
-            );
-            
-            // honor foreign inheritance
-            if ($opts['foreign_inherit_col']) {
-                $select->where(
-                    "{$opts['foreign_alias']}.{$opts['foreign_inherit_col']} = ?",
-                    $opts['foreign_inherit_val']
-                );
-            }
-        }
-        
-        // everything else
-        $select->distinct($opts['distinct'])
-               ->multiWhere($opts['where'])
-               ->group($opts['group'])
-               ->having($opts['having'])
-               ->order($opts['order'])
-               ->setPaging($opts['paging']);
-        
-        // done
-        return $select;
-    }
-    
-    public function getRelatedModel($name)
-    {
-        $class = $this->_related[$name]['class'];
-        $model = Solar::factory($class, array(
-            'sql' => $this->_sql,
-        ));
-        return $model;
-    }
-    
-    /**
-     * 
      * Given a record, fetches a related record or collection for a named
      * relationship.
      * 
@@ -1483,23 +1341,13 @@
         $data = $this->fetchRelatedArray($spec, $name, $page);
         
         // get the relationship type
-        $opts = $this->_related[$name];
-        $type = $opts['type'];
+        $related = $this->getRelated($name);
         
         // record or collection?
-        if ($type == 'has_one' || $type == 'belongs_to') {
-            // create and load into an appropriate Record object, using the
-            // same SQL object
-            $model = Solar::factory($opts['foreign_model'], array(
-                'sql' => $this->_sql
-            ));
-            $result = $model->newRecord($data);
+        if ($related->type == 'has_one' || $related->type == 'belongs_to') {
+            $result = $related->getModel()->newRecord($data);
         } else {
-            // create and load a Collection object using the same SQL object
-            $model = Solar::factory($opts['foreign_model'], array(
-                'sql' => $this->_sql
-            ));
-            $result = $model->newCollection($data);
+            $result = $related->getModel()->newCollection($data);
         }
         
         // done!
@@ -1508,18 +1356,16 @@
     
     public function fetchRelatedArray($spec, $name, $page = null, $bind = null)
     {
-        $select = $this->newSelectRelated($spec, $name);
-        $select->bind($bind);
+        $related = $this->getRelated($name);
         
-        // fetch per the association type
-        $opts = $this->_related[$name];
-        $type = $opts['type'];
-        if ($type == 'has_one' || $type == 'belongs_to') {
-            return $select->fetchOne();
-        } else {
-            $select->limitPage($page);
-            return $select->fetchAll();
+        if (is_array($spec)) {
+            $spec = $this->fixSelectParams($spec);
         }
+        
+        $select = $related->newSelect($spec);
+        $select->bind($bind);
+        $select->limitPage($page);
+        return $select->fetch($related->fetch);
     }
     
     // -----------------------------------------------------------------
@@ -1539,10 +1385,10 @@
      */
     public function newRecord($data)
     {
-        // the model class we'll use for the record
+        // the record class we'll use
         $class = null;
         
-        // look for an inheritance model in relation to $data
+        // look for an inheritance in relation to $data
         $inherit = null;
         if ($this->_inherit_col && ! empty($data[$this->_inherit_col])) {
             // inheritance is available, and a value is set for the
@@ -1572,7 +1418,7 @@
             $class = $this->_record_class;
         }
         
-        // factory the appropriate model class, load it, and return it.
+        // factory the appropriate record class, load it, and return it.
         $record = Solar::factory($class);
         $record->setModel($this);
         $record->load($data);
@@ -1590,7 +1436,7 @@
      */
     public function newCollection($data = null)
     {
-        // the model class we'll use for the collection
+        // the collection class we'll use
         $class = $this->_stack->load('Collection', false);
         
         // final fallback
@@ -1920,9 +1766,7 @@
      */
     protected function _hasOne($name, $opts = null)
     {
-        settype($opts, 'array');
-        $opts['type'] = 'has_one';
-        $this->_related[$name] = $opts;
+        $this->_addRelated($name, 'HasOne', $opts);
     }
     
     /**
@@ -1939,9 +1783,7 @@
      */
     protected function _belongsTo($name, $opts = null)
     {
-        settype($opts, 'array');
-        $opts['type'] = 'belongs_to';
-        $this->_related[$name] = $opts;
+        $this->_addRelated($name, 'BelongsTo', $opts);
     }
     
     /**
@@ -1961,8 +1803,502 @@
      */
     protected function _hasMany($name, $opts = null)
     {
-        settype($opts, 'array');
-        $opts['type'] = 'has_many';
-        $this->_related[$name] = $opts;
+        $this->_addRelated($name, 'HasMany', $opts);
     }
+    
+    /**
+     * 
+     * Support method for adding relations.
+     * 
+     * @param string $name The relationship name, which will double as a
+     * property when records are fetched from the model.
+     * 
+     * @param string $type The relationship type.
+     * 
+     * @param array $opts Additional options for the relationship.
+     * 
+     * @return void
+     * 
+     */
+    protected function _addRelated($name, $type, $opts)
+    {
+        // is the relation name already a column name?
+        if (array_key_exists($name, $this->_table_cols)) {
+            throw $this->_exception(
+                'ERR_RELATED_NAME_CONFLICT',
+                array(
+                    'name'  => $name,
+                    'class' => $this->_class,
+                )
+            );
+        }
+        
+        // is the relation name already in use?
+        if (array_key_exists($name, $this->_related)) {
+            throw $this->_exception(
+                'ERR_RELATED_NAME_EXISTS',
+                array(
+                    'name'  => $name,
+                    'class' => $this->_class,
+                )
+            );
+        }
+        
+        // keep it!
+        $opts['name']  = $name;
+        $opts['class'] = "Solar_Sql_Model_Related_$type";
+        $this->_related[$name] = (array) $opts;
+    }
+    
+    /**
+     * 
+     * Gets the control object for a named relationship.
+     * 
+     * @param string $name The related name.
+     * 
+     * @return Solar_Sql_Model_Related The relationship control object.
+     * 
+     */
+    public function getRelated($name)
+    {
+        if (! array_key_exists($name, $this->_related)) {
+            throw $this->_exception(
+                'ERR_RELATED_NAME_NOT_EXISTS',
+                array(
+                    'name'  => $name,
+                    'class' => $this->_class,
+                )
+            );
+        }
+        
+        if (is_array($this->_related[$name])) {
+            $opts = $this->_related[$name];
+            $this->_related[$name] = Solar::factory($opts['class']);
+            unset($opts['class']);
+            $this->_related[$name]->setNativeModel($this);
+            $this->_related[$name]->load($opts);
+        }
+        
+        return $this->_related[$name];
+    }
+    
+    /**
+     * 
+     * Fixes the stack of parent classes for the model.
+     * 
+     * @return void
+     * 
+     */
+    protected function _fixStack()
+    {
+        $parents = Solar::parents($this->_class, true);
+        array_pop($parents); // Solar_Base
+        array_pop($parents); // Solar_Sql_Model
+        $this->_stack = Solar::factory('Solar_Class_Stack');
+        $this->_stack->add($parents);
+    }
+    
+    /**
+     * 
+     * Loads table name into $this->_table_name, and pre-sets the value of
+     * $this->_inherit_model based on the class name.
+     * 
+     * @return void
+     * 
+     */
+    protected function _fixTableName()
+    {
+        /**
+         * Pre-set the value of $_inherit_model.  Will be modified one
+         * more time in _fixTableCols().
+         */
+        // find the closest parent called *_Model.  we do this so that
+        // we can honor the top-level table name with inherited models.
+        // *do not* use the class stack, as Solar_Sql_Model has been
+        // removed from it.
+        $parents = Solar::parents($this->_class, true);
+        foreach ($parents as $key => $val) {
+            if (substr($val, -6) == '_Model') {
+                break;
+            }
+        }
+        
+        // $key is now the value of the closest "_Model" class.
+        // -1 to get the first class below that (e.g., *_Model_Nodes).
+        // $parent is then the parent class name that represents the base of
+        // the model-inheritance hierarchy (which may not be the immediate
+        // parent in some cases).
+        $parent = $parents[$key - 1];
+        
+        // compare parent class name to the current class name.
+        // if it has an undersore after the parent class name, this class
+        // is considered to be an inheritance model.
+        $len = strlen($parent) + 1;
+        if (substr($this->_class, 0, $len) == "{$parent}_") {
+            $this->_inherit_model = substr($this->_class, $len);
+            $this->_inherit_base = $parent;
+        }
+        
+        // get the part after the last underscore in the parent class name.
+        // e.g., "Solar_Model_Node" => "Node".  If no underscores, use the
+        // parent class name as-is.
+        $pos = strrpos($parent, '_');
+        if ($pos === false) {
+            $table = $parent;
+        } else {
+            $table = substr($parent, $pos + 1);
+        }
+        
+        /**
+         * Auto-set the table name, if needed.
+         */
+        if (empty($this->_table_name)) {
+            // auto-defined table name. change TableName to table_name.
+            // this is our one concession on inflecting names, because if the
+            // class was called Table_Name, it would set the inheritance
+            // model improperly.
+            $table = preg_replace('/([a-z])([A-Z])/', '$1_$2', $table);
+            $this->_table_name = strtolower($table);
+        } else {
+            // user-defined table name.
+            $this->_table_name = strtolower($this->_table_name);
+        }
+    }
+    
+    /**
+     * 
+     * Fixes $this->_index listings.
+     * 
+     * @return void
+     * 
+     */
+    protected function _fixIndex()
+    {
+        // baseline index definition
+        $baseidx = array(
+            'name'    => null,
+            'type'    => 'normal',
+            'cols'    => null,
+        );
+        
+        // fix up each index to have a full set of info
+        foreach ($this->_index as $key => $val) {
+            
+            if (is_int($key) && is_string($val)) {
+                // array('col')
+                $info = array(
+                    'name' => $val,
+                    'type' => 'normal',
+                    'cols' => array($val),
+                );
+            } elseif (is_string($key) && is_string($val)) {
+                // array('col' => 'unique')
+                $info = array(
+                    'name' => $key,
+                    'type' => $val,
+                    'cols' => array($key),
+                );
+            } else {
+                // array('alt' => array('type' => 'normal', 'cols' => array(...)))
+                $info = array_merge($baseidx, (array) $val);
+                $info['name'] = (string) $key;
+                settype($info['cols'], 'array');
+            }
+            
+            $this->_index[$key] = $info;
+        }
+    }
+    
+    /**
+     * 
+     * Fixes table column definitions into $_table_cols, and post-sets
+     * inheritance values.
+     * 
+     * @param StdClass $model The model property catalog.
+     * 
+     * @return void
+     * 
+     */
+    protected function _fixTableCols()
+    {
+        // is a table with the same name already at the database?
+        $list = $this->_sql->fetchTableList();
+        
+        // if not found, attempt to create it
+        if (! in_array($this->_table_name, $list)) {
+            $this->_createTableAndIndexes($this);
+        }
+        
+        // reset the columns to be **as they are at the database**
+        $this->_table_cols = $this->_sql->fetchTableCols($this->_table_name);
+        
+        // @todo add a "sync" check to see if column data in the class
+        // matches column data in the database, and throw an exception
+        // if they don't match pretty closely.
+        
+        // set the primary column based on the first primary key;
+        // ignores later primary keys
+        foreach ($this->_table_cols as $key => $val) {
+            if ($val['primary']) {
+                $this->_primary_col = $key;
+                break;
+            }
+        }
+    }
+    
+    /**
+     * 
+     * Fixes the array-name and table-alias for user input to this model.
+     * 
+     * @return void
+     * 
+     */
+    protected function _fixModelName()
+    {
+        if (! $this->_model_name) {
+            if ($this->_inherit_model) {
+                $this->_model_name = $this->_inherit_model;
+            } else {
+                // get the part after the last Model_ portion
+                $pos = strpos($this->_class, 'Model_');
+                if ($pos) {
+                    $this->_model_name = substr($this->_class, $pos+6);
+                } else {
+                    $this->_model_name = $this->_class;
+                }
+            }
+            
+            // convert FooBar to foo_bar
+            $this->_model_name = strtolower(
+                preg_replace('/([a-z])([A-Z])/', '$1_$2', $this->_model_name)
+            );
+        }
+    }
+    
+    /**
+     * 
+     * Fixes the default order when fetching records from this model.
+     * 
+     * @return void
+     * 
+     */
+    protected function _fixOrder()
+    {
+        if (! $this->_order) {
+            $this->_order = $this->_model_name . '.' . $this->_primary_col;
+        }
+    }
+    
+    /**
+     * 
+     * Fixes up special column indicator properties, and post-sets the
+     * $_inherit_model value based on the existence of the inheritance column.
+     * 
+     * @return void
+     * 
+     * @todo How to make foreign_col recognize that it's inherited, and should
+     * use the parent foreign_col value?  Can we just work up the chain?
+     * 
+     */
+    protected function _fixPropertyCols()
+    {
+        // make sure these actually exist in the table, otherwise unset them
+        $list = array(
+            '_created_col',
+            '_updated_col',
+            '_primary_col',
+            '_inherit_col',
+        );
+        
+        foreach ($list as $col) {
+            if (trim($this->$col) == '' ||
+                ! array_key_exists($this->$col, $this->_table_cols)) {
+                // doesn't exist in the table
+                $this->$col = null;
+            }
+        }
+        
+        // post-set the inheritance model value
+        if (! $this->_inherit_col) {
+            $this->_inherit_model = null;
+            $this->_inherit_base = null;
+        }
+        
+        // set up the fetch-cols list
+        settype($this->_fetch_cols, 'array');
+        if (! $this->_fetch_cols) {
+            $this->_fetch_cols = array_keys($this->_table_cols);
+        }
+        
+        // simply force to array
+        settype($this->_serialize_cols, 'array');
+        
+        // the "sequence" columns.  make sure they point to a sequence name.
+        // e.g., string 'col' becomes 'col' => 'col'.
+        $tmp = array();
+        foreach ((array) $this->_sequence_cols as $key => $val) {
+            if (is_int($key)) {
+                $tmp[$val] = $val;
+            } else {
+                $tmp[$key] = $val;
+            }
+        }
+        $this->_sequence_cols = $tmp;
+        
+        // make sure we have a hint to foreign models as to what colname
+        // to use when referring to this model
+        if (empty($this->_foreign_col)) {
+            if (! $this->_inherit_model) {
+                // not inherited
+                $this->_foreign_col = strtolower($this->_model_name)
+                                     . '_' . $this->_primary_col;
+            } else {
+                // inherited, can't just use the model name as a column name.
+                // need to find base model foreign_col value.
+                $base = Solar::factory($this->_inherit_base, array('sql' => $this->_sql));
+                $this->_foreign_col = $base->foreign_col;
+                unset($base);
+            }
+        }
+    }
+    
+    /**
+     * 
+     * Loads the baseline data filters for each column.
+     * 
+     * @return void
+     * 
+     */
+    protected function _fixFilters()
+    {
+        // make sure we have a filter class
+        if (empty($this->_filter_class)) {
+            $class = $this->_stack->load('Filter', false);
+            if (! $class) {
+                $class = 'Solar_Sql_Model_Filter';
+            }
+            $this->_filter_class = $class;
+        }
+        
+        // make sure filters are an array
+        settype($this->_filters, 'array');
+        
+        // make sure that strings are converted
+        // to arrays so that _applyFilters() works properly.
+        foreach ($this->_filters as $col => $list) {
+            foreach ($list as $key => $val) {
+                if (is_string($val)) {
+                    $this->_filters[$col][$key] = array($val);
+                }
+            }
+        }
+        
+        // low and high range values for integer filters
+        $range = array(
+            'smallint' => array(pow(-2, 15), pow(+2, 15) - 1),
+            'int'      => array(pow(-2, 31), pow(+2, 31) - 1),
+            'bigint'   => array(pow(-2, 63), pow(+2, 63) - 1)
+        );
+        
+        // add final fallback filters based on data type
+        foreach ($this->_table_cols as $col => $info) {
+            
+            $type = $info['type'];
+            switch ($type) {
+            case 'bool':
+                $this->_filters[$col][] = array('validateBool');
+                $this->_filters[$col][] = array('sanitizeBool');
+                break;
+            
+            case 'char':
+            case 'varchar':
+                // only add filters if not serializing
+                if (! in_array($col, $this->_serialize_cols)) {
+                    $this->_filters[$col][] = array('validateString');
+                    $this->_filters[$col][] = array('validateMaxLength',
+                        $info['size']);
+                    $this->_filters[$col][] = array('sanitizeString');
+                }
+                break;
+            
+            case 'smallint':
+            case 'int':
+            case 'bigint':
+                $this->_filters[$col][] = array('validateInt');
+                $this->_filters[$col][] = array('validateRange',
+                    $range[$type][0], $range[$type][1]);
+                $this->_filters[$col][] = array('sanitizeInt');
+                break;
+            
+            case 'numeric':
+                $this->_filters[$col][] = array('validateNumeric');
+                $this->_filters[$col][] = array('validateSizeScope',
+                    $info['size'], $info['scope']);
+                $this->_filters[$col][] = array('sanitizeNumeric');
+                break;
+            
+            case 'float':
+                $this->_filters[$col][] = array('validateFloat');
+                $this->_filters[$col][] = array('sanitizeFloat');
+                break;
+            
+            case 'clob':
+                // no filters, clobs are pretty generic
+                break;
+            
+            case 'date':
+                $this->_filters[$col][] = array('validateIsoDate');
+                $this->_filters[$col][] = array('sanitizeIsoDate');
+                break;
+            
+            case 'time':
+                $this->_filters[$col][] = array('validateIsoTime');
+                $this->_filters[$col][] = array('sanitizeIsoTime');
+                break;
+            
+            case 'timestamp':
+                $this->_filters[$col][] = array('validateIsoTimestamp');
+                $this->_filters[$col][] = array('sanitizeIsoTimestamp');
+                break;
+            }
+        }
+    }
+    
+    /**
+     * 
+     * Creates the table and indexes in the database using $this->_table_cols
+     * and $this->_index.
+     * 
+     * @return void
+     * 
+     */
+    protected function _createTableAndIndexes()
+    {
+        /**
+         * Create the table.
+         */
+        $this->_sql->createTable(
+            $this->_table_name,
+            $this->_table_cols
+        );
+        
+        /**
+         * Create the indexes.
+         */
+        foreach ($this->_index as $name => $info) {
+            try {
+                // create this index
+                $this->_sql->createIndex(
+                    $this->_table_name,
+                    $info['name'],
+                    $info['type'] == 'unique',
+                    $info['cols']
+                );
+            } catch (Exception $e) {
+                // cancel the whole deal.
+                $this->_sql->dropTable($this->_table_name);
+                throw $e;
+            }
+        }
+    }
 }




More information about the Solar-svn mailing list