Location: PHPKode > projects > SolarPHP > solar-system-1.1.1/solar/source/solar/Solar/Sql/Model/Related/HasManyThrough.php
<?php
/**
 * 
 * Represents the characteristics of a relationship where a native model
 * "has many" of a foreign model.  This includes "has many through" (i.e.,
 * a many-to-many relationship through an interceding mapping model).
 * 
 * @category Solar
 * 
 * @package Solar_Sql_Model
 * 
 * @author Paul M. Jones <hide@address.com>
 * 
 * @author Jeff Moore <hide@address.com>
 * 
 * @license http://opensource.org/licenses/bsd-license.php BSD
 * 
 * @version $Id: HasManyThrough.php 4489 2010-03-02 15:34:14Z pmjones $
 * 
 */
class Solar_Sql_Model_Related_HasManyThrough extends Solar_Sql_Model_Related_ToMany
{
    /**
     * 
     * The relationship name through which we find foreign records.
     * 
     * @var string
     * 
     */
    public $through;
    
    /**
     * 
     * The "through" table name. Not definable by the user; is taken from
     * the "through" relationship definition.
     * 
     * @var string
     * 
     */
    public $through_table;
    
    /**
     * 
     * The "through" table alias. Not definable by the user; is taken from
     * the "through" relationship definition.
     * 
     * @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;
    
    /**
     * 
     * The virtual element `through_key` automatically populates the 
     * 'through_foreign_col' value for you.
     * 
     * @var string.
     * 
     */
    public $through_key;
    
    /**
     * 
     * The conditions retrieved from the "through" model.
     * 
     * @var string|array
     * 
     */
    public $through_conditions;
    
    /**
     * 
     * The type of join for the "through" model.
     * 
     * @var string
     * 
     */
    public $through_join_type;
    
    /**
     * 
     * Sets the relationship type.
     * 
     * @return void
     * 
     */
    protected function _setType()
    {
        $this->type = 'has_many_through';
    }
    
    /**
     * 
     * Corrects the foreign_key value in the options; uses the native-model
     * table name as singular when a regular has-many, and the foreign-
     * model primary column as-is when a 'has-many through'.
     * 
     * @param array &$opts The user-defined relationship options.
     * 
     * @return void
     * 
     */
    protected function _fixForeignKey(&$opts)
    {
        $opts['foreign_key'] = $this->_foreign_model->primary_col;
    }
    
    /**
     * 
     * A support method for _fixRelated() to handle has-many relationships.
     * 
     * @param array &$opts The relationship options; these are modified in-
     * place.
     * 
     * @return void
     * 
     */
    protected function _setRelated($opts)
    {
        // retain the name of the "through" related
        $this->through = $opts['through'];
        
        // get the "through" relationship control
        $through = $this->_native_model->getRelated($this->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'];
        }
        
        // retain the "through join type"
        if (empty($opts['through_join_type'])) {
            $this->through_join_type = 'left';
        } else {
            $this->through_join_type = $opts['through_join_type'];
        }
        
        // get the through-table
        $this->through_table = $through->foreign_table;
        
        // get the through-alias
        $this->through_alias = $through->foreign_alias;
        
        // a little magic
        if (empty($opts['through_native_col']) &&
            empty($opts['through_foreign_col']) &&
            ! empty($opts['through_key'])) {
            // pre-define through_foreign_col
            $this->through_key = $opts['through_key'];
            $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 = $this->_native_model->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'];
        }
        
        // retain the "through where" mods
        $this->through_conditions = $through->getForeignConditions($this->through_alias);
    }
    
    /**
     * 
     * Modifies the native fetch params with eager joins so that the through 
     * table and the foreign table are joined properly.
     * 
     * @param Solar_Sql_Model_Params_Eager $eager The eager params.
     * 
     * @param Solar_Sql_Model_Params_Fetch $fetch The native fetch params.
     * 
     * @return void
     * 
     * @see modEagerFetch()
     * 
     */
    protected function _modEagerFetch($eager, $fetch)
    {
        // first, join the native table to the through table
        $thru = array(
            'type' => strtolower($this->through_join_type),
            'name' => "{$this->through_table} AS {$this->through_alias}",
            'cond' => array(),
            'cols' => null,
        );
        
        $thru['cond'][] = "{$fetch['alias']}.{$this->native_col} = "
                        . "{$this->through_alias}.{$this->through_native_col}";
        
        $thru['cond'] = array_merge(
            $thru['cond'],
            $this->through_conditions
        );
        
        // keep for countPages() calls?
        if ($thru['type'] != 'left') {
            $thru['keep'] = true;
        } else {
            $thru['keep'] = false;
        }
        
        $fetch->join($thru);
        
        // now join to the through table to the foreign table
        $join = array(
            'type' => strtolower($eager['join_type']),
            'name' => "{$this->foreign_table} AS {$eager['alias']}",
            'cond' => array(),
            'cols' => null,
        );
        
        $join['cond'][] = "{$eager['alias']}.{$this->foreign_col} = "
                        . "{$this->through_alias}.{$this->through_foreign_col}";
        
        // foreign and eager conditions
        $join['cond'] = array_merge(
            $join['cond'],
            $this->getForeignConditions($eager['alias']),
            (array) $eager['conditions']
        );
        
        // keep for countPages() calls only if we kept "through", and the
        // foreign join is not a left join
        if ($thru['keep'] && $join['type'] != 'left') {
            $join['keep'] = true;
        } else {
            $join['keep'] = false;
        }
        
        // done!
        $fetch->join($join);
        
        // always DISTINCT so we don't get multiple duplicate native rows
        $fetch->distinct(true);
    }
    
