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; */ /********************************************************************** * NOTE TO 20090615: * This structure should be linkable (it was my test case). $entry = $this->LedgerEntry->find ('first', array('link' => array('DebitLedger' => array( 'fields' => array('id'), 'alias' => 'MyDebitLedger', 'Account' => array( 'fields' => array('id'), 'alias' => 'MyDebitAccount'), ), 'MyCreditLedger' => array( 'fields' => array('id'), 'class' => 'CreditLedger', 'MyCreditAccount' => array( 'fields' => array('id'), 'class' => 'Account'), ), 'MyOtherLedger' => array( 'fields' => array('id'), 'class' => 'Ledger', 'alias' => 'AliasToMyOtherLedger', 'Account' => array( 'fields' => array('id'), 'alias' => 'AliasToMyOtherAccount'), ), ), 'conditions' => array('LedgerEntry.id' => $id), )); **********************************************************************/ 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, 'linkalias' => true, ); protected $_defaults = array('type' => 'LEFT'); function pr($lev, $mixed) { if ($lev >= 3) return; pr($mixed); return; $trace = debug_backtrace(false); //array_shift($trace); $calls = array(); foreach ($trace AS $call) { $call = array_intersect_key($call, array('file'=>1, 'line'=>1, //'class'=>1, 'function'=>1, )); $calls[] = implode("; ", $call); } pr(array('debug' => $mixed, 'stack' => $calls)); } /* * This is a function for made recursive str_replaces in an array * NOTE: The palacement of this function is terrible, but I don't * know if I really want to go down this path or not. */ function recursive_array_replace($find, $replace, &$data) { if (!isset($data)) return; if (is_array($data)) { foreach ($data as $key => $value) { if (is_array($value)) { $this->recursive_array_replace($find, $replace, $data[$key]); } else { $data[$key] = str_replace($find, $replace, $value); } } } else { $data = str_replace($find, $replace, $data); } } public function beforeFind(&$Model, $query) { if (!isset($query[$this->_key])) return $query; if (!isset($query['fields']) || $query['fields'] === true) { $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)); $reference = array('class' => $Model->alias, 'alias' => $Model->alias, ); $result = array_diff_key($query, array($this->_key => 1)); $this->buildQuery($Model, $Model->alias, $Model->alias, $query[$this->_key], $result); return $result; } function buildQuery(&$Reference, $referenceClass, $referenceAlias, $links, &$result) { if (empty($links)) return; $this->pr(10, array('begin' => 'Linkable::buildQuery', 'args' => compact('referenceClass', 'referenceAlias', 'links'), )); //$defaults = $this->_defaults;// + array($this->_key => array()); //$optionsKeys = $this->_options + array($this->_key => true); $links = Set::normalize($links); $this->pr(24, array('checkpoint' => 'Normalized links', compact('links'), )); foreach ($links as $alias => $options) { if (is_null($options)) { $options = array(); } //$options = array_intersect_key($options, $optionsKeys); //$options = am($this->_defaults, compact('alias'), $options); $options += compact('alias'); $options += $this->_defaults; if (empty($options['alias'])) { throw new InvalidArgumentException(sprintf('%s::%s must receive aliased links', get_class($this), __FUNCTION__)); } if (empty($options['class'])) $options['class'] = $alias; if (!isset($options['conditions'])) $options['conditions'] = null; elseif (!is_array($options['conditions'])) $options['conditions'] = array($options['conditions']); $this->pr(20, array('checkpoint' => 'Begin Model Work', compact('referenceAlias', 'alias', 'options'), )); $modelClass = $options['class']; $modelAlias = $options['alias']; $Model =& ClassRegistry::init($modelClass); // the incoming model to be linked in query $this->pr(12, array('checkpoint' => 'Model Established', 'Reference' => ($referenceAlias .' : '. $referenceClass . ' ('. $Reference->alias .' : '. $Reference->name .')'), 'Model' => ($modelAlias .' : '. $modelClass . ' ('. $Model->alias .' : '. $Model->name .')'), )); $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])) { $this->pr(12, array('checkpoint' => "Reference ($referenceClass) defines association to Model ($modelClass)")); $associatedThroughReference = 1; $type = $associations[$Model->alias]; $association = $Reference->{$type}[$Model->alias]; } elseif (($associations = $Model->getAssociated()) && isset($associations[$Reference->alias])) { $this->pr(12, array('checkpoint' => "Model ($modelClass) defines association to Reference ($referenceClass)")); $type = $associations[$Reference->alias]; $association = $Model->{$type}[$Reference->alias]; } else { // No relationship... make our best effort to create one. $this->pr(12, array('checkpoint' => "No assocation between Reference ($referenceClass) and Model ($modelClass)")); $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) { $primaryAlias = $referenceAlias; $foreignAlias = $modelAlias; $primaryModel = $Reference; $foreignModel = $Model; } else { $primaryAlias = $modelAlias; $foreignAlias = $referenceAlias; $primaryModel = $Model; $foreignModel = $Reference; } if ($associatedThroughReference) $associationAlias = $referenceAlias; else $associationAlias = $modelAlias; // A couple exceptions before performing a union of // options and association. Namely, most fields result // in either/or, but a couple should include BOTH the // options AND the association settings. foreach (array('fields', 'conditions') AS $fld) { if (isset($options[$fld]) && is_array($options[$fld]) && isset($association[$fld]) && is_array($association[$fld])) $options[$fld] = array_merge($options[$fld], $association[$fld]); } // For any option that's not already set, use // whatever is specified by the assocation. $options += array_intersect_key($association, $this->_options); // Replace all instances of the MODEL_ALIAS variable // tag with the correct model alias. $this->recursive_array_replace("%{MODEL_ALIAS}", $associationAlias, $options['conditions']); $this->pr(15, array('checkpoint' => 'Models Established - Check Associations', 'primaryModel' => $primaryAlias .' : '. $primaryModel->name, 'foreignModel' => $foreignAlias .' : '. $foreignModel->name, compact('type', 'association', 'options'), )); if ($type === 'hasAndBelongsToMany') { if (isset($association['with'])) $linkClass = $association['with']; else $linkClass = Inflector::classify($association['joinTable']); $Link =& $Model->{$linkClass}; if (isset($options['linkalias'])) $linkAlias = $options['linkalias']; else $linkAlias = $Link->alias; // foreignKey and associationForeignKey can refer to either // the model or the reference, depending on which class // actually defines the association. Make sure to we're // using the foreign keys to point to the right class. if ($associatedThroughReference) { $modelAFK = 'associationForeignKey'; $referenceAFK = 'foreignKey'; } else { $modelAFK = 'foreignKey'; $referenceAFK = 'associationForeignKey'; } $this->pr(17, array('checkpoint' => 'Linking HABTM', compact('linkClass', 'linkAlias', 'modelAFK', 'referenceAFK'), )); // 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[$modelAFK], $linkAlias); $referenceLink = $Link->escapeField($association[$referenceAFK], $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(null, $referenceAlias); $modelKey = $Model->escapeField(null, $modelAlias); $this->pr(21, array('checkpoint' => 'HABTM links/keys', array(compact('modelLink', 'modelKey'), compact('referenceLink', 'referenceKey')), )); // 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' => $modelAlias, '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['class'] = $linkClass; $options['alias'] = $linkAlias; $options['table'] = $Link->getDataSource()->fullTableName($Link); $options['conditions'][] = "{$referenceLink} = {$referenceKey}"; } elseif (isset($association['foreignKey']) && $association['foreignKey']) { $foreignKey = $primaryModel->escapeField($association['foreignKey'], $primaryAlias); $primaryKey = $foreignModel->escapeField($foreignModel->primaryKey, $foreignAlias); $this->pr(17, array('checkpoint' => 'Linking due to foreignKey', compact('foreignKey', '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 { $this->pr(17, array('checkpoint' => 'Linking with no logic (expecting user defined)', )); // No Foreign Key... nothing we can do. } $this->pr(19, array('checkpoint' => 'Conditions', array('options[conditions]' => $options['conditions'], ), )); if (empty($options['table'])) { $options['table'] = $db->fullTableName($Model, true); } if (!isset($options['fields']) || !is_array($options['fields'])) $options['fields'] = $db->fields($Model, $modelAlias); elseif (!empty($options['fields'])) $options['fields'] = $db->fields($Model, $modelAlias, $options['fields']); $result['fields'] = array_merge($result['fields'], $options['fields']); $result['joins'][] = array_intersect_key($options, array('type' => true, 'alias' => true, 'table' => true, 'joins' => true, 'conditions' => true)); $sublinks = array_diff_key($options, $this->_options); $this->buildQuery($Model, $modelClass, $modelAlias, $sublinks, $result); $this->pr(19, array('checkpoint' => 'Model Join Complete', compact('referenceAlias', 'modelAlias', 'options', 'result'), )); } $this->pr(20, array('return' => 'Linkable::buildQuery', compact('referenceAlias'), )); } }