Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
240a17a
* This is a fork by GiulianoB (on github)
Jun 5, 2009
ef9ada6
Jun 5, 2009
97764ef
Adding tests and fixtures, proves problem with COUNT queries for pagi…
Terr Mar 12, 2010
eb4c934
Exempts COUNT queries from field list modification, fixing pagination
Terr Mar 12, 2010
4f2f705
Refactoring testLinkable method name
Terr Mar 14, 2010
192d451
Adding ORDER BY pagination tests
Terr Mar 14, 2010
1d0d9c5
Converting spaces to tabs
Terr Mar 14, 2010
dc7b7f9
Adding more tests (on-the-fly association, hasMany) and fixtures
Terr Mar 14, 2010
d4d5f1e
Replaces obsolete Model::bind() call for Model::bindModel(), fixing e…
Terr Mar 15, 2010
22bc069
Adding more tests and fixtures. Code coverage currently at 88.78%
Terr Mar 19, 2010
5961748
Removes if-condition that prevents field lists to default to the mode…
Terr Mar 19, 2010
b4c5f51
Swapping old README for new one which includes installation instructi…
Terr Mar 31, 2010
b40fad9
Updating README
Terr Mar 31, 2010
97d44af
Updating the README some more
Terr Apr 28, 2010
fe0a5d7
Fixed case in which COUNT was removed from queries
May 1, 2010
10fd61d
Adding test case for fix in fe0a5d75512f2568b2817c0162059d1bc59b6146
Terr May 1, 2010
b595283
Adding Chad Jablonski to the list of authors
Terr May 1, 2010
3e6c200
Adding giulianob to the list of authors
Terr May 8, 2010
2c1f081
Removing white spaces
Terr May 8, 2010
51074d7
Removing more white spaces
Terr May 8, 2010
a00615b
Adding new tests and fixtures for associations with custom aliases an…
Terr May 8, 2010
c2989c0
Adding newlines and brackets to make code more readable, removing tra…
Terr May 8, 2010
d936ae0
Removing test for on-the-fly HABTM associations, need to figure out t…
May 10, 2010
5c9521a
Fix issue where model that is related with a non-belongsTo relationsh…
n8man Jan 7, 2011
110d45c
Added test to confirm issue #2
Terr Jan 7, 2011
0f3b122
Merge branch 'master' of https://github.com/n8man/linkable into n8man…
Terr Jan 7, 2011
a631188
Added Nathan Porter to the list of authors
Terr Jan 7, 2011
cf3cf37
Changed protocol in GitHub URLs to 'https'
Terr Jan 7, 2011
737c287
rename to cake 2 dirs
Dec 8, 2011
f05f689
rename linkable to cake2.0
Dec 8, 2011
6276cf1
add a missing coma in README
Dec 10, 2011
358db2e
Update Behavior
Dec 10, 2011
0b6f011
revert tests but use test cfg db
Dec 15, 2011
1a9af1b
Merge in 2.0 init commits from Dereuromark
Dec 10, 2011
08809f7
make test cases run - test model was using test_suite connection now …
Dec 15, 2011
b1b9b29
remove old format fixtures
Dec 15, 2011
fbfe43a
Fixed the @fixme. Aliased models now have the Alias in fields
Feb 27, 2012
2d6f635
Fixed aliasing of join conditions when passing a 'class' param
Mar 3, 2012
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions Model/Behavior/LinkableBehavior.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?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.
* Original behavior by rafaelbandeira3 on GitHub.
* Includes modifications from Terr, n8man, and Chad Jablonski
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* GiulianoB ( https://github.com/giulianob/linkable )
*
* @version 1.0;
*
* @version 1.1:
* -Brought in improvements and test cases from Terr. However, THIS VERSION OF LINKABLE IS NOT DROP IN COMPATIBLE WITH Terr's VERSION!
* -If fields aren't specified, will now return all columns of that model
* -No need to specify the foreign key condition if a custom condition is given. Linkable will automatically include the foreign key relationship.
* -Ability to specify the exact condition Linkable should use (e.g. $this->Post->find('first', array('link' => array('User' => array('conditions' => array('exactly' => 'User.last_post_id = Post.id'))))) )
* This is usually required when doing on-the-fly joins since Linkable generally assumes a belongsTo relationship when no specific relationship is found and may produce invalid foreign key conditions.
* -Linkable will no longer break queries that use SQL COUNTs
*
* @version 1.2:
* @modified Mark Scherer
* - works with cakephp2.0 (89.84 test coverage)
*/
class LinkableBehavior extends ModelBehavior {

protected $_key = 'link';

protected $_options = array(
'type' => true, 'table' => true, 'alias' => true,
'conditions' => true, 'fields' => true, 'reference' => true,
'class' => true, 'defaults' => true
);

protected $_defaults = array('type' => 'LEFT');

public function beforeFind(Model $Model, $query) {
if (isset($query[$this->_key])) {
$optionsDefaults = $this->_defaults + array('reference' => $Model->alias, $this->_key => array());
$optionsKeys = $this->_options + array($this->_key => true);

// If containable is being used, then let it set the recursive!
if (empty($query['contain'])) {
$query = am(array('joins' => array()), $query, array('recursive' => -1));
} else {
$query = am(array('joins' => array()), $query);
}
$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']);
}

// the incoming model to be linked in query
$_Model = ClassRegistry::init($options['class']);
// the already in query model that links to $_Model
$Reference = ClassRegistry::init($options['reference']);
$db = $_Model->getDataSource();
$associations = $_Model->getAssociated();
if (isset($Reference->belongsTo[$_Model->alias])) {
$type = 'hasOne';
$association = $Reference->belongsTo[$_Model->alias];
} else if (isset($associations[$Reference->alias])) {
$type = $associations[$Reference->alias];
$association = $_Model->{$type}[$Reference->alias];
} else {
$_Model->bindModel(array('belongsTo' => array($Reference->alias)));
$type = 'belongsTo';
$association = $_Model->{$type}[$Reference->alias];
$_Model->unbindModel(array('belongsTo' => array($Reference->alias)));
}

if (!isset($options['conditions'])) {
$options['conditions'] = array();
} else if (!is_array($options['conditions'])) {
// Support for string conditions
$options['conditions'] = array($options['conditions']);
}

if (isset($options['conditions']['exactly'])) {
if (is_array($options['conditions']['exactly']))
$options['conditions'] = reset($options['conditions']['exactly']);
else
$options['conditions'] = array($options['conditions']['exactly']);
} else {
if ($type === 'belongsTo') {
$modelKey = $_Model->escapeField($association['foreignKey']);
$modelKey = str_replace($_Model->alias, $options['alias'], $modelKey);
$referenceKey = $Reference->escapeField($Reference->primaryKey);
$options['conditions'][] = "{$referenceKey} = {$modelKey}";
} elseif ($type === 'hasAndBelongsToMany') {
if (isset($association['with'])) {
$Link = $_Model->{$association['with']};
if (isset($Link->belongsTo[$_Model->alias])) {
$modelLink = $Link->escapeField($Link->belongsTo[$_Model->alias]['foreignKey']);
}
if (isset($Link->belongsTo[$Reference->alias])) {
$referenceLink = $Link->escapeField($Link->belongsTo[$Reference->alias]['foreignKey']);
}
} else {
$Link = $_Model->{Inflector::classify($association['joinTable'])};
}
if (empty($modelLink)) {
$modelLink = $Link->escapeField(Inflector::underscore($_Model->alias) . '_id');
}
if (empty($referenceLink)) {
$referenceLink = $Link->escapeField(Inflector::underscore($Reference->alias) . '_id');
}
$referenceKey = $Reference->escapeField();
$query['joins'][] = array(
'alias' => $Link->alias,
'table' => $Link->table, //$Link->getDataSource()->fullTableName($Link),
'conditions' => "{$referenceLink} = {$referenceKey}",
'type' => 'LEFT'
);
$modelKey = $_Model->escapeField();
$modelKey = str_replace($_Model->alias, $options['alias'], $modelKey);
$options['conditions'][] = "{$modelLink} = {$modelKey}";
} else {
$referenceKey = $Reference->escapeField($association['foreignKey']);
$modelKey = $_Model->escapeField($_Model->primaryKey);
$modelKey = str_replace($_Model->alias, $options['alias'], $modelKey);
$options['conditions'][] = "{$modelKey} = {$referenceKey}";
}
}

if (empty($options['table'])) {
$options['table'] = $_Model->table;
}

// Decide whether we should mess with the fields or not
// If this query is a COUNT query then we just leave it alone
if (!isset($query['fields']) || is_array($query['fields']) || strpos($query['fields'], 'COUNT(*)') === FALSE) {
if (!empty($options['fields'])) {
if ($options['fields'] === true && !empty($association['fields'])) {
$options['fields'] = $db->fields($_Model, null, $association['fields']);
} elseif ($options['fields'] === true) {
$options['fields'] = $db->fields($_Model);
} else {
$options['fields'] = $db->fields($_Model, null, $options['fields']);
}
}
else if (!isset($options['fields']) || (isset($options['fields']) && !is_array($options['fields'])))
{
if (!empty($association['fields'])) {
$options['fields'] = $db->fields($_Model, null, $association['fields']);
} else {
$options['fields'] = $db->fields($_Model);
}
}

if (!empty($options['class']) && $options['class'] !== $alias) {
$options['fields'] = str_replace($options['class'], $alias, $options['fields']);
}
if (is_array($query['fields'])) {
$query['fields'] = array_merge($query['fields'], $options['fields']);
} else {
// If user didn't specify any fields then select all fields by default (just as find would)
$query['fields'] = array_merge($db->fields($Model), $options['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, 'conditions' => true));
}
$cont++;
$notDone = isset($iterators[$cont]);
} while ($notDone);
}

unset($query['link']);

return $query;
}
}
26 changes: 0 additions & 26 deletions README

This file was deleted.

66 changes: 66 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
### Linkable Plugin
CakePHP Plugin - PHP 5 only

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.

Original behavior by rafaelbandeira3 on GitHub. Includes modifications from Terr, n8man, and Chad Jablonski

Licensed under The MIT License
Redistributions of files must retain the above copyright notice.

This version is maintaned by:
GiulianoB ( https://bitbucket.org/giulianob/linkable/ )

### version 1.1:
- Brought in improvements and test cases from Terr. However, THIS VERSION OF LINKABLE IS NOT DROP IN COMPATIBLE WITH Terr's VERSION!
- If fields aren't specified, will now return all columns of that model
- No need to specify the foreign key condition if a custom condition is given. Linkable will automatically include the foreign key relationship.
- Ability to specify the exact condition Linkable should use. This is usually required when doing on-the-fly joins since Linkable generally assumes a belongsTo relationship when no specific relationship is found and may produce invalid foreign key conditions. Example:

$this->Post->find('first', array('link' => array('User' => array('conditions' => array('exactly' => 'User.last_post_id = Post.id')))))

- Linkable will no longer break queries that use SQL COUNTs

### Complex Example

Here's a complex example using both linkable and containable at the same time :)

Relationships involved:
CasesRun is the HABTM table of TestRun <-> TestCases
CasesRun belongsTo TestRun
CasesRun belongsTo User
CasesRun belongsTo TestCase
TestCase belongsTo TestSuite
TestSuite belongsTo TestHarness
CasesRun HABTM Tags

$this->TestRun->CasesRun->find('all', array(
'link' => array(
'User' => array('fields' => 'username'),
'TestCase' => array('fields' => array('TestCase.automated', 'TestCase.name'),
'TestSuite' => array('fields' => array('TestSuite.name'),
'TestHarness' => array('fields' => array('TestHarness.name'))
)
)
),
'conditions' => array('test_run_id' => $id),
'contain' => array(
'Tag'
),
'fields' => array(
'CasesRun.id', 'CasesRun.state', 'CasesRun.modified', 'CasesRun.comments'
)
))

Output SQL:

SELECT `CasesRun`.`id`, `CasesRun`.`state`, `CasesRun`.`modified`, `CasesRun`.`comments`, `User`.`username`, `TestCase`.`automated`, `TestCase`.`name`, `TestSuite`.`name`, `TestHarness`.`name` FROM `cases_runs` AS `CasesRun` LEFT JOIN `users` AS `User` ON (`User`.`id` = `CasesRun`.`user_id`) LEFT JOIN `test_cases` AS `TestCase` ON (`TestCase`.`id` = `CasesRun`.`test_case_id`) LEFT JOIN `test_suites` AS `TestSuite` ON (`TestSuite`.`id` = `TestCase`.`test_suite_id`) LEFT JOIN `test_harnesses` AS `TestHarness` ON (`TestHarness`.`id` = `TestSuite`.`test_harness_id`) WHERE `test_run_id` = 32

SELECT `Tag`.`id`, `Tag`.`name`, `CasesRunsTag`.`id`, `CasesRunsTag`.`cases_run_id`, `CasesRunsTag`.`tag_id` FROM `tags` AS `Tag` JOIN `cases_runs_tags` AS `CasesRunsTag` ON (`CasesRunsTag`.`cases_run_id` IN (345325, 345326, 345327, 345328) AND `CasesRunsTag`.`tag_id` = `Tag`.`id`) WHERE 1 = 1

If you were to try this example with containable, you would find that it generates a lot of queries to fetch all of the data records. Linkable produces a single query with joins instead.

### More examples
Look into the unit tests for some more ways of using Linkable
Loading