    /**
     * 
     * Fetches eager results into an existing single native array row.
     * 
     * @param Solar_Sql_Model_Params_Eager $eager The eager params.
     * 
     * @param array &$array The existing native result row.
     * 
     * @return void
     * 
     */
    protected function _fetchIntoArrayOne($eager, &$array)
    {
        // build the "through" join and its conditions
        $thru = array(
            'type' => 'inner',
            'name' => "{$this->through_table} AS {$this->through_alias}",
            'cond' => array(),
            'cols' => null,
        );
        
        $thru['cond'][] = "{$eager['alias']}.{$this->foreign_col} = "
                        . "{$this->through_alias}.{$this->through_foreign_col}";
        
        $thru['cond'] = array_merge(
            $thru['cond'],
            $this->through_conditions
        );
        
        // build the foreign where conditions
        $where = array();
        $col = "{$this->through_alias}.{$this->through_native_col}";
        $where["$col = ?"] = $array[$this->native_col];
        
        $where = array_merge(
            $where,
            $this->getForeignConditions($eager['alias'])
        );
        
        $params = array(
            'alias' => $eager['alias'],
            'cols'  => $eager['cols'],
            'join'  => $thru,
            'where' => $where,
            'order' => $this->order,
            'eager' => $eager['eager'],
        );
        
        $data = $this->_foreign_model->fetchAllAsArray($params);
        
        $array[$this->name] = $data;
    }
    
    /**
     * 
     * Fetches eager results into an existing native array rowset.
     * 
     * @param Solar_Sql_Model_Params_Eager $eager The eager params.
     * 
     * @param array &$array The existing native result row.
     * 
     * @param Solar_Sql_Model_Params_Fetch $fetch The native fetch settings.
     * 
     * @return void
     * 
     */
    protected function _fetchIntoArrayAll($eager, &$array, $fetch)
    {
        // build the "through" join and its conditions
        $index_col = "{$this->native_alias}__{$this->through_native_col}";
        $thru = array(
            'type' => 'inner',
            'name' => "{$this->through_table} AS {$this->through_alias}",
            'cond' => array(),
            'cols' => "{$this->through_native_col} AS {$index_col}",
        );
        
        $thru['cond'][] = "{$eager['alias']}.{$this->foreign_col} = "
                        . "{$this->through_alias}.{$this->through_foreign_col}";
        
        $thru['cond'] = array_merge(
            $thru['cond'],
            $this->through_conditions
        );
        
        // build the foreign join or where
        $col = "{$this->through_alias}.{$this->through_native_col}";
        
        $use_select = $eager['native_by'] == 'select'
                   || count($array) > $eager['wherein_max'];
        
        $join = null;
        $where = null;
        if ($use_select) {
            $join = $this->_getNativeBySelect($eager, $fetch, $col);
            $join['cond'] = array_merge(
                (array) $join['cond'],
                $this->getForeignConditions($eager['alias'])
            );
        } else {
            $where = array_merge(
                $this->_getNativeByWherein($eager, $array, $col),
                $this->getForeignConditions($eager['alias'])
            );
        }
        
        $params = array(
            'alias' => $eager['alias'],
            'cols'  => $eager['cols'],
            'join'  => ($join) ? array($thru, $join) : $thru,
            'where' => $where,
            'order' => $this->order,
            'eager' => $eager['eager'],
        );
        
        $data = $this->_foreign_model->fetchAllAsArray($params);
        $data = $this->_collate($data, $index_col);
        
        // now we have all the foreign rows for all-of-all of the native rows.
        // next is to tie each of those foreign sets to the appropriate
        // native result rows.
        foreach ($array as &$row) {
            $key = $row[$this->native_col];
            if (! empty($data[$key])) {
                $row[$this->name] = $data[$key];
            } else {
                $row[$this->name] = $this->_getEmpty();
            }
        }
    }
    
