git-svn-id: file:///svn-source/pmgr/branches/initial_20090526/site@49 97e9348a-65ac-dc4b-aefc-98561f571b83
237 lines
11 KiB
PHP
237 lines
11 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;
|
|
*/
|
|
|
|
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();
|
|
$associations = $_Model->getAssociated();
|
|
/* pr("Assocations of $_Model->name"); */
|
|
/* pr($associations); */
|
|
if (isset($associations[$Reference->alias])) {
|
|
$type = $associations[$Reference->alias];
|
|
$association = $_Model->{$type}[$Reference->alias];
|
|
} else {
|
|
$_Model->bind($Reference->alias);
|
|
$type = 'belongsTo';
|
|
$association = $_Model->{$type}[$Reference->alias];
|
|
$_Model->unbindModel(array('belongsTo' => array($Reference->alias)));
|
|
}
|
|
|
|
if (empty($options['conditions'])) {
|
|
if ($type === 'belongsTo') {
|
|
$modelKey = $_Model->escapeField($association['foreignKey']);
|
|
$referenceKey = $Reference->escapeField($Reference->primaryKey);
|
|
$options['conditions'] = "{$referenceKey} = {$modelKey}";
|
|
} elseif ($type === 'hasAndBelongsToMany') {
|
|
/* pr("Association"); */
|
|
/* pr($association); */
|
|
if (isset($association['with']))
|
|
$Link =& $_Model->{$association['with']};
|
|
else
|
|
$Link =& $_Model->{Inflector::classify($association['joinTable'])};
|
|
|
|
// 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']);
|
|
$referenceLink = $Link->escapeField($association['associationForeignKey']);
|
|
|
|
// 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']);
|
|
if (empty($referenceLink) && isset($Link->belongsTo[$Reference->alias]))
|
|
$referenceLink = $Link->escapeField($Link->belongsTo[$Reference->alias]['foreignKey']);
|
|
|
|
// 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');
|
|
if (empty($referenceLink))
|
|
$referenceLink = $Link->escapeField(Inflector::underscore($Reference->alias) . '_id');
|
|
|
|
// Get the primary key from the tables we're joining.
|
|
$referenceKey = $Reference->escapeField();
|
|
$modelKey = $_Model->escapeField();
|
|
|
|
/* pr("bf pre options:"); */
|
|
/* pr($options); */
|
|
|
|
/* [joins] => Array */
|
|
/* ( */
|
|
/* [0] => Array */
|
|
/* ( */
|
|
/* [type] => LEFT */
|
|
/* [table] => `pmgr_contacts_leases` */
|
|
/* [alias] => ContactsLease */
|
|
/* [joins] => Array */
|
|
/* ( */
|
|
/* [0] => Array */
|
|
/* ( */
|
|
/* [type] => INNER */
|
|
/* [table] => `pmgr_leases` */
|
|
/* [alias] => Lease */
|
|
/* [conditions] => `Lease`.`id` = `ContactsLease`.`lease_id` */
|
|
/* ) */
|
|
/* ) */
|
|
/* [conditions] => `ContactsLease`.`contact_id` = `Contact`.`id` */
|
|
/* ) */
|
|
/* ) */
|
|
/* LEFT JOIN (`pmgr_contacts_leases` AS `ContactsLease` */
|
|
/* INNER JOIN `pmgr_leases` AS `Lease` */
|
|
/* ON (`Lease`.`id` = `ContactsLease`.`lease_id`) */
|
|
/* ) ON (`ContactsLease`.`contact_id` = `Contact`.`id`) */
|
|
|
|
|
|
// 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));
|
|
|
|
// 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'] = $association['conditions'];
|
|
elseif (!empty($association['conditions']))
|
|
$options['conditions'] = array($association['conditions']);
|
|
|
|
// 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'] = $Link->alias;
|
|
$options['table'] = $Link->getDataSource()->fullTableName($Link);
|
|
$options['conditions'][] = "{$referenceLink} = {$referenceKey}";
|
|
|
|
/* pr("bf post options:"); */
|
|
/* pr($options); */
|
|
} else {
|
|
$referenceKey = $Reference->escapeField($association['foreignKey']);
|
|
$modelKey = $_Model->escapeField($_Model->primaryKey);
|
|
$options['conditions'] = "{$modelKey} = {$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'])));
|
|
}
|
|
/* pr("bf pre query:"); */
|
|
/* pr($query); */
|
|
/* $aik = array('type' => true, 'alias' => true, 'table' => true, 'joins' => true, 'conditions' => true); */
|
|
/* pr("aik"); */
|
|
/* pr($aik); */
|
|
/* $ojoin = array_intersect_key($options, $aik); */
|
|
/* pr("ojoin"); */
|
|
/* pr($ojoin); */
|
|
/* $query['joins'][] = $ojoin; */
|
|
/* pr("bf post query:"); */
|
|
/* pr($query); */
|
|
$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;
|
|
}
|
|
} |