git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716@388 97e9348a-65ac-dc4b-aefc-98561f571b83
486 lines
18 KiB
PHP
486 lines
18 KiB
PHP
<?php
|
|
/*
|
|
* LinkableBehavior
|
|
* Light-weight approach for data mining on deep relations between models.
|
|
* Join tables based on model relations to easily enable right to left find operations.
|
|
*
|
|
* Can be used as a alternative to the ContainableBehavior:
|
|
* - On data fetching only in right to left operations,
|
|
* wich means that in "one to many" relations (hasMany, hasAndBelongsToMany)
|
|
* should only be used from the "many to one" tables. i.e:
|
|
* To fetch all Users assigneds to a Project with ProjectAssignment,
|
|
* $Project->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 <rafaelbandeira3(at)gmail(dot)com>
|
|
*
|
|
* Licensed under The MIT License
|
|
* Redistributions of files must retain the above copyright notice.
|
|
*
|
|
* @version 1.0;
|
|
*/
|
|
|
|
|
|
/**********************************************************************
|
|
* NOTE TO <AP> 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, $Model->findQueryType,
|
|
$query[$this->_key], $result);
|
|
return $result;
|
|
}
|
|
|
|
function buildQuery(&$Reference, $referenceClass, $referenceAlias, $query_type, $links, &$result) {
|
|
if (empty($links))
|
|
return;
|
|
|
|
$this->pr(10,
|
|
array('begin' => 'Linkable::buildQuery',
|
|
'args' => compact('referenceClass', 'referenceAlias', 'query_type', '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;
|
|
|
|
$this->pr(30,
|
|
array('checkpoint' => 'Options/Association pre-merge',
|
|
compact('association', 'options'),
|
|
));
|
|
|
|
// 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) {
|
|
$this->pr(31,
|
|
array('checkpoint' => 'Options/Associations field original',
|
|
compact('fld') +
|
|
array("options[$fld]" =>
|
|
array('value' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? $options[$fld] : '-null-') : '-unset-',
|
|
'type' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? gettype($options[$fld]) : '-null-') : '-unset-'),
|
|
"association[$fld]" =>
|
|
array('value' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? $association[$fld] : '-null-') : '-unset-',
|
|
'type' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? gettype($association[$fld]) : '-null-') : '-unset-'),
|
|
)));
|
|
|
|
if (!isset($options[$fld]) ||
|
|
(empty($options[$fld]) && !is_array($options[$fld])))
|
|
unset($options[$fld]);
|
|
elseif (!empty($options[$fld]) && !is_array($options[$fld]))
|
|
$options[$fld] = array($options[$fld]);
|
|
|
|
if (!isset($association[$fld]) ||
|
|
(empty($association[$fld]) && !is_array($association[$fld])))
|
|
unset($association[$fld]);
|
|
elseif (!empty($association[$fld]) && !is_array($association[$fld]))
|
|
$association[$fld] = array($association[$fld]);
|
|
|
|
|
|
$this->pr(31,
|
|
array('checkpoint' => 'Options/Associations field normalize',
|
|
compact('fld') +
|
|
array("options[$fld]" =>
|
|
array('value' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? $options[$fld] : '-null-') : '-unset-',
|
|
'type' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? gettype($options[$fld]) : '-null-') : '-unset-'),
|
|
"association[$fld]" =>
|
|
array('value' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? $association[$fld] : '-null-') : '-unset-',
|
|
'type' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? gettype($association[$fld]) : '-null-') : '-unset-'),
|
|
)));
|
|
|
|
if (isset($options[$fld]) && isset($association[$fld]))
|
|
$options[$fld] = array_merge($options[$fld],
|
|
$association[$fld]);
|
|
|
|
$this->pr(31,
|
|
array('checkpoint' => 'Options/Associations field merge complete',
|
|
compact('fld') +
|
|
array("options[$fld]" =>
|
|
array('value' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? $options[$fld] : '-null-') : '-unset-',
|
|
'type' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? gettype($options[$fld]) : '-null-') : '-unset-'),
|
|
"association[$fld]" =>
|
|
array('value' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? $association[$fld] : '-null-') : '-unset-',
|
|
'type' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? gettype($association[$fld]) : '-null-') : '-unset-'),
|
|
)));
|
|
}
|
|
|
|
$this->pr(30,
|
|
array('checkpoint' => 'Options/Association post-merge',
|
|
compact('association', 'options'),
|
|
));
|
|
|
|
// 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']);
|
|
|
|
// When performing a count query, fields are useless.
|
|
// For everything else, we need to add them into the set.
|
|
if ($query_type !== 'count')
|
|
$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, $query_type, $sublinks, $result);
|
|
|
|
$this->pr(19,
|
|
array('checkpoint' => 'Model Join Complete',
|
|
compact('referenceAlias', 'modelAlias', 'options', 'result'),
|
|
));
|
|
}
|
|
|
|
$this->pr(20,
|
|
array('return' => 'Linkable::buildQuery',
|
|
compact('referenceAlias'),
|
|
));
|
|
}
|
|
} |