[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