find('all', array('link' => 'User', 'conditions' => 'project_id = 1')) * - Won't produce the desired result as data came from users table will be lost. * $User->find('all', array('link' => 'Project', 'conditions' => 'project_id = 1')) * - Will fetch all users related to the specified project in one query * * - On data mining as a much lighter approach - can reduce 300+ query find operations * in one single query with joins; "or your money back!" ;-) * * - Has the 'fields' param enabled to make it easy to replace Containable usage, * only change the 'contain' param to 'link'. * * Linkable Behavior. Taking it easy in your DB. * RafaelBandeira * * Licensed under The MIT License * Redistributions of files must retain the above copyright notice. * * @version 1.0; */ class LinkableBehavior extends ModelBehavior { protected $_key = 'link'; protected $_options = array( 'type' => true, 'table' => true, 'alias' => true, 'joins' => true, 'conditions' => true, 'fields' => true, 'reference' => true, 'class' => true, 'defaults' => true ); protected $_defaults = array('type' => 'LEFT'); public function beforeFind(&$Model, $query) { /* pr("Linkable::beforeFind() begin"); pr($query); */ if (isset($query[$this->_key])) { $optionsDefaults = $this->_defaults + array('reference' => $Model->alias, $this->_key => array()); $optionsKeys = $this->_options + array($this->_key => true); if (!isset($query['fields']) || $query['fields'] === true) { //$query['fields'] = array_keys($Model->_schema); $query['fields'] = $Model->getDataSource()->fields($Model); } elseif (!is_array($query['fields'])) { $query['fields'] = array($query['fields']); } $query = am(array('joins' => array()), $query, array('recursive' => -1)); $iterators[] = $query[$this->_key]; $cont = 0; do { $iterator = $iterators[$cont]; $defaults = $optionsDefaults; if (isset($iterator['defaults'])) { $defaults = array_merge($defaults, $iterator['defaults']); unset($iterator['defaults']); } $iterations = Set::normalize($iterator); foreach ($iterations as $alias => $options) { if (is_null($options)) { $options = array(); } $options = am($defaults, compact('alias'), $options); if (empty($options['alias'])) { throw new InvalidArgumentException(sprintf('%s::%s must receive aliased links', get_class($this), __FUNCTION__)); } if (empty($options['table']) && empty($options['class'])) { $options['class'] = $options['alias']; } elseif (!empty($options['table']) && empty($options['class'])) { $options['class'] = Inflector::classify($options['table']); } $_Model =& ClassRegistry::init($options['class']); // the incoming model to be linked in query $Reference =& ClassRegistry::init($options['reference']); // the already in query model that links to $_Model /* pr("_Model: $options[class] : $_Model->name ($_Model->alias)"); */ /* pr("Reference: $options[reference] : $Reference->name ($Reference->alias)"); */ $db =& $_Model->getDataSource(); $associatedThroughReference = 0; $association = null; // Figure out how these two models are related, creating // a relationship if one doesn't otherwise already exists. if (($associations = $Reference->getAssociated()) && isset($associations[$_Model->alias])) { /* pr("Reference defines association to _Model"); */ $associatedThroughReference = 1; $type = $associations[$_Model->alias]; $association = $Reference->{$type}[$_Model->alias]; } elseif (($associations = $_Model->getAssociated()) && isset($associations[$Reference->alias])) { /* pr("_Model defines association to Reference"); */ $type = $associations[$Reference->alias]; $association = $_Model->{$type}[$Reference->alias]; } else { // No relationship... make our best effort to create one. /* pr("No assocation between _Model and Reference"); */ $type = 'belongsTo'; $_Model->bind($Reference->alias); // Grab the association now, since we'll unbind in a moment. $association = $_Model->{$type}[$Reference->alias]; $_Model->unbindModel(array('belongsTo' => array($Reference->alias))); } // Determine which model holds the foreign key if (($type === 'hasMany' || $type === 'hasOne') ^ $associatedThroughReference) { $primaryModel = $Reference; $foreignModel = $_Model; } else { $primaryModel = $_Model; $foreignModel = $Reference; } /* pr("primaryModel: $primaryModel->name ($primaryModel->alias)"); */ /* pr("foreignModel: $foreignModel->name ($foreignModel->alias)"); */ /* pr($type); */ /* pr($association); */ if (empty($options['conditions'])) { if ($type === 'hasAndBelongsToMany') { if (isset($association['with'])) $Link =& $_Model->{$association['with']}; else $Link =& $_Model->{Inflector::classify($association['joinTable'])}; $linkAlias = $Link->alias; //$linkAlias = $Link->alias . $options['alias']; // Get the foreign key fields (for the link table) directly from // the defined model associations, if they exists. This is the // users direct specification, and therefore definitive if present. $modelLink = $Link->escapeField($association['foreignKey'], $linkAlias); $referenceLink = $Link->escapeField($association['associationForeignKey'], $linkAlias); // If we haven't figured out the foreign keys, see if there is a // model for the link table, and if it has the appropriate // associations with the two tables we're trying to join. if (empty($modelLink) && isset($Link->belongsTo[$_Model->alias])) $modelLink = $Link->escapeField($Link->belongsTo[$_Model->alias]['foreignKey'], $linkAlias); if (empty($referenceLink) && isset($Link->belongsTo[$Reference->alias])) $referenceLink = $Link->escapeField($Link->belongsTo[$Reference->alias]['foreignKey'], $linkAlias); // We're running quite thin here. None of the models spell // out the appropriate linkages. We'll have to SWAG it. if (empty($modelLink)) $modelLink = $Link->escapeField(Inflector::underscore($_Model->alias) . '_id', $linkAlias); if (empty($referenceLink)) $referenceLink = $Link->escapeField(Inflector::underscore($Reference->alias) . '_id', $linkAlias); // Get the primary key from the tables we're joining. $referenceKey = $Reference->escapeField(); $modelKey = $_Model->escapeField(); // Join the linkage table to our model. We'll use an inner join, // as the whole purpose of the linkage table is to make this // connection. As we are embedding this join, the INNER will not // cause any problem with the overall query, should the user not // be concerned with whether or not the join has any results. // They control that with the 'type' parameter which will be at // the top level join. $options['joins'][] = array('type' => 'INNER', 'alias' => $options['alias'], 'conditions' => "{$modelKey} = {$modelLink}", 'table' => $db->fullTableName($_Model, true)); // Now for the top level join. This will be added into the list // of joins down below, outside of the HABTM specific code. $options['alias'] = $linkAlias; $options['table'] = $Link->getDataSource()->fullTableName($Link); $options['conditions'][] = "{$referenceLink} = {$referenceKey}"; } elseif (isset($association['foreignKey']) && $association['foreignKey']) { $foreignKey = $primaryModel->escapeField($association['foreignKey']); $primaryKey = $foreignModel->escapeField($foreignModel->primaryKey); // Only differentiating to help show the logical flow. // Either way works and this test can be tossed out if (($type === 'hasMany' || $type === 'hasOne') ^ $associatedThroughReference) $options['conditions'][] = "{$primaryKey} = {$foreignKey}"; else $options['conditions'][] = "{$foreignKey} = {$primaryKey}"; } else { // No Foreign Key... nothing we can do. $options['conditions'] = array(); } // The user may have specified conditions directly in the model // for this join. Make sure to adhere to those conditions. if (isset($association['conditions']) && is_array($association['conditions'])) $options['conditions'] = array_merge($options['conditions'], $association['conditions']); elseif (!empty($association['conditions'])) $options['conditions'][] = $association['conditions']; /* pr(array("Link ", $options['conditions'])); */ //pr($modelKey); //pr($referenceKey); } if (empty($options['table'])) { $options['table'] = $db->fullTableName($_Model, true); } if (!isset($options['fields']) || !is_array($options['fields'])) $options['fields'] = $db->fields($_Model); elseif (!empty($options['fields'])) $options['fields'] = $db->fields($_Model, null, $options['fields']); $query['fields'] = array_merge($query['fields'], $options['fields'], (empty($association['fields']) ? array() : $db->fields($_Model, null, $association['fields']))); $options[$this->_key] = am($options[$this->_key], array_diff_key($options, $optionsKeys)); $options = array_intersect_key($options, $optionsKeys); if (!empty($options[$this->_key])) { $iterators[] = $options[$this->_key] + array('defaults' => array_merge($defaults, array('reference' => $options['class']))); } $query['joins'][] = array_intersect_key($options, array('type' => true, 'alias' => true, 'table' => true, 'joins' => true, 'conditions' => true)); } ++$cont; $notDone = isset($iterators[$cont]); } while ($notDone); } /* pr("Linkable::beforeFind() end"); pr($query); */ return $query; } }