    /**
     * 
     * Fetches the related collection for a native ID or record.
     * 
     * @param mixed $spec If a scalar, treated as the native primary key
     * value; if an array or record, retrieves the native primary key value
     * from it.
     * 
     * @return object The related collection object.
     * 
     */
    public function fetch($spec)
    {
        if ($spec instanceof Solar_Sql_Model_Record || is_array($spec)) {
            $native_id = $spec[$this->native_col];
        } else {
            $native_id = $spec;
        }
        
        // build up the "through" join
        $thru = array(
            'type' => 'inner',
            'name' => "{$this->through_table} AS {$this->through_alias}",
            'cond' => array(),
            'cols' => null,
        );
        
        $thru['cond'][] = "{$this->foreign_alias}.{$this->foreign_col} = "
                        . "{$this->through_alias}.{$this->through_foreign_col}";
        
        $thru['cond'] = array_merge(
            $thru['cond'],
            $this->through_conditions
        );
        
        // build up the "where" conditions
        $where = array();
        $cond  = "{$this->through_alias}.{$this->through_native_col} = ?";
        $where[$cond] = $native_id;
        $where = array_merge(
            $where,
            $this->getForeignConditions($this->foreign_alias)
        );
        
        // build the fetch, and go
        $fetch = array(
            'alias' => $this->foreign_alias,
            'join'  => $thru,
            'where' => $where,
            'order' => $this->order,
        );
        
        $obj = $this->_foreign_model->fetchAll($fetch);
        
        if (! $obj) {
            $obj = $this->fetchEmpty();
        }
        
        return $obj;
    }
    
    /**
     * 
     * Collates a result array by an array key, grouping the results by that
     * value.
     *
     * @param array $array The result array.
     *
     * @param string $key The key in the array to collate by.
     * 
     * @return array An array of collated elements, keyed by the collation 
     * value.
     * 
     */
    protected function _collate($array, $key)
    {
        $collated = array();
        foreach ($array as $i => $row) {
            $val = $row[$key];
            unset($row[$key]); // clear the key from the array
            $collated[$val][] = $row;
        }
        return $collated;
    }
    
    /**
     * 
     * Returns a new, empty collection when there is no related data.
     * 
     * @return Solar_Sql_Model_Collection
     * 
     */
    public function fetchEmpty()
    {
        return $this->fetchNew();
    }
    
    /**
     * 
     * Saves the related "through" collection *and* the foreign collection
     * from a native record.
     * 
     * Ensures the "through" collection has an entry for each foreign record,
     * and adds/removes entried in the "through" collection as needed.
     * 
     * @param Solar_Sql_Model_Record $native The native record to save from.
     * 
     * @return void
     * 
     */
    public function save($native)
    {
        // get the foreign collection to work with
        $foreign = $native->{$this->name};
        
        // get the through collection to work with
        $through = $native->{$this->through};
        
        // if no foreign records, kill off all through records
        if ($foreign->isEmpty()) {
            $through->deleteAll();
            return;
        }
        
        // save the foreign records as they are, which creates the necessary
        // primary key values the through mapping will need
        $foreign->save();
        
        // the list of existing foreign values
        $foreign_list = $foreign->getColVals($this->foreign_col);
        
        // the list of existing through values
        $through_list = $through->getColVals($this->through_foreign_col);
        
        // find mappings that *do* exist but shouldn't, and delete them
        foreach ($through_list as $through_key => $through_val) {
            if (! in_array($through_val, $foreign_list)) {
                $through->deleteOne($through_key);
            }
        }
        
        // make sure all existing "through" have the right native IDs on them
        foreach ($through as $record) {
            $record->{$this->through_native_col} = $native->{$this->native_col};
        }
        
        // find mappings that *don't* exist, and add them
        foreach ($foreign_list as $foreign_val) {
            if (! in_array($foreign_val, $through_list)) {
                $through->appendNew(array(
                    $this->through_native_col  => $native->{$this->native_col},
                    $this->through_foreign_col => $foreign_val,
                ));
            }
        }
        
        // done with the mappings, save them
        $through->save();
    }
    
    /**
     * 
     * Are the related "foreign" and "through" collections valid?
     * 
     * @param Solar_Sql_Model_Record $native The native record.
     * 
     * @return bool
     * 
     */
    public function isInvalid($native)
    {
        $foreign = $native->{$this->name};
        $through = $native->{$this->through};
        
        // no foreign and no through means they can't be invalid
        if (! $foreign && ! $through) {
            return false;
        }
        
        // is foreign invalid?
        if ($foreign && $foreign->isInvalid()) {
            return true;
        }
        
        // is through invalid?
        if ($through && $through->isInvalid()) {
            return true;
        }
        
        // both foreign and through are valid
        return false;
    }
}
Return current item: SolarPHP