From 240a17a873e911e7bf60197cfddad6fb754414a2 Mon Sep 17 00:00:00 2001 From: giulianob Date: Fri, 5 Jun 2009 09:26:50 -0700 Subject: [PATCH 01/37] * This is a fork by GiulianoB (on github) * It provides the following new features: * -Plays nice with Containable which means that you can force INNER JOINS for hasOne/belongsTo and at the same time do a query on a hasMany/HABTM relationship. * * -The original code required the relationship to be established from the target to the source. * (e.g. if you are linking Post => User then User would have to define a hasOne Post relationship. * However, this proves problematic when doing on-the-fly binds as you would have to bind on more than just the model you are querying from) * --- models/behaviors/linkable.php | 47 ++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php index 5b57b8d..9772f99 100644 --- a/models/behaviors/linkable.php +++ b/models/behaviors/linkable.php @@ -23,6 +23,16 @@ * Linkable Behavior. Taking it easy in your DB. * RafaelBandeira * + * + * This is a fork by GiulianoB (on github) + * It provides the following new features: + * -Plays nice with Containable which means that you can force INNER JOINS for hasOne/belongsTo and at the same time do a query on a hasMany/HABTM relationship. + * + * -The original code required the relationship to be established from the target to the source. + * (e.g. if you are linking Post => User then User would have to define a hasOne Post relationship. + * However, this proves problematic when doing on-the-fly binds as you would have to bind on more than just the model you are querying from) + * + * * Licensed under The MIT License * Redistributions of files must retain the above copyright notice. * @@ -45,7 +55,10 @@ public function beforeFind(&$Model, $query) { if (isset($query[$this->_key])) { $optionsDefaults = $this->_defaults + array('reference' => $Model->alias, $this->_key => array()); $optionsKeys = $this->_options + array($this->_key => true); - $query = am(array('joins' => array()), $query, array('recursive' => -1)); + if (empty($query['contain'])) + $query = am(array('joins' => array()), $query, array('recursive' => -1)); + else + $query = am(array('joins' => array()), $query); //if containable is being used let it set the recursive! $iterators[] = $query[$this->_key]; $cont = 0; do { @@ -63,8 +76,7 @@ public function beforeFind(&$Model, $query) { $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'])) { @@ -77,6 +89,9 @@ public function beforeFind(&$Model, $query) { if (isset($associations[$Reference->alias])) { $type = $associations[$Reference->alias]; $association = $_Model->{$type}[$Reference->alias]; + } else if (isset($Reference->belongsTo[$_Model->alias])) { + $type = 'hasOne'; + $association = $Reference->belongsTo[$_Model->alias]; } else { $_Model->bind($Reference->alias); $type = 'belongsTo'; @@ -125,6 +140,7 @@ public function beforeFind(&$Model, $query) { if (empty($options['table'])) { $options['table'] = $db->fullTableName($_Model, true); } + if (!empty($options['fields'])) { if ($options['fields'] === true && !empty($association['fields'])) { $options['fields'] = $db->fields($_Model, null, $association['fields']); @@ -133,20 +149,39 @@ public function beforeFind(&$Model, $query) { } else { $options['fields'] = $db->fields($_Model, null, $options['fields']); } - $query['fields'] = array_merge($query['fields'], $options['fields']); + + if (is_array($query['fields'])) + $query['fields'] = array_merge($query['fields'], $options['fields']); + else + $query['fields'] = array_merge($db->fields($Model), $options['fields']); } - + else if (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 (is_array($query['fields'])) + $query['fields'] = array_merge($query['fields'], $options['fields']); + else + $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']))); } + $options['conditions'] = array($options['conditions']); $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; } } \ No newline at end of file From ef9ada6a9ab68abcdcf1add7669d17663ba830e4 Mon Sep 17 00:00:00 2001 From: giulianob Date: Fri, 5 Jun 2009 09:35:52 -0700 Subject: [PATCH 02/37] --- README | 59 +++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/README b/README index 38fc648..5f984b3 100644 --- a/README +++ b/README @@ -3,24 +3,45 @@ CakePHP Plugin - PHP 5 only LinkableBehavior. Taking it easy in your DB. -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'. +This is a fork by GiulianoB (on github) +It provides the following new features: +-Plays nice with Containable which means that you can force INNER JOINS for hasOne/belongsTo and at the same time do a query on a hasMany/HABTM relationship. + +-The original code required the relationship to be established from the target to the source. +(e.g. if you are linking Post => User then User would have to define a hasOne Post relationship. +However, this proves problematic when doing on-the-fly binds as you would have to bind on more than just the model you are querying from) + +This hasn't gotten much testing but here is an example of how it can be used. -RafaelBandeira -http://rafaelbandeira3.wordpress.com +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' + ) +)) + +Example 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 -Licensed under The MIT License -Redistributions of files must retain the above copyright notice. - -@version 1.0; \ No newline at end of file From 97764ef805e8098798ebd1e9539ae014eca55762 Mon Sep 17 00:00:00 2001 From: Terr Date: Fri, 12 Mar 2010 15:32:42 +0100 Subject: [PATCH 03/37] Adding tests and fixtures, proves problem with COUNT queries for pagination --- .../cases/models/behaviors/linkable.test.php | 138 ++++++++++++++++++ tests/fixtures/profile_fixture.php | 19 +++ tests/fixtures/user_fixture.php | 18 +++ 3 files changed, 175 insertions(+) create mode 100644 tests/cases/models/behaviors/linkable.test.php create mode 100644 tests/fixtures/profile_fixture.php create mode 100644 tests/fixtures/user_fixture.php diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php new file mode 100644 index 0000000..3d4738b --- /dev/null +++ b/tests/cases/models/behaviors/linkable.test.php @@ -0,0 +1,138 @@ +User =& ClassRegistry::init('User'); + } + + public function testLinkable() + { + $arrayExpected = array( + 'User' => array('id' => 1, 'username' => 'CakePHP'), + 'Profile' => array ('id' => 1, 'user_id' => 1, 'biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.') + ); + + $arrayResult = $this->User->find('first'); + $this->assertTrue(isset($arrayResult['Profile']), 'Association via Containable: %s'); + $this->assertEqual($arrayResult, $arrayExpected, 'Association via Containable: %s'); + + $arrayResult = $this->User->find('first', array( + 'fields' => array( + 'id', + 'username' + ), + 'contain' => false, + 'link' => array( + 'Profile' => array( + 'fields' => array( + 'id', + 'user_id', + 'biography' + ) + ) + ) + )); + + // Same association, but this time with Linkable + $this->assertTrue(isset($arrayResult['Profile']), 'Association via Linkable: %s'); + $this->assertEqual($arrayResult, $arrayExpected, 'Association via Linkable: %s'); + + $arrayResult = $this->User->find('first', array( + 'contain' => false, + 'link' => array( + 'Profile' + ) + )); + + $this->assertTrue(isset($arrayResult['Profile']), 'Association via Linkable (automatic fields): %s'); + $this->assertEqual($arrayResult, $arrayExpected, 'Association via Linkable (automatic fields): %s'); + + // No field list for primary model + $arrayExpected = array( + 'User' => array('id' => 1, 'username' => 'CakePHP'), + 'Profile' => array ('biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.') + ); + + $arrayResult = $this->User->find('first', array( + 'contain' => false, + 'link' => array( + 'Profile' => array( + 'fields' => array( + 'biography' + ) + ) + ) + )); + + $this->assertTrue(isset($arrayResult['Profile']), 'Association via Linkable (no primary fields): %s'); + $this->assertEqual($arrayResult, $arrayExpected, 'Association via Linkable (no primary fields): %s'); + } + + public function testPagination() + { + $objController = new Controller(); + $objController->uses = array('User'); + $objController->constructClasses(); + $objController->params['url']['url'] = '/'; + + $objController->paginate = array( + 'fields' => array( + 'username' + ), + 'contain' => false, + 'link' => array( + 'Profile' => array( + 'fields' => array( + 'biography' + ) + ) + ), + + 'limit' => 2 + ); + + $arrayResult = $objController->paginate('User'); + + $this->assertEqual($objController->params['paging']['User']['count'], 4, 'Paging: total records count: %s'); + } +} \ No newline at end of file diff --git a/tests/fixtures/profile_fixture.php b/tests/fixtures/profile_fixture.php new file mode 100644 index 0000000..8e50e98 --- /dev/null +++ b/tests/fixtures/profile_fixture.php @@ -0,0 +1,19 @@ + array('type' => 'integer', 'key' => 'primary'), + 'user_id' => array('type' => 'integer'), + 'biography' => array('type' => 'string', 'length' => 255, 'null' => false) + ); + + var $records = array( + array ('id' => 1, 'user_id' => 1, 'biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.'), + array ('id' => 2, 'user_id' => 2, 'biography' => ''), + array ('id' => 3, 'user_id' => 3, 'biography' => ''), + array ('id' => 4, 'user_id' => 4, 'biography' => '') + ); +} diff --git a/tests/fixtures/user_fixture.php b/tests/fixtures/user_fixture.php new file mode 100644 index 0000000..f4ccc9d --- /dev/null +++ b/tests/fixtures/user_fixture.php @@ -0,0 +1,18 @@ + array('type' => 'integer', 'key' => 'primary'), + 'username' => array('type' => 'string', 'length' => 255, 'null' => false) + ); + + var $records = array( + array('id' => 1, 'username' => 'CakePHP'), + array('id' => 2, 'username' => 'Zend'), + array('id' => 3, 'username' => 'Symfony'), + array('id' => 4, 'username' => 'CodeIgniter') + ); +} From eb4c93479e3b126d796674298516051d7285f2ef Mon Sep 17 00:00:00 2001 From: Terr Date: Fri, 12 Mar 2010 15:34:57 +0100 Subject: [PATCH 04/37] Exempts COUNT queries from field list modification, fixing pagination --- models/behaviors/linkable.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php index 9772f99..625adc4 100644 --- a/models/behaviors/linkable.php +++ b/models/behaviors/linkable.php @@ -146,14 +146,22 @@ public function beforeFind(&$Model, $query) { $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']); + } + // Leave COUNT() queries alone + elseif($options['fields'] != 'COUNT(*) AS `count`') + { + $options['fields'] = $db->fields($_Model, null, $options['fields']); } - if (is_array($query['fields'])) + if (is_array($query['fields'])) + { $query['fields'] = array_merge($query['fields'], $options['fields']); - else - $query['fields'] = array_merge($db->fields($Model), $options['fields']); + } + // Leave COUNT() queries alone + elseif($query['fields'] != 'COUNT(*) AS `count`') + { + $query['fields'] = array_merge($db->fields($Model), $options['fields']); + } } else if (isset($options['fields']) && !is_array($options['fields'])) { From 4f2f7056800041106668bee901300baf0ecbdbbe Mon Sep 17 00:00:00 2001 From: Terr Date: Sun, 14 Mar 2010 11:54:23 +0100 Subject: [PATCH 05/37] Refactoring testLinkable method name --- tests/cases/models/behaviors/linkable.test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index 3d4738b..a32d502 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -45,7 +45,7 @@ public function startTest() $this->User =& ClassRegistry::init('User'); } - public function testLinkable() + public function testBelongsTo() { $arrayExpected = array( 'User' => array('id' => 1, 'username' => 'CakePHP'), From 192d451d7746f05e7518fd5252d9ba006bc0a006 Mon Sep 17 00:00:00 2001 From: Terr Date: Sun, 14 Mar 2010 11:56:16 +0100 Subject: [PATCH 06/37] Adding ORDER BY pagination tests --- .../cases/models/behaviors/linkable.test.php | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index a32d502..393c433 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -127,12 +127,47 @@ public function testPagination() ) ) ), - 'limit' => 2 ); $arrayResult = $objController->paginate('User'); $this->assertEqual($objController->params['paging']['User']['count'], 4, 'Paging: total records count: %s'); + + // Pagination with order on a row from table joined with Linkable + $objController->paginate = array( + 'fields' => array( + 'id' + ), + 'contain' => false, + 'link' => array( + 'Profile' => array( + 'fields' => array( + 'user_id' + ) + ) + ), + 'limit' => 2, + 'order' => 'Profile.user_id DESC' + ); + + $arrayResult = $objController->paginate('User'); + + $arrayExpected = array( + 0 => array( + 'User' => array( + 'id' => 4 + ), + 'Profile' => array ('user_id' => 4) + ), + 1 => array( + 'User' => array( + 'id' => 3 + ), + 'Profile' => array ('user_id' => 3) + ) + ); + + $this->assertEqual($arrayResult, $arrayExpected, 'Paging with order on join table row: %s'); } } \ No newline at end of file From 1d0d9c549271daa7a39b483a54b8cce912357244 Mon Sep 17 00:00:00 2001 From: Terr Date: Sun, 14 Mar 2010 11:59:48 +0100 Subject: [PATCH 07/37] Converting spaces to tabs --- tests/fixtures/user_fixture.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/user_fixture.php b/tests/fixtures/user_fixture.php index f4ccc9d..33eb994 100644 --- a/tests/fixtures/user_fixture.php +++ b/tests/fixtures/user_fixture.php @@ -5,8 +5,8 @@ class UserFixture extends CakeTestFixture var $name = 'User'; var $fields = array( - 'id' => array('type' => 'integer', 'key' => 'primary'), - 'username' => array('type' => 'string', 'length' => 255, 'null' => false) + 'id' => array('type' => 'integer', 'key' => 'primary'), + 'username' => array('type' => 'string', 'length' => 255, 'null' => false) ); var $records = array( From dc7b7f9d62fdc995258eaea52ce3aa6f59b6faf7 Mon Sep 17 00:00:00 2001 From: Terr Date: Sun, 14 Mar 2010 12:01:59 +0100 Subject: [PATCH 08/37] Adding more tests (on-the-fly association, hasMany) and fixtures --- .../cases/models/behaviors/linkable.test.php | 130 +++++++++++++++--- tests/fixtures/comment_fixture.php | 20 +++ tests/fixtures/generic_fixture.php | 18 +++ 3 files changed, 152 insertions(+), 16 deletions(-) create mode 100644 tests/fixtures/comment_fixture.php create mode 100644 tests/fixtures/generic_fixture.php diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index 393c433..d2ada79 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -19,6 +19,10 @@ class User extends TestModel public $hasOne = array( 'Profile' ); + + public $hasMany = array( + 'Comment' + ); } class Profile extends TestModel @@ -35,7 +39,9 @@ class LinkableTestCase extends CakeTestCase { public $fixtures = array( 'plugin.linkable.user', - 'plugin.linkable.profile' + 'plugin.linkable.profile', + 'plugin.linkable.generic', + 'plugin.linkable.comment' ); public $Post; @@ -52,10 +58,15 @@ public function testBelongsTo() 'Profile' => array ('id' => 1, 'user_id' => 1, 'biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.') ); - $arrayResult = $this->User->find('first'); - $this->assertTrue(isset($arrayResult['Profile']), 'Association via Containable: %s'); - $this->assertEqual($arrayResult, $arrayExpected, 'Association via Containable: %s'); + $arrayResult = $this->User->find('first', array( + 'contain' => array( + 'Profile' + ) + )); + $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Containable: %s'); + $this->assertEqual($arrayResult, $arrayExpected, 'belongsTo association via Containable: %s'); + // Same association, but this time with Linkable $arrayResult = $this->User->find('first', array( 'fields' => array( 'id', @@ -73,10 +84,11 @@ public function testBelongsTo() ) )); - // Same association, but this time with Linkable - $this->assertTrue(isset($arrayResult['Profile']), 'Association via Linkable: %s'); - $this->assertEqual($arrayResult, $arrayExpected, 'Association via Linkable: %s'); - + $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Linkable: %s'); + $this->assertTrue(!empty($arrayResult['Profile']), 'belongsTo association via Linkable: %s'); + $this->assertEqual($arrayResult, $arrayExpected, 'belongsTo association via Linkable: %s'); + + // Linkable association, no field lists $arrayResult = $this->User->find('first', array( 'contain' => false, 'link' => array( @@ -84,28 +96,114 @@ public function testBelongsTo() ) )); - $this->assertTrue(isset($arrayResult['Profile']), 'Association via Linkable (automatic fields): %s'); - $this->assertEqual($arrayResult, $arrayExpected, 'Association via Linkable (automatic fields): %s'); + $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Linkable (automatic fields): %s'); + $this->assertEqual($arrayResult, $arrayExpected, 'belongsTo association via Linkable (automatic fields): %s'); - // No field list for primary model + // On-the-fly association via Linkable $arrayExpected = array( 'User' => array('id' => 1, 'username' => 'CakePHP'), - 'Profile' => array ('biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.') + 'Generic' => array('id' => 1, 'text' => '') ); $arrayResult = $this->User->find('first', array( 'contain' => false, 'link' => array( - 'Profile' => array( + 'Generic' => array( + 'class' => 'Generic', + 'conditions' => 'User.id = Generic.id', 'fields' => array( - 'biography' + 'id', + 'text' ) ) ) )); - $this->assertTrue(isset($arrayResult['Profile']), 'Association via Linkable (no primary fields): %s'); - $this->assertEqual($arrayResult, $arrayExpected, 'Association via Linkable (no primary fields): %s'); + $this->assertTrue(isset($arrayResult['Generic']), 'On-the-fly belongsTo association via Linkable: %s'); + $this->assertEqual($arrayResult, $arrayExpected, 'On-the-fly belongsTo association via Linkable: %s'); + + // On-the-fly association via Linkable, with order on the associations' row + $arrayExpected = array( + 'User' => array('id' => 4, 'username' => 'CodeIgniter'), + 'Generic' => array('id' => 4, 'text' => '') + ); + + $arrayResult = $this->User->find('first', array( + 'contain' => false, + 'link' => array( + 'Generic' => array( + 'class' => 'Generic', + 'conditions' => 'User.id = Generic.id', + 'fields' => array( + 'id', + 'text' + ) + ) + ), + 'order' => 'Generic.id DESC' + )); + + $this->assertEqual($arrayResult, $arrayExpected, 'On-the-fly belongsTo association via Linkable, with order: %s'); + } + + public function testHasMany() + { + // hasMany association via Containable + $arrayExpected = array( + 'User' => array('id' => 1, 'username' => 'CakePHP'), + 'Comment' => array( + 0 => array( + 'id' => 1, + 'user_id' => 1, + 'body' => 'Text' + ), + 1 => array( + 'id' => 2, + 'user_id' => 1, + 'body' => 'Text' + ), + ) + ); + + $arrayResult = $this->User->find('first', array( + 'contain' => array( + 'Comment' + ), + 'order' => 'User.id ASC' + )); + $this->assertTrue(isset($arrayResult['Comment']), 'hasMany association via Containable: %s'); + $this->assertEqual($arrayResult, $arrayExpected, 'hasMany association via Containable: %s'); + + // Same association, but this time with Linkable + $arrayExpected = array( + 'User' => array('id' => 1, 'username' => 'CakePHP'), + 'Comment' => array( + 'id' => 1, + 'user_id' => 1, + 'body' => 'Text' + ) + ); + + $arrayResult = $this->User->find('first', array( + 'fields' => array( + 'id', + 'username' + ), + 'contain' => false, + 'link' => array( + 'Comment' => array( + 'fields' => array( + 'id', + 'user_id', + 'body' + ) + ) + ), + 'order' => 'User.id ASC', + 'group' => 'User.id' + )); + + $this->assertEqual($arrayResult, $arrayExpected, 'hasMany association via Linkable: %s'); } public function testPagination() diff --git a/tests/fixtures/comment_fixture.php b/tests/fixtures/comment_fixture.php new file mode 100644 index 0000000..ea17cd9 --- /dev/null +++ b/tests/fixtures/comment_fixture.php @@ -0,0 +1,20 @@ + array('type' => 'integer', 'key' => 'primary'), + 'user_id' => array('type' => 'integer'), + 'body' => array('type' => 'string', 'length' => 255, 'null' => false) + ); + + var $records = array( + array('id' => 1, 'user_id' => 1, 'body' => 'Text'), + array('id' => 2, 'user_id' => 1, 'body' => 'Text'), + array('id' => 3, 'user_id' => 2, 'body' => 'Text'), + array('id' => 4, 'user_id' => 3, 'body' => 'Text'), + array('id' => 5, 'user_id' => 4, 'body' => 'Text') + ); +} diff --git a/tests/fixtures/generic_fixture.php b/tests/fixtures/generic_fixture.php new file mode 100644 index 0000000..3d062a3 --- /dev/null +++ b/tests/fixtures/generic_fixture.php @@ -0,0 +1,18 @@ + array('type' => 'integer', 'key' => 'primary'), + 'text' => array('type' => 'string', 'length' => 255, 'null' => false) + ); + + var $records = array( + array ('id' => 1, 'text' => ''), + array ('id' => 2, 'text' => ''), + array ('id' => 3, 'text' => ''), + array ('id' => 4, 'text' => '') + ); +} From d4d5f1e14af1febd5da783cdae61579b56a0a7fa Mon Sep 17 00:00:00 2001 From: Terr Date: Mon, 15 Mar 2010 16:20:11 +0100 Subject: [PATCH 09/37] Replaces obsolete Model::bind() call for Model::bindModel(), fixing error in CakePHP 1.3 (RC2) --- models/behaviors/linkable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php index 625adc4..9824281 100644 --- a/models/behaviors/linkable.php +++ b/models/behaviors/linkable.php @@ -93,7 +93,7 @@ public function beforeFind(&$Model, $query) { $type = 'hasOne'; $association = $Reference->belongsTo[$_Model->alias]; } else { - $_Model->bind($Reference->alias); + $_Model->bindModel(array('belongsTo' => array($Reference->alias))); $type = 'belongsTo'; $association = $_Model->{$type}[$Reference->alias]; $_Model->unbindModel(array('belongsTo' => array($Reference->alias))); From 22bc06937830b21bf8521f7a593c048dea225028 Mon Sep 17 00:00:00 2001 From: Terr Date: Fri, 19 Mar 2010 23:54:52 +0100 Subject: [PATCH 10/37] Adding more tests and fixtures. Code coverage currently at 88.78% --- .../cases/models/behaviors/linkable.test.php | 112 ++++++++++++++++-- tests/fixtures/post_fixture.php | 17 +++ tests/fixtures/posts_tag_fixture.php | 20 ++++ tests/fixtures/tag_fixture.php | 18 +++ 4 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures/post_fixture.php create mode 100644 tests/fixtures/posts_tag_fixture.php create mode 100644 tests/fixtures/tag_fixture.php diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index d2ada79..76959c2 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -5,6 +5,8 @@ class TestModel extends CakeTestModel { + public $useDbConfig = 'test_suite'; + public $actsAs = array( 'Containable', 'Linkable.Linkable' @@ -13,9 +15,6 @@ class TestModel extends CakeTestModel class User extends TestModel { - public $name = 'User'; - public $useDbConfig = 'test_suite'; - public $hasOne = array( 'Profile' ); @@ -27,21 +26,44 @@ class User extends TestModel class Profile extends TestModel { - public $name = 'Profile'; - public $useDbConfig = 'test_suite'; - public $belongsTo = array( 'User' ); } +class Post extends TestModel +{ + public $belongsTo = array( + 'User' + ); + + public $hasAndBelongsToMany = array( + 'Tag' + ); +} + +class PostTag extends TestModel +{ +} + +class Tag extends TestModel +{ + public $hasAndBelongsToMany = array( + 'Post' + ); +} + class LinkableTestCase extends CakeTestCase { public $fixtures = array( 'plugin.linkable.user', 'plugin.linkable.profile', 'plugin.linkable.generic', - 'plugin.linkable.comment' + 'plugin.linkable.comment', + 'plugin.linkable.post', + 'plugin.linkable.posts_tag', + 'plugin.linkable.tag', + 'plugin.linkable.user' ); public $Post; @@ -98,7 +120,7 @@ public function testBelongsTo() $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Linkable (automatic fields): %s'); $this->assertEqual($arrayResult, $arrayExpected, 'belongsTo association via Linkable (automatic fields): %s'); - + // On-the-fly association via Linkable $arrayExpected = array( 'User' => array('id' => 1, 'username' => 'CakePHP'), @@ -148,7 +170,7 @@ public function testBelongsTo() public function testHasMany() { - // hasMany association via Containable + // hasMany association via Containable. Should still work when Linkable is loaded $arrayExpected = array( 'User' => array('id' => 1, 'username' => 'CakePHP'), 'Comment' => array( @@ -206,6 +228,78 @@ public function testHasMany() $this->assertEqual($arrayResult, $arrayExpected, 'hasMany association via Linkable: %s'); } + public function testComplexAssociations() + { + $this->Post =& ClassRegistry::init('Post'); + + $arrayExpected = array( + 'Post' => array('id' => 1, 'title' => 'Post 1', 'user_id' => 1), + 'Tag' => array('name' => 'General'), + 'Profile' => array('biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.'), + 'MainTag' => array('name' => 'General'), + 'Generic' => array('id' => 1,'text' => ''), + 'User' => array('id' => 1, 'username' => 'CakePHP') + ); + + $arrayResult = $this->Post->find('first', array( + 'conditions' => array( + 'MainTag.id' => 1 + ), + 'link' => array( + 'User' => array( + 'conditions' => 'Post.user_id = User.id', + 'Profile' => array( + 'fields' => array( + 'biography' + ), + 'Generic' => array( + 'class' => 'Generic', + 'conditions' => 'User.id = Generic.id' + ) + ) + ), + 'Tag' => array( + 'table' => 'tags', + 'fields' => array( + 'name' + ) + ), + 'MainTag' => array( + 'class' => 'Tag', + 'conditions' => 'PostsTag.post_id = Post.id', + 'fields' => array( + 'MainTag.name' // @fixme Wants to use class name (Tag) instead of alias (MainTag) + ) + ) + ) + )); + + $this->assertEqual($arrayExpected, $arrayResult, 'Complex find: %s'); + + // Linkable and Containable combined + $arrayExpected = array( + 'Post' => array('id' => 1, 'title' => 'Post 1', 'user_id' => 1), + 'Tag' => array( + array('id' => 1, 'name' => 'General', 'PostsTag' => array('id' => 1, 'post_id' => 1, 'tag_id' => 1, 'main' => 0)), + array('id' => 2, 'name' => 'Test I', 'PostsTag' => array('id' => 2, 'post_id' => 1, 'tag_id' => 2, 'main' => 1)) + ), + 'User' => array('id' => 1, 'username' => 'CakePHP') + ); + + $arrayResult = $this->Post->find('first', array( + 'contain' => array( + 'Tag' + ), + 'link' => array( + 'User' => array( + 'conditions' => 'User.id = Post.user_id' + ) + ) + )); + + $this->assertEqual($arrayResult, $arrayExpected, 'Linkable and Containable combined: %s'); + } + public function testPagination() { $objController = new Controller(); diff --git a/tests/fixtures/post_fixture.php b/tests/fixtures/post_fixture.php new file mode 100644 index 0000000..ab63608 --- /dev/null +++ b/tests/fixtures/post_fixture.php @@ -0,0 +1,17 @@ + array('type' => 'integer', 'key' => 'primary'), + 'title' => array('type' => 'string', 'length' => 255, 'null' => false), + 'user_id' => array('type' => 'integer'), + ); + + var $records = array( + array ('id' => 1, 'title' => 'Post 1', 'user_id' => 1), + array ('id' => 2, 'title' => 'Post 2', 'user_id' => 2) + ); +} \ No newline at end of file diff --git a/tests/fixtures/posts_tag_fixture.php b/tests/fixtures/posts_tag_fixture.php new file mode 100644 index 0000000..c041019 --- /dev/null +++ b/tests/fixtures/posts_tag_fixture.php @@ -0,0 +1,20 @@ + array('type' => 'integer', 'key' => 'primary'), + 'post_id' => array('type' => 'integer'), + 'tag_id' => array('type' => 'integer'), + 'main' => array('type' => 'integer') + ); + + var $records = array( + array ('id' => 1, 'post_id' => 1, 'tag_id' => 1, 'main' => 0), + array ('id' => 2, 'post_id' => 1, 'tag_id' => 2, 'main' => 1), + array ('id' => 3, 'post_id' => 2, 'tag_id' => 3, 'main' => 0), + array ('id' => 4, 'post_id' => 2, 'tag_id' => 4, 'main' => 0), + ); +} \ No newline at end of file diff --git a/tests/fixtures/tag_fixture.php b/tests/fixtures/tag_fixture.php new file mode 100644 index 0000000..fe8d217 --- /dev/null +++ b/tests/fixtures/tag_fixture.php @@ -0,0 +1,18 @@ + array('type' => 'integer', 'key' => 'primary'), + 'name' => array('type' => 'string', 'length' => 255, 'null' => false) + ); + + var $records = array( + array ('id' => 1, 'name' => 'General'), + array ('id' => 2, 'name' => 'Test I'), + array ('id' => 3, 'name' => 'Test II'), + array ('id' => 4, 'name' => 'Test III') + ); +} From 5961748795c903e530fc7493c05c30205f3f04e9 Mon Sep 17 00:00:00 2001 From: Terr Date: Fri, 19 Mar 2010 23:59:57 +0100 Subject: [PATCH 11/37] Removes if-condition that prevents field lists to default to the model's fields when none given in the find options --- models/behaviors/linkable.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php index 9824281..e9d3287 100644 --- a/models/behaviors/linkable.php +++ b/models/behaviors/linkable.php @@ -163,17 +163,25 @@ public function beforeFind(&$Model, $query) { $query['fields'] = array_merge($db->fields($Model), $options['fields']); } } - else if (isset($options['fields']) && !is_array($options['fields'])) + else { if (!empty($association['fields'])) + { $options['fields'] = $db->fields($_Model, null, $association['fields']); + } else + { $options['fields'] = $db->fields($_Model); + } if (is_array($query['fields'])) + { $query['fields'] = array_merge($query['fields'], $options['fields']); + } else + { $query['fields'] = array_merge($db->fields($Model), $options['fields']); + } } $options[$this->_key] = am($options[$this->_key], array_diff_key($options, $optionsKeys)); From b4c5f51db78e2cb1abf63a06d2329ebc038d6256 Mon Sep 17 00:00:00 2001 From: Terr Date: Thu, 1 Apr 2010 00:07:11 +0200 Subject: [PATCH 12/37] Swapping old README for new one which includes installation instructions and examples --- README | 47 ---------------------- README.md | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 47 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 5f984b3..0000000 --- a/README +++ /dev/null @@ -1,47 +0,0 @@ -Linkable Plugin -CakePHP Plugin - PHP 5 only - -LinkableBehavior. Taking it easy in your DB. - -This is a fork by GiulianoB (on github) -It provides the following new features: --Plays nice with Containable which means that you can force INNER JOINS for hasOne/belongsTo and at the same time do a query on a hasMany/HABTM relationship. - --The original code required the relationship to be established from the target to the source. -(e.g. if you are linking Post => User then User would have to define a hasOne Post relationship. -However, this proves problematic when doing on-the-fly binds as you would have to bind on more than just the model you are querying from) - -This hasn't gotten much testing but here is an example of how it can be used. - -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' - ) -)) - -Example 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 - diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1f314d --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# Linkable Plugin +CakePHP plugin, PHP 5 + +http://github.com/Terr/linkable + +Maintained by: +Arjen Verstoep + +Originally authored by: +RafaelBandeira +http://rafaelbandeira3.wordpress.com + +## Introduction + +Linkable is a lightweight approach for data mining on deep relations between models. Joins tables based on model relations to easily enable right to left find operations. + +## Requirements +- CakePHP 1.2.x or 1.3.x +- PHP 5 + +## Installation + +1. [Download] the latest release for your version of CakePHP or clone the Github repository + +2. Place the files in a directory called 'linkable' inside the *app/plugins* directory of your CakePHP project. + +3. Add the LinkableBehavior to a model or your AppModel: + + var $actsAs = array('Linkable.Linkable'); + +## Usage + +Use it as a option to a find call. For example, getting a Post record with their associated (belongsTo) author User record: + + $this->Post->find('first', array( + 'link' => array( + 'User' + ) + )); + +This returns a Post record with it's associated User data. However, this isn't much different from what you can do Containable, and with the same amount of queries. Things start to change when linking hasMany or hasAndBelongsToMany associations. + +Because Linkable uses joins instead of seperate queries to get associated models, it is possible to apply conditions that operate from right to left (Tag -> Post) on hasMany and hasAndBelongsToMany associations. + +For example, finding all posts with a specific tag (hasAndBelongsToMany assocation): + + $this->Post->find('all', array( + 'conditions' => array( + 'Tag.name' => 'CakePHP' + ), + 'link' => array( + 'Tag' + ) + )); + +But what if you would still like all associated tags for the posts, while still applying the condition from the previous example? Fortunately, Linkable works well together with Containable. This example also shows some of the options Linkable has: + + $this->Post->find('all', array( + 'conditions' => array( + 'TagFilter.name' => 'CakePHP' + ), + 'link' => array( + 'PostsTag' => array( + 'TagFilter' => array( + 'class' => 'Tag', + 'conditions' => 'TagFilter.id = PostsTag.tag_id', // Join condition (LEFT JOIN x ON ...) + 'fields' => array( + 'TagFilter.id' + ) + ) + ) + ), + 'contain' => array( + 'Tag' + ) + )); + +If you're thinking: yeesh, that is a lot of code, then I agree with you ;). Linkable's automagical handling of associations with non-standard names has room for improvement. Please, feel free to contribute to the project via GitHub. + +As a last example, pagination: + + $this->paginate = array( + 'fields' => array( + 'title' + ), + 'conditions' => array( + 'Tag.name' => 'CakePHP' + ), + 'link' => array( + 'Tag' + ) + 'limit' => 10 + ); + + $this->paginate('Post'); + +### Notes + +On data fetching in right to left operations, which means that in "one to many" relations (hasMany, hasAndBelongsToMany) it should be used in the opposite direction ("many to one"), i.e: + +To fetch all Users assigned to a Project: + + $this->Project->find('all', array('link' => 'User', 'conditions' => 'project_id = 1')); +This won't produce the desired result as only a single user will be returned. + + $this->User->find('all', array('link' => 'Project', 'conditions' => 'project_id = 1')); +This will fetch all users related to the specified project in one query. + +## License + +Licensed under The MIT License +Redistributions of files must retain the above copyright notice. + +[Download]: http://github.com/Terr/linkable/downloads \ No newline at end of file From b40fad9b197ff1d849374d7dec571ece37c74770 Mon Sep 17 00:00:00 2001 From: Terr Date: Thu, 1 Apr 2010 00:21:50 +0200 Subject: [PATCH 13/37] Updating README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f1f314d..cc0456d 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ CakePHP plugin, PHP 5 http://github.com/Terr/linkable Maintained by: -Arjen Verstoep +Arjen Verstoep [terr (at) terr (dot) nl] Originally authored by: -RafaelBandeira +Rafael Bandeira [rafaelbandeira3 (at) gmail (dot) com] http://rafaelbandeira3.wordpress.com ## Introduction From 97d44af3e2e59bd23b73e37b87b9731e3a695097 Mon Sep 17 00:00:00 2001 From: Terr Date: Wed, 28 Apr 2010 21:39:09 +0200 Subject: [PATCH 14/37] Updating the README some more --- README.md | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index cc0456d..0912e2b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,15 @@ # Linkable Plugin CakePHP plugin, PHP 5 -http://github.com/Terr/linkable - -Maintained by: -Arjen Verstoep [terr (at) terr (dot) nl] - -Originally authored by: -Rafael Bandeira [rafaelbandeira3 (at) gmail (dot) com] -http://rafaelbandeira3.wordpress.com - -## Introduction +## Introduction ## Linkable is a lightweight approach for data mining on deep relations between models. Joins tables based on model relations to easily enable right to left find operations. -## Requirements +## Requirements ## - CakePHP 1.2.x or 1.3.x - PHP 5 -## Installation +## Installation ## 1. [Download] the latest release for your version of CakePHP or clone the Github repository @@ -28,7 +19,7 @@ Linkable is a lightweight approach for data mining on deep relations between mod var $actsAs = array('Linkable.Linkable'); -## Usage +## Usage ## Use it as a option to a find call. For example, getting a Post record with their associated (belongsTo) author User record: @@ -77,7 +68,9 @@ But what if you would still like all associated tags for the posts, while still If you're thinking: yeesh, that is a lot of code, then I agree with you ;). Linkable's automagical handling of associations with non-standard names has room for improvement. Please, feel free to contribute to the project via GitHub. -As a last example, pagination: +### Pagination ### + +As a last example, pagination. This will find and paginate all posts with the tag 'CakePHP': $this->paginate = array( 'fields' => array( @@ -94,9 +87,9 @@ As a last example, pagination: $this->paginate('Post'); -### Notes +### Notes ## -On data fetching in right to left operations, which means that in "one to many" relations (hasMany, hasAndBelongsToMany) it should be used in the opposite direction ("many to one"), i.e: +When fetching data in right to left operations, meaning in "one to many" relations (hasMany, hasAndBelongsToMany), it should be used in the opposite direction ("many to one"), i.e: To fetch all Users assigned to a Project: @@ -106,7 +99,11 @@ This won't produce the desired result as only a single user will be returned. $this->User->find('all', array('link' => 'Project', 'conditions' => 'project_id = 1')); This will fetch all users related to the specified project in one query. -## License +## Authors ## +- Originally authored by: Rafael Bandeira (rafaelbandeira3 (at) gmail (dot) com), http://rafaelbandeira3.wordpress.com +- Maintained by: Arjen Verstoep (terr (at) terr (dot) nl) + +## License ## Licensed under The MIT License Redistributions of files must retain the above copyright notice. From fe0a5d75512f2568b2817c0162059d1bc59b6146 Mon Sep 17 00:00:00 2001 From: Chad Jablonski Date: Fri, 30 Apr 2010 21:04:28 -0700 Subject: [PATCH 15/37] Fixed case in which COUNT was removed from queries --- models/behaviors/linkable.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php index e9d3287..a0b8141 100644 --- a/models/behaviors/linkable.php +++ b/models/behaviors/linkable.php @@ -178,7 +178,8 @@ public function beforeFind(&$Model, $query) { { $query['fields'] = array_merge($query['fields'], $options['fields']); } - else + // Leave COUNT() queries alone + elseif($query['fields'] != 'COUNT(*) AS `count`') { $query['fields'] = array_merge($db->fields($Model), $options['fields']); } @@ -200,4 +201,4 @@ public function beforeFind(&$Model, $query) { return $query; } -} \ No newline at end of file +} From 10fd61db1c132820291e6986a98f0275080a74c4 Mon Sep 17 00:00:00 2001 From: Terr Date: Sat, 1 May 2010 14:01:42 +0200 Subject: [PATCH 16/37] Adding test case for fix in fe0a5d75512f2568b2817c0162059d1bc59b6146 --- tests/cases/models/behaviors/linkable.test.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index 76959c2..60ce0a6 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -361,5 +361,18 @@ public function testPagination() ); $this->assertEqual($arrayResult, $arrayExpected, 'Paging with order on join table row: %s'); + + // Pagination without specifying any fields + $objController->paginate = array( + 'contain' => false, + 'link' => array( + 'Profile' + ), + 'limit' => 2, + 'order' => 'Profile.user_id DESC' + ); + + $arrayResult = $objController->paginate('User'); + $this->assertEqual($objController->params['paging']['User']['count'], 4, 'Paging without any field lists: total records count: %s'); } } \ No newline at end of file From b59528313ef974fab7e63d956c9ff75c4cad3a5d Mon Sep 17 00:00:00 2001 From: Terr Date: Sat, 1 May 2010 14:08:43 +0200 Subject: [PATCH 17/37] Adding Chad Jablonski to the list of authors --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0912e2b..6a9a120 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,8 @@ This will fetch all users related to the specified project in one query. ## Authors ## - Originally authored by: Rafael Bandeira (rafaelbandeira3 (at) gmail (dot) com), http://rafaelbandeira3.wordpress.com -- Maintained by: Arjen Verstoep (terr (at) terr (dot) nl) +- Maintained by: Arjen Verstoep (terr (at) terr (dot) nl), http://github.com/Terr +- Chad Jablonski, http://github.com/cjab ## License ## From 3e6c200f117b10e50cf0307fe1a522caee8fbaca Mon Sep 17 00:00:00 2001 From: Terr Date: Sat, 8 May 2010 12:58:13 +0200 Subject: [PATCH 18/37] Adding giulianob to the list of authors --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6a9a120..6e2b10c 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ This will fetch all users related to the specified project in one query. ## Authors ## - Originally authored by: Rafael Bandeira (rafaelbandeira3 (at) gmail (dot) com), http://rafaelbandeira3.wordpress.com - Maintained by: Arjen Verstoep (terr (at) terr (dot) nl), http://github.com/Terr +- giulianob, http://github.com/giulianob - Chad Jablonski, http://github.com/cjab ## License ## From 2c1f081fb14ba29876902018d420555b8c28f7e9 Mon Sep 17 00:00:00 2001 From: Terr Date: Sat, 8 May 2010 17:48:41 +0200 Subject: [PATCH 19/37] Removing white spaces --- .../cases/models/behaviors/linkable.test.php | 27 +++++++++---------- tests/fixtures/legacy_company_fixture.php | 17 ++++++++++++ tests/fixtures/legacy_product_fixture.php | 18 +++++++++++++ 3 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/legacy_company_fixture.php create mode 100644 tests/fixtures/legacy_product_fixture.php diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index 60ce0a6..922731c 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -65,14 +65,14 @@ class LinkableTestCase extends CakeTestCase 'plugin.linkable.tag', 'plugin.linkable.user' ); - + public $Post; - + public function startTest() { $this->User =& ClassRegistry::init('User'); } - + public function testBelongsTo() { $arrayExpected = array( @@ -120,7 +120,7 @@ public function testBelongsTo() $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Linkable (automatic fields): %s'); $this->assertEqual($arrayResult, $arrayExpected, 'belongsTo association via Linkable (automatic fields): %s'); - + // On-the-fly association via Linkable $arrayExpected = array( 'User' => array('id' => 1, 'username' => 'CakePHP'), @@ -205,7 +205,7 @@ public function testHasMany() 'body' => 'Text' ) ); - + $arrayResult = $this->User->find('first', array( 'fields' => array( 'id', @@ -227,11 +227,11 @@ public function testHasMany() $this->assertEqual($arrayResult, $arrayExpected, 'hasMany association via Linkable: %s'); } - + public function testComplexAssociations() { $this->Post =& ClassRegistry::init('Post'); - + $arrayExpected = array( 'Post' => array('id' => 1, 'title' => 'Post 1', 'user_id' => 1), 'Tag' => array('name' => 'General'), @@ -240,7 +240,7 @@ public function testComplexAssociations() 'Generic' => array('id' => 1,'text' => ''), 'User' => array('id' => 1, 'username' => 'CakePHP') ); - + $arrayResult = $this->Post->find('first', array( 'conditions' => array( 'MainTag.id' => 1 @@ -296,17 +296,17 @@ public function testComplexAssociations() ) ) )); - + $this->assertEqual($arrayResult, $arrayExpected, 'Linkable and Containable combined: %s'); } - + public function testPagination() { $objController = new Controller(); $objController->uses = array('User'); $objController->constructClasses(); $objController->params['url']['url'] = '/'; - + $objController->paginate = array( 'fields' => array( 'username' @@ -321,7 +321,7 @@ public function testPagination() ), 'limit' => 2 ); - + $arrayResult = $objController->paginate('User'); $this->assertEqual($objController->params['paging']['User']['count'], 4, 'Paging: total records count: %s'); @@ -361,7 +361,7 @@ public function testPagination() ); $this->assertEqual($arrayResult, $arrayExpected, 'Paging with order on join table row: %s'); - + // Pagination without specifying any fields $objController->paginate = array( 'contain' => false, @@ -375,4 +375,3 @@ public function testPagination() $arrayResult = $objController->paginate('User'); $this->assertEqual($objController->params['paging']['User']['count'], 4, 'Paging without any field lists: total records count: %s'); } -} \ No newline at end of file diff --git a/tests/fixtures/legacy_company_fixture.php b/tests/fixtures/legacy_company_fixture.php new file mode 100644 index 0000000..d5cd598 --- /dev/null +++ b/tests/fixtures/legacy_company_fixture.php @@ -0,0 +1,17 @@ + array('type' => 'integer', 'key' => 'primary'), + 'company_name' => array('type' => 'string', 'length' => 255, 'null' => false), + ); + + var $records = array( + array('company_id' => 1, 'company_name' => 'Vintage Stuff Manufactory'), + array('company_id' => 2, 'company_name' => 'Modern Steam Cars Inc.'), + array('company_id' => 3, 'company_name' => 'Joe & Co Crate Shipping Company') + ); +} diff --git a/tests/fixtures/legacy_product_fixture.php b/tests/fixtures/legacy_product_fixture.php new file mode 100644 index 0000000..2207878 --- /dev/null +++ b/tests/fixtures/legacy_product_fixture.php @@ -0,0 +1,18 @@ + array('type' => 'integer', 'key' => 'primary'), + 'name' => array('type' => 'string', 'length' => 255, 'null' => false), + 'the_company_that_builds_it_id' => array('type' => 'integer'), + 'the_company_that_delivers_it_id' => array('type' => 'integer') + ); + + var $records = array( + array('product_id' => 1, 'name' => 'Velocipede', 'the_company_that_builds_it_id' => 1, 'the_company_that_delivers_it_id' => 3), + array('product_id' => 2, 'name' => 'Oruktor Amphibolos', 'the_company_that_builds_it_id' => 2, 'the_company_that_delivers_it_id' => 2), + ); +} From 51074d7bdd5b3d095a403e86c82e465119256e79 Mon Sep 17 00:00:00 2001 From: Terr Date: Sat, 8 May 2010 17:49:49 +0200 Subject: [PATCH 20/37] Removing more white spaces --- tests/cases/models/behaviors/linkable.test.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index 922731c..79d7609 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -6,7 +6,7 @@ class TestModel extends CakeTestModel { public $useDbConfig = 'test_suite'; - + public $actsAs = array( 'Containable', 'Linkable.Linkable' @@ -36,7 +36,7 @@ class Post extends TestModel public $belongsTo = array( 'User' ); - + public $hasAndBelongsToMany = array( 'Tag' ); @@ -275,7 +275,7 @@ public function testComplexAssociations() )); $this->assertEqual($arrayExpected, $arrayResult, 'Complex find: %s'); - + // Linkable and Containable combined $arrayExpected = array( 'Post' => array('id' => 1, 'title' => 'Post 1', 'user_id' => 1), @@ -285,7 +285,7 @@ public function testComplexAssociations() ), 'User' => array('id' => 1, 'username' => 'CakePHP') ); - + $arrayResult = $this->Post->find('first', array( 'contain' => array( 'Tag' From a00615b54f77de2e10d364833e6deb80c760c0d1 Mon Sep 17 00:00:00 2001 From: Terr Date: Sat, 8 May 2010 18:17:09 +0200 Subject: [PATCH 21/37] Adding new tests and fixtures for associations with custom aliases and foreign keys --- .../cases/models/behaviors/linkable.test.php | 213 +++++++++++++++++- tests/fixtures/post_fixture.php | 2 +- tests/fixtures/tag_fixture.php | 15 +- 3 files changed, 219 insertions(+), 11 deletions(-) diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index 79d7609..d76464f 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -51,6 +51,41 @@ class Tag extends TestModel public $hasAndBelongsToMany = array( 'Post' ); + + public $belongsTo = array( + 'Parent' => array( + 'className' => 'Tag', + 'foreignKey' => 'parent_id' + ) + ); +} + +class LegacyProduct extends TestModel +{ + public $primaryKey = 'product_id'; + + public $belongsTo = array( + 'Maker' => array( + 'className' => 'LegacyCompany', + 'foreignKey' => 'the_company_that_builds_it_id' + ), + 'Transporter' => array( + 'className' => 'LegacyCompany', + 'foreignKey' => 'the_company_that_delivers_it_id' + ) + ); +} + +class LegacyCompany extends TestModel +{ + public $primaryKey = 'company_id'; + + public $hasMany = array( + 'ProductsMade' => array( + 'className' => 'LegacyProduct', + 'foreignKey' => 'the_company_that_builds_it_id' + ) + ); } class LinkableTestCase extends CakeTestCase @@ -63,7 +98,9 @@ class LinkableTestCase extends CakeTestCase 'plugin.linkable.post', 'plugin.linkable.posts_tag', 'plugin.linkable.tag', - 'plugin.linkable.user' + 'plugin.linkable.user', + 'plugin.linkable.legacy_product', + 'plugin.linkable.legacy_company' ); public $Post; @@ -280,8 +317,8 @@ public function testComplexAssociations() $arrayExpected = array( 'Post' => array('id' => 1, 'title' => 'Post 1', 'user_id' => 1), 'Tag' => array( - array('id' => 1, 'name' => 'General', 'PostsTag' => array('id' => 1, 'post_id' => 1, 'tag_id' => 1, 'main' => 0)), - array('id' => 2, 'name' => 'Test I', 'PostsTag' => array('id' => 2, 'post_id' => 1, 'tag_id' => 2, 'main' => 1)) + array('id' => 1, 'name' => 'General', 'parent_id' => null, 'PostsTag' => array('id' => 1, 'post_id' => 1, 'tag_id' => 1, 'main' => 0)), + array('id' => 2, 'name' => 'Test I', 'parent_id' => 1, 'PostsTag' => array('id' => 2, 'post_id' => 1, 'tag_id' => 2, 'main' => 1)) ), 'User' => array('id' => 1, 'username' => 'CakePHP') ); @@ -375,3 +412,173 @@ public function testPagination() $arrayResult = $objController->paginate('User'); $this->assertEqual($objController->params['paging']['User']['count'], 4, 'Paging without any field lists: total records count: %s'); } + + /** + * Series of tests that assert if Linkable can adapt to assocations that + * have aliases different from their standard model names + */ + public function testNonstandardAssociationNames() + { + $this->Tag =& ClassRegistry::init('Tag'); + + $arrayExpected = array( + 'Tag' => array( + 'name' => 'Test I' + ), + 'Parent' => array( + 'name' => 'General' + ) + ); + + $arrayResult = $this->Tag->find('first', array( + 'fields' => array( + 'name' + ), + 'conditions' => array( + 'Tag.id' => 2 + ), + 'link' => array( + 'Parent' => array( + 'fields' => array( + 'name' + ) + ) + ) + )); + + $this->assertEqual($arrayExpected, $arrayResult, 'Association with non-standard name: %s'); + + + $this->LegacyProduct =& ClassRegistry::init('LegacyProduct'); + + $arrayExpected = array( + 'LegacyProduct' => array( + 'name' => 'Velocipede' + ), + 'Maker' => array( + 'company_name' => 'Vintage Stuff Manufactory' + ), + 'Transporter' => array( + 'company_name' => 'Joe & Co Crate Shipping Company' + ) + ); + + $arrayResult = $this->LegacyProduct->find('first', array( + 'fields' => array( + 'name' + ), + 'conditions' => array( + 'LegacyProduct.product_id' => 1 + ), + 'link' => array( + 'Maker' => array( + 'fields' => array( + 'company_name' + ) + ), + 'Transporter' => array( + 'fields' => array( + 'company_name' + ) + ) + ) + )); + + $this->assertEqual($arrayExpected, $arrayResult, 'belongsTo associations with custom foreignKey: %s'); + + $arrayExpected = array( + 'ProductsMade' => array( + 'name' => 'Velocipede' + ), + 'Maker' => array( + 'company_name' => 'Vintage Stuff Manufactory' + ) + ); + + $arrayResult = $this->LegacyProduct->Maker->find('first', array( + 'fields' => array( + 'company_name' + ), + 'conditions' => array( + 'Maker.company_id' => 1 + ), + 'link' => array( + 'ProductsMade' => array( + 'fields' => array( + 'name' + ) + ) + ) + )); + + $this->assertEqual($arrayExpected, $arrayResult, 'hasMany association with custom foreignKey: %s'); + + + $this->Post =& ClassRegistry::init('Post'); + + $arrayExpected = array( + 0 => array( + 'Post' => array( + 'id' => 1, + 'title' => 'Post 1', + 'user_id' => 1 + ), + 'PostsTag' => array( + 'id' => 1, + 'post_id' => 1, + 'tag_id' => 1, + 'main' => 0 + ), + 'TagFilter' => array( + 'id' => 1 + ), + 'Tag' => array( + 0 => array( + 'id' => 1, + 'name' => 'General', + 'parent_id' => NULL, + 'PostsTag' => array( + 'id' => 1, + 'post_id' => 1, + 'tag_id' => 1, + 'main' => 0 + ) + ), + 1 => array( + 'id' => 2, + 'name' => 'Test I', + 'parent_id' => 1, + 'PostsTag' => array( + 'id' => 2, + 'post_id' => 1, + 'tag_id' => 2, + 'main' => 1 + ) + ) + ) + ) + ); + + $arrayResult = $this->Post->find('all', array( + 'conditions' => array( + 'TagFilter.name' => 'General' + ), + 'link' => array( + 'PostsTag' => array( + 'TagFilter' => array( + 'class' => 'Tag', + //'conditions' => 'TagFilter.id = PostsTag.tag_id', + 'fields' => array( + 'TagFilter.id' + ) + ) + ) + ), + 'contain' => array( + 'Tag' + ) + )); + + $this->assertEqual($arrayExpected, $arrayResult, 'On-the-fly HABTM association with alias different from model name: %s'); + } +} diff --git a/tests/fixtures/post_fixture.php b/tests/fixtures/post_fixture.php index ab63608..19c132d 100644 --- a/tests/fixtures/post_fixture.php +++ b/tests/fixtures/post_fixture.php @@ -14,4 +14,4 @@ class PostFixture extends CakeTestFixture array ('id' => 1, 'title' => 'Post 1', 'user_id' => 1), array ('id' => 2, 'title' => 'Post 2', 'user_id' => 2) ); -} \ No newline at end of file +} diff --git a/tests/fixtures/tag_fixture.php b/tests/fixtures/tag_fixture.php index fe8d217..eca4f6d 100644 --- a/tests/fixtures/tag_fixture.php +++ b/tests/fixtures/tag_fixture.php @@ -3,16 +3,17 @@ class TagFixture extends CakeTestFixture { var $name = 'Tag'; - + var $fields = array( 'id' => array('type' => 'integer', 'key' => 'primary'), - 'name' => array('type' => 'string', 'length' => 255, 'null' => false) + 'name' => array('type' => 'string', 'length' => 255, 'null' => false), + 'parent_id' => array('type' => 'integer') ); - + var $records = array( - array ('id' => 1, 'name' => 'General'), - array ('id' => 2, 'name' => 'Test I'), - array ('id' => 3, 'name' => 'Test II'), - array ('id' => 4, 'name' => 'Test III') + array ('id' => 1, 'name' => 'General', 'parent_id' => null), + array ('id' => 2, 'name' => 'Test I', 'parent_id' => 1), + array ('id' => 3, 'name' => 'Test II', 'parent_id' => null), + array ('id' => 4, 'name' => 'Test III', 'parent_id' => null) ); } From c2989c000176ae78546df222b1a1ad25fbbf68bf Mon Sep 17 00:00:00 2001 From: Terr Date: Sat, 8 May 2010 18:32:10 +0200 Subject: [PATCH 22/37] Adding newlines and brackets to make code more readable, removing trailing white spaces --- models/behaviors/linkable.php | 115 ++++++++++++++++------------------ 1 file changed, 53 insertions(+), 62 deletions(-) diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php index a0b8141..4cbb709 100644 --- a/models/behaviors/linkable.php +++ b/models/behaviors/linkable.php @@ -1,48 +1,21 @@ 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 - * - * - * This is a fork by GiulianoB (on github) - * It provides the following new features: - * -Plays nice with Containable which means that you can force INNER JOINS for hasOne/belongsTo and at the same time do a query on a hasMany/HABTM relationship. - * - * -The original code required the relationship to be established from the target to the source. - * (e.g. if you are linking Post => User then User would have to define a hasOne Post relationship. - * However, this proves problematic when doing on-the-fly binds as you would have to bind on more than just the model you are querying from) - * - * * Licensed under The MIT License * Redistributions of files must retain the above copyright notice. - * - * @version 1.0; + * + * http://github.com/Terr/linkable + * + * @version 1.0; */ - + class LinkableBehavior extends ModelBehavior { - + protected $_key = 'link'; - + protected $_options = array( 'type' => true, 'table' => true, 'alias' => true, 'conditions' => true, 'fields' => true, 'reference' => true, @@ -50,48 +23,62 @@ class LinkableBehavior extends ModelBehavior { ); protected $_defaults = array('type' => 'LEFT'); - + public function beforeFind(&$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 (empty($query['contain'])) + + if (empty($query['contain'])) { $query = am(array('joins' => array()), $query, array('recursive' => -1)); - else - $query = am(array('joins' => array()), $query); //if containable is being used let it set the recursive! + } else { + // If containable is being used, then let it set the recursive! + $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']); } + $_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 $db =& $_Model->getDataSource(); $associations = $_Model->getAssociated(); + if (isset($associations[$Reference->alias])) { $type = $associations[$Reference->alias]; $association = $_Model->{$type}[$Reference->alias]; } else if (isset($Reference->belongsTo[$_Model->alias])) { $type = 'hasOne'; - $association = $Reference->belongsTo[$_Model->alias]; + $association = $Reference->belongsTo[$_Model->alias]; } else { $_Model->bindModel(array('belongsTo' => array($Reference->alias))); $type = 'belongsTo'; @@ -107,21 +94,26 @@ public function beforeFind(&$Model, $query) { } 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, @@ -129,18 +121,20 @@ public function beforeFind(&$Model, $query) { 'conditions' => "{$referenceLink} = {$referenceKey}", 'type' => 'LEFT' ); + $modelKey = $_Model->escapeField(); $options['conditions'] = "{$modelLink} = {$modelKey}"; } else { $referenceKey = $Reference->escapeField($association['foreignKey']); $modelKey = $_Model->escapeField($_Model->primaryKey); - $options['conditions'] = "{$modelKey} = {$referenceKey}"; + $options['conditions'] = "{$modelKey} = {$referenceKey}"; } } + if (empty($options['table'])) { $options['table'] = $db->fullTableName($_Model, true); } - + if (!empty($options['fields'])) { if ($options['fields'] === true && !empty($association['fields'])) { $options['fields'] = $db->fields($_Model, null, $association['fields']); @@ -152,10 +146,10 @@ public function beforeFind(&$Model, $query) { { $options['fields'] = $db->fields($_Model, null, $options['fields']); } - + if (is_array($query['fields'])) { - $query['fields'] = array_merge($query['fields'], $options['fields']); + $query['fields'] = array_merge($query['fields'], $options['fields']); } // Leave COUNT() queries alone elseif($query['fields'] != 'COUNT(*) AS `count`') @@ -165,40 +159,37 @@ public function beforeFind(&$Model, $query) { } else { - if (!empty($association['fields'])) - { + if (!empty($association['fields'])) { $options['fields'] = $db->fields($_Model, null, $association['fields']); - } - else - { + } else { $options['fields'] = $db->fields($_Model); } - - if (is_array($query['fields'])) - { + + if (is_array($query['fields'])) { $query['fields'] = array_merge($query['fields'], $options['fields']); - } - // Leave COUNT() queries alone - elseif($query['fields'] != 'COUNT(*) AS `count`') - { + } // Leave COUNT() queries alone + elseif($query['fields'] != 'COUNT(*) AS `count`') { $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']))); } + $options['conditions'] = array($options['conditions']); $query['joins'][] = array_intersect_key($options, array('type' => true, 'alias' => true, 'table' => true, 'conditions' => true)); } - ++$cont; + + $cont++; $notDone = isset($iterators[$cont]); } while ($notDone); - } + } unset($query['link']); - + return $query; } } From d936ae01bf134588bd5cfdce3b0dc2094245ebc1 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 10 May 2010 10:44:55 +0200 Subject: [PATCH 23/37] Removing test for on-the-fly HABTM associations, need to figure out the best syntax to do these kind of joins --- .../cases/models/behaviors/linkable.test.php | 68 ------------------- 1 file changed, 68 deletions(-) diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index d76464f..4409811 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -512,73 +512,5 @@ public function testNonstandardAssociationNames() )); $this->assertEqual($arrayExpected, $arrayResult, 'hasMany association with custom foreignKey: %s'); - - - $this->Post =& ClassRegistry::init('Post'); - - $arrayExpected = array( - 0 => array( - 'Post' => array( - 'id' => 1, - 'title' => 'Post 1', - 'user_id' => 1 - ), - 'PostsTag' => array( - 'id' => 1, - 'post_id' => 1, - 'tag_id' => 1, - 'main' => 0 - ), - 'TagFilter' => array( - 'id' => 1 - ), - 'Tag' => array( - 0 => array( - 'id' => 1, - 'name' => 'General', - 'parent_id' => NULL, - 'PostsTag' => array( - 'id' => 1, - 'post_id' => 1, - 'tag_id' => 1, - 'main' => 0 - ) - ), - 1 => array( - 'id' => 2, - 'name' => 'Test I', - 'parent_id' => 1, - 'PostsTag' => array( - 'id' => 2, - 'post_id' => 1, - 'tag_id' => 2, - 'main' => 1 - ) - ) - ) - ) - ); - - $arrayResult = $this->Post->find('all', array( - 'conditions' => array( - 'TagFilter.name' => 'General' - ), - 'link' => array( - 'PostsTag' => array( - 'TagFilter' => array( - 'class' => 'Tag', - //'conditions' => 'TagFilter.id = PostsTag.tag_id', - 'fields' => array( - 'TagFilter.id' - ) - ) - ) - ), - 'contain' => array( - 'Tag' - ) - )); - - $this->assertEqual($arrayExpected, $arrayResult, 'On-the-fly HABTM association with alias different from model name: %s'); } } From 5c9521a1ced0f9c8e8ebfbc7879ec143e009d85c Mon Sep 17 00:00:00 2001 From: n8man Date: Fri, 7 Jan 2011 01:30:57 -0800 Subject: [PATCH 24/37] Fix issue where model that is related with a non-belongsTo relationship (hasMany, hasOne) is aliased and the alias is related using a belongsTo, the aliased relationship is not treated as belongsTo and additional records may be returned --- models/behaviors/linkable.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php index 4cbb709..0d0f8cf 100644 --- a/models/behaviors/linkable.php +++ b/models/behaviors/linkable.php @@ -73,12 +73,12 @@ public function beforeFind(&$Model, $query) { $db =& $_Model->getDataSource(); $associations = $_Model->getAssociated(); - if (isset($associations[$Reference->alias])) { - $type = $associations[$Reference->alias]; - $association = $_Model->{$type}[$Reference->alias]; - } else if (isset($Reference->belongsTo[$_Model->alias])) { + 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'; From 110d45c838a1d3c9190fe7045601a244a78a53d0 Mon Sep 17 00:00:00 2001 From: Terr Date: Fri, 7 Jan 2011 21:07:15 +0100 Subject: [PATCH 25/37] Added test to confirm issue #2 --- .../cases/models/behaviors/linkable.test.php | 54 ++++++++++++++++++- tests/fixtures/order_item_fixture.php | 15 ++++++ tests/fixtures/shipment_fixture.php | 18 +++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/order_item_fixture.php create mode 100644 tests/fixtures/shipment_fixture.php diff --git a/tests/cases/models/behaviors/linkable.test.php b/tests/cases/models/behaviors/linkable.test.php index 4409811..e9fae43 100644 --- a/tests/cases/models/behaviors/linkable.test.php +++ b/tests/cases/models/behaviors/linkable.test.php @@ -88,6 +88,27 @@ class LegacyCompany extends TestModel ); } +class Shipment extends TestModel +{ + public $belongsTo = array( + 'OrderItem' + ); +} + +class OrderItem extends TestModel +{ + public $hasMany = array( + 'Shipment' + ); + + public $belongsTo = array( + 'ActiveShipment' => array( + 'className' => 'Shipment', + 'foreignKey' => 'active_shipment_id', + ), + ); +} + class LinkableTestCase extends CakeTestCase { public $fixtures = array( @@ -100,7 +121,9 @@ class LinkableTestCase extends CakeTestCase 'plugin.linkable.tag', 'plugin.linkable.user', 'plugin.linkable.legacy_product', - 'plugin.linkable.legacy_company' + 'plugin.linkable.legacy_company', + 'plugin.linkable.shipment', + 'plugin.linkable.order_item', ); public $Post; @@ -513,4 +536,33 @@ public function testNonstandardAssociationNames() $this->assertEqual($arrayExpected, $arrayResult, 'hasMany association with custom foreignKey: %s'); } + + public function testAliasedBelongsToWithSameModelAsHasMany() + { + $this->OrderItem =& ClassRegistry::init('OrderItem'); + + $arrayExpected = array( + 0 => array( + 'OrderItem' => array( + 'id' => 50, + 'active_shipment_id' => 320 + ), + 'ActiveShipment' => array( + 'id' => 320, + 'ship_date' => '2011-01-07', + 'order_item_id' => 50 + ) + ) + ); + + $arrayResult = $this->OrderItem->find('all', array( + 'recursive' => -1, + 'conditions' => array( + 'ActiveShipment.ship_date' => date('2011-01-07'), + ), + 'link' => array('ActiveShipment'), + )); + + $this->assertEqual($arrayExpected, $arrayResult, 'belongsTo association with alias (requested), with hasMany to the same model without alias: %s'); + } } diff --git a/tests/fixtures/order_item_fixture.php b/tests/fixtures/order_item_fixture.php new file mode 100644 index 0000000..2fcf97f --- /dev/null +++ b/tests/fixtures/order_item_fixture.php @@ -0,0 +1,15 @@ + array('type' => 'integer', 'key' => 'primary'), + 'active_shipment_id' => array('type' => 'integer'), + ); + + var $records = array( + array ('id' => 50, 'active_shipment_id' => 320) + ); +} diff --git a/tests/fixtures/shipment_fixture.php b/tests/fixtures/shipment_fixture.php new file mode 100644 index 0000000..ef2d9ef --- /dev/null +++ b/tests/fixtures/shipment_fixture.php @@ -0,0 +1,18 @@ + array('type' => 'integer', 'key' => 'primary'), + 'ship_date' => array('type' => 'date'), + 'order_item_id' => array('type' => 'integer') + ); + + var $records = array( + array ('id' => 320, 'ship_date' => '2011-01-07', 'order_item_id' => 50), + array ('id' => 319, 'ship_date' => '2011-01-07', 'order_item_id' => 50), + array ('id' => 310, 'ship_date' => '2011-01-07', 'order_item_id' => 50) + ); +} From a6311884f005c1d9284ea68f46fe752ec279e386 Mon Sep 17 00:00:00 2001 From: Terr Date: Fri, 7 Jan 2011 21:54:15 +0100 Subject: [PATCH 26/37] Added Nathan Porter to the list of authors --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6e2b10c..fcb0ddb 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Use it as a option to a find call. For example, getting a Post record with their 'User' ) )); - + This returns a Post record with it's associated User data. However, this isn't much different from what you can do Containable, and with the same amount of queries. Things start to change when linking hasMany or hasAndBelongsToMany associations. Because Linkable uses joins instead of seperate queries to get associated models, it is possible to apply conditions that operate from right to left (Tag -> Post) on hasMany and hasAndBelongsToMany associations. @@ -84,7 +84,7 @@ As a last example, pagination. This will find and paginate all posts with the ta ) 'limit' => 10 ); - + $this->paginate('Post'); ### Notes ## @@ -104,10 +104,11 @@ This will fetch all users related to the specified project in one query. - Maintained by: Arjen Verstoep (terr (at) terr (dot) nl), http://github.com/Terr - giulianob, http://github.com/giulianob - Chad Jablonski, http://github.com/cjab +- Nathan Porter, https://github.com/n8man ## License ## Licensed under The MIT License Redistributions of files must retain the above copyright notice. -[Download]: http://github.com/Terr/linkable/downloads \ No newline at end of file +[Download]: http://github.com/Terr/linkable/downloads From cf3cf3724ea3b36d526e25d2c9172384f63300a6 Mon Sep 17 00:00:00 2001 From: Terr Date: Fri, 7 Jan 2011 21:54:42 +0100 Subject: [PATCH 27/37] Changed protocol in GitHub URLs to 'https' --- README.md | 8 ++++---- models/behaviors/linkable.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fcb0ddb..839aac9 100644 --- a/README.md +++ b/README.md @@ -101,9 +101,9 @@ This will fetch all users related to the specified project in one query. ## Authors ## - Originally authored by: Rafael Bandeira (rafaelbandeira3 (at) gmail (dot) com), http://rafaelbandeira3.wordpress.com -- Maintained by: Arjen Verstoep (terr (at) terr (dot) nl), http://github.com/Terr -- giulianob, http://github.com/giulianob -- Chad Jablonski, http://github.com/cjab +- Maintained by: Arjen Verstoep (terr (at) terr (dot) nl), https://github.com/Terr +- giulianob, https://github.com/giulianob +- Chad Jablonski, https://github.com/cjab - Nathan Porter, https://github.com/n8man ## License ## @@ -111,4 +111,4 @@ This will fetch all users related to the specified project in one query. Licensed under The MIT License Redistributions of files must retain the above copyright notice. -[Download]: http://github.com/Terr/linkable/downloads +[Download]: https://github.com/Terr/linkable/downloads diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php index 0d0f8cf..280b335 100644 --- a/models/behaviors/linkable.php +++ b/models/behaviors/linkable.php @@ -7,7 +7,7 @@ * Licensed under The MIT License * Redistributions of files must retain the above copyright notice. * - * http://github.com/Terr/linkable + * https://github.com/Terr/linkable * * @version 1.0; */ From 737c287fca1afab2125e0d7e0c4ffa63f99a90ad Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 8 Dec 2011 20:16:20 +0000 Subject: [PATCH 28/37] rename to cake 2 dirs --- {models/behaviors => Model/Behavior}/linkable.php | 0 {tests/cases => Test/Case}/models/behaviors/linkable.test.php | 0 {tests/fixtures => Test/Fixture}/comment_fixture.php | 0 {tests/fixtures => Test/Fixture}/generic_fixture.php | 0 {tests/fixtures => Test/Fixture}/legacy_company_fixture.php | 0 {tests/fixtures => Test/Fixture}/legacy_product_fixture.php | 0 {tests/fixtures => Test/Fixture}/order_item_fixture.php | 0 {tests/fixtures => Test/Fixture}/post_fixture.php | 0 {tests/fixtures => Test/Fixture}/posts_tag_fixture.php | 0 {tests/fixtures => Test/Fixture}/profile_fixture.php | 0 {tests/fixtures => Test/Fixture}/shipment_fixture.php | 0 {tests/fixtures => Test/Fixture}/tag_fixture.php | 0 {tests/fixtures => Test/Fixture}/user_fixture.php | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename {models/behaviors => Model/Behavior}/linkable.php (100%) rename {tests/cases => Test/Case}/models/behaviors/linkable.test.php (100%) rename {tests/fixtures => Test/Fixture}/comment_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/generic_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/legacy_company_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/legacy_product_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/order_item_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/post_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/posts_tag_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/profile_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/shipment_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/tag_fixture.php (100%) rename {tests/fixtures => Test/Fixture}/user_fixture.php (100%) diff --git a/models/behaviors/linkable.php b/Model/Behavior/linkable.php similarity index 100% rename from models/behaviors/linkable.php rename to Model/Behavior/linkable.php diff --git a/tests/cases/models/behaviors/linkable.test.php b/Test/Case/models/behaviors/linkable.test.php similarity index 100% rename from tests/cases/models/behaviors/linkable.test.php rename to Test/Case/models/behaviors/linkable.test.php diff --git a/tests/fixtures/comment_fixture.php b/Test/Fixture/comment_fixture.php similarity index 100% rename from tests/fixtures/comment_fixture.php rename to Test/Fixture/comment_fixture.php diff --git a/tests/fixtures/generic_fixture.php b/Test/Fixture/generic_fixture.php similarity index 100% rename from tests/fixtures/generic_fixture.php rename to Test/Fixture/generic_fixture.php diff --git a/tests/fixtures/legacy_company_fixture.php b/Test/Fixture/legacy_company_fixture.php similarity index 100% rename from tests/fixtures/legacy_company_fixture.php rename to Test/Fixture/legacy_company_fixture.php diff --git a/tests/fixtures/legacy_product_fixture.php b/Test/Fixture/legacy_product_fixture.php similarity index 100% rename from tests/fixtures/legacy_product_fixture.php rename to Test/Fixture/legacy_product_fixture.php diff --git a/tests/fixtures/order_item_fixture.php b/Test/Fixture/order_item_fixture.php similarity index 100% rename from tests/fixtures/order_item_fixture.php rename to Test/Fixture/order_item_fixture.php diff --git a/tests/fixtures/post_fixture.php b/Test/Fixture/post_fixture.php similarity index 100% rename from tests/fixtures/post_fixture.php rename to Test/Fixture/post_fixture.php diff --git a/tests/fixtures/posts_tag_fixture.php b/Test/Fixture/posts_tag_fixture.php similarity index 100% rename from tests/fixtures/posts_tag_fixture.php rename to Test/Fixture/posts_tag_fixture.php diff --git a/tests/fixtures/profile_fixture.php b/Test/Fixture/profile_fixture.php similarity index 100% rename from tests/fixtures/profile_fixture.php rename to Test/Fixture/profile_fixture.php diff --git a/tests/fixtures/shipment_fixture.php b/Test/Fixture/shipment_fixture.php similarity index 100% rename from tests/fixtures/shipment_fixture.php rename to Test/Fixture/shipment_fixture.php diff --git a/tests/fixtures/tag_fixture.php b/Test/Fixture/tag_fixture.php similarity index 100% rename from tests/fixtures/tag_fixture.php rename to Test/Fixture/tag_fixture.php diff --git a/tests/fixtures/user_fixture.php b/Test/Fixture/user_fixture.php similarity index 100% rename from tests/fixtures/user_fixture.php rename to Test/Fixture/user_fixture.php From f05f6898d53ffe1f6c475c1c7fabee761e6efdca Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 8 Dec 2011 20:17:32 +0000 Subject: [PATCH 29/37] rename linkable to cake2.0 --- Model/Behavior/{linkable.php => LinkableBehavior.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Model/Behavior/{linkable.php => LinkableBehavior.php} (100%) diff --git a/Model/Behavior/linkable.php b/Model/Behavior/LinkableBehavior.php similarity index 100% rename from Model/Behavior/linkable.php rename to Model/Behavior/LinkableBehavior.php From 6276cf196c5632b6aaebb9c0c76646c182a668a0 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 10 Dec 2011 23:47:16 +0000 Subject: [PATCH 30/37] add a missing coma in README --- Model/Behavior/LinkableBehavior.php | 56 ++++++++++++++++++++++++----- README.md | 2 +- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/Model/Behavior/LinkableBehavior.php b/Model/Behavior/LinkableBehavior.php index 280b335..5f71db6 100644 --- a/Model/Behavior/LinkableBehavior.php +++ b/Model/Behavior/LinkableBehavior.php @@ -12,6 +12,7 @@ * @version 1.0; */ +App::uses('ModelBehavior', 'Model'); class LinkableBehavior extends ModelBehavior { protected $_key = 'link'; @@ -24,7 +25,25 @@ class LinkableBehavior extends ModelBehavior { protected $_defaults = array('type' => 'LEFT'); - public function beforeFind(&$Model, $query) { +/** + * Initializes this behavior for the model $Model + * + * @param Model $Model + * @param array $settigs list of settings to be used for this model + * @return void + */ + public function setup(Model $Model, $settings = array()) { + if (!isset($this->settings[$Model->alias])) { + $this->settings[$Model->alias] = array( + 'delimiter' => ';', + 'enclosure' => '"', + 'hasHeader' => true + ); + } + $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], $settings); + } + + public function beforeFind(Model $Model, $query) { if (isset($query[$this->_key])) { $optionsDefaults = $this->_defaults + array('reference' => $Model->alias, $this->_key => array()); @@ -67,16 +86,27 @@ public function beforeFind(&$Model, $query) { } 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 - $db =& $_Model->getDataSource(); - $associations = $_Model->getAssociated(); - + App::uses('ConnectionManager', 'Model'); + $sources = ConnectionManager::sourceList(); + //diebug($options); + + $_Model = ClassRegistry::init($options['class']); // the incoming model to be linked in query + $Reference = ClassRegistry::init($options['reference']); + //debug($_Model); + //diebug($Reference); // the already in query model that links to $_Model + //$db =& $_Model->getDataSource(); + $db = ConnectionManager::getDataSource($_Model->useDbConfig); + $associations = ConnectionManager::getDataSource($Reference->useDbConfig); + //debug($_Model); + // + // debug($Reference); + // debug($associations); + // debug($associations[$Reference->alias]); + // debug($Reference->belongsTo[$_Model->alias]); if (isset($Reference->belongsTo[$_Model->alias])) { $type = 'hasOne'; $association = $Reference->belongsTo[$_Model->alias]; - } else if (isset($associations[$Reference->alias])) { + } else if (!empty($associations[$Reference->alias])) { $type = $associations[$Reference->alias]; $association = $_Model->{$type}[$Reference->alias]; } else { @@ -125,6 +155,16 @@ public function beforeFind(&$Model, $query) { $modelKey = $_Model->escapeField(); $options['conditions'] = "{$modelLink} = {$modelKey}"; } else { + //try { + // $_Model->getDataSource()->fullTableName($_Model); + //} catch(MissingTableException $e) { + // if(array_key_exists($_Model->alias, array_flip(array_keys($Reference->belongsTo)))) { + // // debug($Reference->belongsTo[$_Model->alias]); + // //die('self join'); + // } + // //debug($_Model->alias);debug($Reference->belongsTo); + // //die('self join'); + //} $referenceKey = $Reference->escapeField($association['foreignKey']); $modelKey = $_Model->escapeField($_Model->primaryKey); $options['conditions'] = "{$modelKey} = {$referenceKey}"; diff --git a/README.md b/README.md index 839aac9..643b6a5 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ As a last example, pagination. This will find and paginate all posts with the ta ), 'link' => array( 'Tag' - ) + ), 'limit' => 10 ); From 358db2e214ea0b1c331817e36ba3a1f213f7f82f Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 10 Dec 2011 23:47:37 +0000 Subject: [PATCH 31/37] Update Behavior --- Model/Behavior/LinkableBehavior.php | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/Model/Behavior/LinkableBehavior.php b/Model/Behavior/LinkableBehavior.php index 5f71db6..b372ea9 100644 --- a/Model/Behavior/LinkableBehavior.php +++ b/Model/Behavior/LinkableBehavior.php @@ -25,6 +25,8 @@ class LinkableBehavior extends ModelBehavior { protected $_defaults = array('type' => 'LEFT'); + public $settings = array(); + /** * Initializes this behavior for the model $Model * @@ -35,9 +37,9 @@ class LinkableBehavior extends ModelBehavior { public function setup(Model $Model, $settings = array()) { if (!isset($this->settings[$Model->alias])) { $this->settings[$Model->alias] = array( - 'delimiter' => ';', - 'enclosure' => '"', - 'hasHeader' => true + 'type' => true, 'table' => true, 'alias' => true, + 'conditions' => true, 'fields' => true, 'reference' => true, + 'class' => true, 'defaults' => true ); } $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], $settings); @@ -88,21 +90,12 @@ public function beforeFind(Model $Model, $query) { } App::uses('ConnectionManager', 'Model'); $sources = ConnectionManager::sourceList(); - //diebug($options); $_Model = ClassRegistry::init($options['class']); // the incoming model to be linked in query $Reference = ClassRegistry::init($options['reference']); - //debug($_Model); - //diebug($Reference); // the already in query model that links to $_Model - //$db =& $_Model->getDataSource(); + // the already in query model that links to $_Model $db = ConnectionManager::getDataSource($_Model->useDbConfig); $associations = ConnectionManager::getDataSource($Reference->useDbConfig); - //debug($_Model); - // - // debug($Reference); - // debug($associations); - // debug($associations[$Reference->alias]); - // debug($Reference->belongsTo[$_Model->alias]); if (isset($Reference->belongsTo[$_Model->alias])) { $type = 'hasOne'; $association = $Reference->belongsTo[$_Model->alias]; @@ -155,16 +148,6 @@ public function beforeFind(Model $Model, $query) { $modelKey = $_Model->escapeField(); $options['conditions'] = "{$modelLink} = {$modelKey}"; } else { - //try { - // $_Model->getDataSource()->fullTableName($_Model); - //} catch(MissingTableException $e) { - // if(array_key_exists($_Model->alias, array_flip(array_keys($Reference->belongsTo)))) { - // // debug($Reference->belongsTo[$_Model->alias]); - // //die('self join'); - // } - // //debug($_Model->alias);debug($Reference->belongsTo); - // //die('self join'); - //} $referenceKey = $Reference->escapeField($association['foreignKey']); $modelKey = $_Model->escapeField($_Model->primaryKey); $options['conditions'] = "{$modelKey} = {$referenceKey}"; From 0b6f011cbb87c57ecd5900dff7f7518ac8c16c3b Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 15 Dec 2011 15:13:35 +0000 Subject: [PATCH 32/37] revert tests but use test cfg db --- .../Model/Behavior/LinkableBehaviorTest.php | 564 ++++++++++++++++++ Test/Fixture/CommentFixture.php | 19 + Test/Fixture/GenericFixture.php | 17 + Test/Fixture/LegacyCompanyFixture.php | 16 + Test/Fixture/LegacyProductFixture.php | 17 + Test/Fixture/OrderItemFixture.php | 14 + Test/Fixture/PostFixture.php | 16 + Test/Fixture/PostsTagFixture.php | 19 + Test/Fixture/ProfileFixture.php | 18 + Test/Fixture/ShipmentFixture.php | 17 + Test/Fixture/TagFixture.php | 18 + Test/Fixture/UserFixture.php | 17 + 12 files changed, 752 insertions(+) create mode 100644 Test/Case/Model/Behavior/LinkableBehaviorTest.php create mode 100644 Test/Fixture/CommentFixture.php create mode 100644 Test/Fixture/GenericFixture.php create mode 100644 Test/Fixture/LegacyCompanyFixture.php create mode 100644 Test/Fixture/LegacyProductFixture.php create mode 100644 Test/Fixture/OrderItemFixture.php create mode 100644 Test/Fixture/PostFixture.php create mode 100644 Test/Fixture/PostsTagFixture.php create mode 100644 Test/Fixture/ProfileFixture.php create mode 100644 Test/Fixture/ShipmentFixture.php create mode 100644 Test/Fixture/TagFixture.php create mode 100644 Test/Fixture/UserFixture.php diff --git a/Test/Case/Model/Behavior/LinkableBehaviorTest.php b/Test/Case/Model/Behavior/LinkableBehaviorTest.php new file mode 100644 index 0000000..0536f70 --- /dev/null +++ b/Test/Case/Model/Behavior/LinkableBehaviorTest.php @@ -0,0 +1,564 @@ +User = ClassRegistry::init('User'); + } + + public function endTest() { + + unset($this->User); + } + + public function testBelongsTo() + { + $arrayExpected = array( + 'User' => array('id' => 1, 'username' => 'CakePHP'), + 'Profile' => array ('id' => 1, 'user_id' => 1, 'biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.') + ); + + $arrayResult = $this->User->find('first', array( + 'contain' => array( + 'Profile' + ) + )); + $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Containable: %s'); + $this->assertEquals($arrayResult, $arrayExpected, 'belongsTo association via Containable: %s'); + + // Same association, but this time with Linkable + $arrayResult = $this->User->find('first', array( + 'fields' => array( + 'id', + 'username' + ), + 'contain' => false, + 'link' => array( + 'Profile' => array( + 'fields' => array( + 'id', + 'user_id', + 'biography' + ) + ) + ) + )); + + $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Linkable: %s'); + $this->assertTrue(!empty($arrayResult['Profile']), 'belongsTo association via Linkable: %s'); + $this->assertEquals($arrayResult, $arrayExpected, 'belongsTo association via Linkable: %s'); + + // Linkable association, no field lists + $arrayResult = $this->User->find('first', array( + 'contain' => false, + 'link' => array( + 'Profile' + ) + )); + + $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Linkable (automatic fields): %s'); + $this->assertEquals($arrayResult, $arrayExpected, 'belongsTo association via Linkable (automatic fields): %s'); + + // On-the-fly association via Linkable + $arrayExpected = array( + 'User' => array('id' => 1, 'username' => 'CakePHP'), + 'Generic' => array('id' => 1, 'text' => '') + ); + + $arrayResult = $this->User->find('first', array( + 'contain' => false, + 'link' => array( + 'Generic' => array( + 'class' => 'Generic', + 'conditions' => array('exactly' => 'User.id = Generic.id'), + 'fields' => array( + 'id', + 'text' + ) + ) + ) + )); + + $this->assertTrue(isset($arrayResult['Generic']), 'On-the-fly belongsTo association via Linkable: %s'); + $this->assertEquals($arrayResult, $arrayExpected, 'On-the-fly belongsTo association via Linkable: %s'); + + // On-the-fly association via Linkable, with order on the associations' row and using array conditions instead of plain string + $arrayExpected = array( + 'User' => array('id' => 4, 'username' => 'CodeIgniter'), + 'Generic' => array('id' => 4, 'text' => '') + ); + + $arrayResult = $this->User->find('first', array( + 'contain' => false, + 'link' => array( + 'Generic' => array( + 'class' => 'Generic', + 'conditions' => array('exactly' => array('User.id = Generic.id')), + 'fields' => array( + 'id', + 'text' + ) + ) + ), + 'order' => 'Generic.id DESC' + )); + + $this->assertEquals($arrayResult, $arrayExpected, 'On-the-fly belongsTo association via Linkable, with order: %s'); + } + + public function testHasMany() + { + // hasMany association via Containable. Should still work when Linkable is loaded + $arrayExpected = array( + 'User' => array('id' => 1, 'username' => 'CakePHP'), + 'Comment' => array( + 0 => array( + 'id' => 1, + 'user_id' => 1, + 'body' => 'Text' + ), + 1 => array( + 'id' => 2, + 'user_id' => 1, + 'body' => 'Text' + ), + ) + ); + + $arrayResult = $this->User->find('first', array( + 'contain' => array( + 'Comment' + ), + 'order' => 'User.id ASC' + )); + $this->assertTrue(isset($arrayResult['Comment']), 'hasMany association via Containable: %s'); + $this->assertEquals($arrayResult, $arrayExpected, 'hasMany association via Containable: %s'); + + // Same association, but this time with Linkable + $arrayExpected = array( + 'User' => array('id' => 1, 'username' => 'CakePHP'), + 'Comment' => array( + 'id' => 1, + 'user_id' => 1, + 'body' => 'Text' + ) + ); + + $arrayResult = $this->User->find('first', array( + 'fields' => array( + 'id', + 'username' + ), + 'contain' => false, + 'link' => array( + 'Comment' => array( + 'fields' => array( + 'id', + 'user_id', + 'body' + ) + ) + ), + 'order' => 'User.id ASC', + 'group' => 'User.id' + )); + + $this->assertEquals($arrayResult, $arrayExpected, 'hasMany association via Linkable: %s'); + } + + public function testComplexAssociations() + { + $this->Post = ClassRegistry::init('Post'); + + $arrayExpected = array( + 'Post' => array('id' => 1, 'title' => 'Post 1', 'user_id' => 1), + 'Tag' => array('name' => 'General'), + 'Profile' => array('biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.'), + 'MainTag' => array('name' => 'General'), + 'Generic' => array('id' => 1,'text' => ''), + 'User' => array('id' => 1, 'username' => 'CakePHP') + ); + + $arrayResult = $this->Post->find('first', array( + 'conditions' => array( + 'MainTag.id' => 1 + ), + 'link' => array( + 'User' => array( + 'Profile' => array( + 'fields' => array( + 'biography' + ), + 'Generic' => array( + 'class' => 'Generic', + 'conditions' => array('exactly' => 'User.id = Generic.id'), + ) + ) + ), + 'Tag' => array( + 'table' => 'tags', + 'fields' => array( + 'name' + ) + ), + 'MainTag' => array( + 'class' => 'Tag', + 'conditions' => array('exactly' => 'PostsTag.post_id = Post.id'), + 'fields' => array( + 'MainTag.name' // @fixme Wants to use class name (Tag) instead of alias (MainTag) + ) + ) + ) + )); + + $this->assertEquals($arrayExpected, $arrayResult, 'Complex find: %s'); + + // Linkable and Containable combined + $arrayExpected = array( + 'Post' => array('id' => 1, 'title' => 'Post 1', 'user_id' => 1), + 'Tag' => array( + array('id' => 1, 'name' => 'General', 'parent_id' => null, 'PostsTag' => array('id' => 1, 'post_id' => 1, 'tag_id' => 1, 'main' => 0)), + array('id' => 2, 'name' => 'Test I', 'parent_id' => 1, 'PostsTag' => array('id' => 2, 'post_id' => 1, 'tag_id' => 2, 'main' => 1)) + ), + 'User' => array('id' => 1, 'username' => 'CakePHP') + ); + + $arrayResult = $this->Post->find('first', array( + 'contain' => array( + 'Tag' + ), + 'link' => array( + 'User' + ) + )); + + $this->assertEquals($arrayResult, $arrayExpected, 'Linkable and Containable combined: %s'); + } + + public function _testPagination() + { + $objController = new Controller(new CakeRequest('/'), new CakeResponse()); + $objController->layout = 'ajax'; + $objController->uses = array('User'); + $objController->constructClasses(); + $objController->request->url = '/'; + + $objController->paginate = array( + 'fields' => array( + 'username' + ), + 'contain' => false, + 'link' => array( + 'Profile' => array( + 'fields' => array( + 'biography' + ) + ) + ), + 'limit' => 2 + ); + + $arrayResult = $objController->paginate('User'); + + $this->assertEquals($objController->params['paging']['User']['count'], 4, 'Paging: total records count: %s'); + + // Pagination with order on a row from table joined with Linkable + $objController->paginate = array( + 'fields' => array( + 'id' + ), + 'contain' => false, + 'link' => array( + 'Profile' => array( + 'fields' => array( + 'user_id' + ) + ) + ), + 'limit' => 2, + 'order' => 'Profile.user_id DESC' + ); + + $arrayResult = $objController->paginate('User'); + + $arrayExpected = array( + 0 => array( + 'User' => array( + 'id' => 4 + ), + 'Profile' => array ('user_id' => 4) + ), + 1 => array( + 'User' => array( + 'id' => 3 + ), + 'Profile' => array ('user_id' => 3) + ) + ); + + $this->assertEquals($arrayResult, $arrayExpected, 'Paging with order on join table row: %s'); + + // Pagination without specifying any fields + $objController->paginate = array( + 'contain' => false, + 'link' => array( + 'Profile' + ), + 'limit' => 2, + 'order' => 'Profile.user_id DESC' + ); + + $arrayResult = $objController->paginate('User'); + $this->assertEquals($objController->params['paging']['User']['count'], 4, 'Paging without any field lists: total records count: %s'); + } + + /** + * Series of tests that assert if Linkable can adapt to assocations that + * have aliases different from their standard model names + */ + public function _testNonstandardAssociationNames() + { + $this->Tag = ClassRegistry::init('Tag'); + + $arrayExpected = array( + 'Tag' => array( + 'name' => 'Test I' + ), + 'Parent' => array( + 'name' => 'General' + ) + ); + + $arrayResult = $this->Tag->find('first', array( + 'fields' => array( + 'name' + ), + 'conditions' => array( + 'Tag.id' => 2 + ), + 'link' => array( + 'Parent' => array( + 'fields' => array( + 'name' + ) + ) + ) + )); + + $this->assertEquals($arrayExpected, $arrayResult, 'Association with non-standard name: %s'); + + + $this->LegacyProduct = ClassRegistry::init('LegacyProduct'); + + $arrayExpected = array( + 'LegacyProduct' => array( + 'name' => 'Velocipede' + ), + 'Maker' => array( + 'company_name' => 'Vintage Stuff Manufactory' + ), + 'Transporter' => array( + 'company_name' => 'Joe & Co Crate Shipping Company' + ) + ); + + $arrayResult = $this->LegacyProduct->find('first', array( + 'fields' => array( + 'name' + ), + 'conditions' => array( + 'LegacyProduct.product_id' => 1 + ), + 'link' => array( + 'Maker' => array( + 'fields' => array( + 'company_name' + ) + ), + 'Transporter' => array( + 'fields' => array( + 'company_name' + ) + ) + ) + )); + + $this->assertEquals($arrayExpected, $arrayResult, 'belongsTo associations with custom foreignKey: %s'); + + $arrayExpected = array( + 'ProductsMade' => array( + 'name' => 'Velocipede' + ), + 'Maker' => array( + 'company_name' => 'Vintage Stuff Manufactory' + ) + ); + + $arrayResult = $this->LegacyProduct->Maker->find('first', array( + 'fields' => array( + 'company_name' + ), + 'conditions' => array( + 'Maker.company_id' => 1 + ), + 'link' => array( + 'ProductsMade' => array( + 'fields' => array( + 'name' + ) + ) + ) + )); + + $this->assertEquals($arrayExpected, $arrayResult, 'hasMany association with custom foreignKey: %s'); + } + + public function _testAliasedBelongsToWithSameModelAsHasMany() + { + $this->OrderItem = ClassRegistry::init('OrderItem'); + + $arrayExpected = array( + 0 => array( + 'OrderItem' => array( + 'id' => 50, + 'active_shipment_id' => 320 + ), + 'ActiveShipment' => array( + 'id' => 320, + 'ship_date' => '2011-01-07', + 'order_item_id' => 50 + ) + ) + ); + + $arrayResult = $this->OrderItem->find('all', array( + 'recursive' => -1, + 'conditions' => array( + 'ActiveShipment.ship_date' => date('2011-01-07'), + ), + 'link' => array('ActiveShipment'), + )); + + $this->assertEquals($arrayExpected, $arrayResult, 'belongsTo association with alias (requested), with hasMany to the same model without alias: %s'); + } +} + + +class TestModel extends CakeTestModel { + public $useDbConfig = 'test'; + + public $recursive = 0; + + public $actsAs = array( + 'Containable', + 'Linkable.Linkable', + ); +} + +class User extends TestModel { + public $hasOne = array( + 'Profile' + ); + + public $hasMany = array( + 'Comment', + 'Post' + ); +} + +class Profile extends TestModel { + public $belongsTo = array( + 'User' + ); +} + +class Post extends TestModel { + public $belongsTo = array( + 'User' + ); + + public $hasAndBelongsToMany = array( + 'Tag' + ); +} + +class PostTag extends TestModel { +} + +class Tag extends TestModel { + public $hasAndBelongsToMany = array( + 'Post' + ); + + public $belongsTo = array( + 'Parent' => array( + 'className' => 'Tag', + 'foreignKey' => 'parent_id' + ) + ); +} + +class LegacyProduct extends TestModel { + public $primaryKey = 'product_id'; + + public $belongsTo = array( + 'Maker' => array( + 'className' => 'LegacyCompany', + 'foreignKey' => 'the_company_that_builds_it_id' + ), + 'Transporter' => array( + 'className' => 'LegacyCompany', + 'foreignKey' => 'the_company_that_delivers_it_id' + ) + ); +} + +class LegacyCompany extends TestModel { + public $primaryKey = 'company_id'; + + public $hasMany = array( + 'ProductsMade' => array( + 'className' => 'LegacyProduct', + 'foreignKey' => 'the_company_that_builds_it_id' + ) + ); +} + +class Shipment extends TestModel { + public $belongsTo = array( + 'OrderItem' + ); +} + +class OrderItem extends TestModel { + public $hasMany = array( + 'Shipment' + ); + + public $belongsTo = array( + 'ActiveShipment' => array( + 'className' => 'Shipment', + 'foreignKey' => 'active_shipment_id', + ), + ); +} \ No newline at end of file diff --git a/Test/Fixture/CommentFixture.php b/Test/Fixture/CommentFixture.php new file mode 100644 index 0000000..c4dd9bf --- /dev/null +++ b/Test/Fixture/CommentFixture.php @@ -0,0 +1,19 @@ + array('type' => 'integer', 'key' => 'primary'), + 'user_id' => array('type' => 'integer'), + 'body' => array('type' => 'string', 'length' => 255, 'null' => false) + ); + + public $records = array( + array('id' => 1, 'user_id' => 1, 'body' => 'Text'), + array('id' => 2, 'user_id' => 1, 'body' => 'Text'), + array('id' => 3, 'user_id' => 2, 'body' => 'Text'), + array('id' => 4, 'user_id' => 3, 'body' => 'Text'), + array('id' => 5, 'user_id' => 4, 'body' => 'Text') + ); +} diff --git a/Test/Fixture/GenericFixture.php b/Test/Fixture/GenericFixture.php new file mode 100644 index 0000000..129d44c --- /dev/null +++ b/Test/Fixture/GenericFixture.php @@ -0,0 +1,17 @@ + array('type' => 'integer', 'key' => 'primary'), + 'text' => array('type' => 'string', 'length' => 255, 'null' => false) + ); + + public $records = array( + array ('id' => 1, 'text' => ''), + array ('id' => 2, 'text' => ''), + array ('id' => 3, 'text' => ''), + array ('id' => 4, 'text' => '') + ); +} diff --git a/Test/Fixture/LegacyCompanyFixture.php b/Test/Fixture/LegacyCompanyFixture.php new file mode 100644 index 0000000..624fc0b --- /dev/null +++ b/Test/Fixture/LegacyCompanyFixture.php @@ -0,0 +1,16 @@ + array('type' => 'integer', 'key' => 'primary'), + 'company_name' => array('type' => 'string', 'length' => 255, 'null' => false), + ); + + public $records = array( + array('company_id' => 1, 'company_name' => 'Vintage Stuff Manufactory'), + array('company_id' => 2, 'company_name' => 'Modern Steam Cars Inc.'), + array('company_id' => 3, 'company_name' => 'Joe & Co Crate Shipping Company') + ); +} diff --git a/Test/Fixture/LegacyProductFixture.php b/Test/Fixture/LegacyProductFixture.php new file mode 100644 index 0000000..6e0474a --- /dev/null +++ b/Test/Fixture/LegacyProductFixture.php @@ -0,0 +1,17 @@ + array('type' => 'integer', 'key' => 'primary'), + 'name' => array('type' => 'string', 'length' => 255, 'null' => false), + 'the_company_that_builds_it_id' => array('type' => 'integer'), + 'the_company_that_delivers_it_id' => array('type' => 'integer') + ); + + public $records = array( + array('product_id' => 1, 'name' => 'Velocipede', 'the_company_that_builds_it_id' => 1, 'the_company_that_delivers_it_id' => 3), + array('product_id' => 2, 'name' => 'Oruktor Amphibolos', 'the_company_that_builds_it_id' => 2, 'the_company_that_delivers_it_id' => 2), + ); +} diff --git a/Test/Fixture/OrderItemFixture.php b/Test/Fixture/OrderItemFixture.php new file mode 100644 index 0000000..414d87d --- /dev/null +++ b/Test/Fixture/OrderItemFixture.php @@ -0,0 +1,14 @@ + array('type' => 'integer', 'key' => 'primary'), + 'active_shipment_id' => array('type' => 'integer'), + ); + + public $records = array( + array ('id' => 50, 'active_shipment_id' => 320) + ); +} diff --git a/Test/Fixture/PostFixture.php b/Test/Fixture/PostFixture.php new file mode 100644 index 0000000..50aac19 --- /dev/null +++ b/Test/Fixture/PostFixture.php @@ -0,0 +1,16 @@ + array('type' => 'integer', 'key' => 'primary'), + 'title' => array('type' => 'string', 'length' => 255, 'null' => false), + 'user_id' => array('type' => 'integer'), + ); + + public $records = array( + array ('id' => 1, 'title' => 'Post 1', 'user_id' => 1), + array ('id' => 2, 'title' => 'Post 2', 'user_id' => 2) + ); +} diff --git a/Test/Fixture/PostsTagFixture.php b/Test/Fixture/PostsTagFixture.php new file mode 100644 index 0000000..8ba2ed3 --- /dev/null +++ b/Test/Fixture/PostsTagFixture.php @@ -0,0 +1,19 @@ + array('type' => 'integer', 'key' => 'primary'), + 'post_id' => array('type' => 'integer'), + 'tag_id' => array('type' => 'integer'), + 'main' => array('type' => 'integer') + ); + + public $records = array( + array ('id' => 1, 'post_id' => 1, 'tag_id' => 1, 'main' => 0), + array ('id' => 2, 'post_id' => 1, 'tag_id' => 2, 'main' => 1), + array ('id' => 3, 'post_id' => 2, 'tag_id' => 3, 'main' => 0), + array ('id' => 4, 'post_id' => 2, 'tag_id' => 4, 'main' => 0), + ); +} \ No newline at end of file diff --git a/Test/Fixture/ProfileFixture.php b/Test/Fixture/ProfileFixture.php new file mode 100644 index 0000000..6fbc4dd --- /dev/null +++ b/Test/Fixture/ProfileFixture.php @@ -0,0 +1,18 @@ + array('type' => 'integer', 'key' => 'primary'), + 'user_id' => array('type' => 'integer'), + 'biography' => array('type' => 'string', 'length' => 255, 'null' => false) + ); + + public $records = array( + array ('id' => 1, 'user_id' => 1, 'biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.'), + array ('id' => 2, 'user_id' => 2, 'biography' => ''), + array ('id' => 3, 'user_id' => 3, 'biography' => ''), + array ('id' => 4, 'user_id' => 4, 'biography' => '') + ); +} diff --git a/Test/Fixture/ShipmentFixture.php b/Test/Fixture/ShipmentFixture.php new file mode 100644 index 0000000..ab356ef --- /dev/null +++ b/Test/Fixture/ShipmentFixture.php @@ -0,0 +1,17 @@ + array('type' => 'integer', 'key' => 'primary'), + 'ship_date' => array('type' => 'date'), + 'order_item_id' => array('type' => 'integer') + ); + + public $records = array( + array ('id' => 320, 'ship_date' => '2011-01-07', 'order_item_id' => 50), + array ('id' => 319, 'ship_date' => '2011-01-07', 'order_item_id' => 50), + array ('id' => 310, 'ship_date' => '2011-01-07', 'order_item_id' => 50) + ); +} diff --git a/Test/Fixture/TagFixture.php b/Test/Fixture/TagFixture.php new file mode 100644 index 0000000..44a5587 --- /dev/null +++ b/Test/Fixture/TagFixture.php @@ -0,0 +1,18 @@ + array('type' => 'integer', 'key' => 'primary'), + 'name' => array('type' => 'string', 'length' => 255, 'null' => false), + 'parent_id' => array('type' => 'integer') + ); + + public $records = array( + array ('id' => 1, 'name' => 'General', 'parent_id' => null), + array ('id' => 2, 'name' => 'Test I', 'parent_id' => 1), + array ('id' => 3, 'name' => 'Test II', 'parent_id' => null), + array ('id' => 4, 'name' => 'Test III', 'parent_id' => null) + ); +} diff --git a/Test/Fixture/UserFixture.php b/Test/Fixture/UserFixture.php new file mode 100644 index 0000000..aeece22 --- /dev/null +++ b/Test/Fixture/UserFixture.php @@ -0,0 +1,17 @@ + array('type' => 'integer', 'key' => 'primary'), + 'username' => array('type' => 'string', 'length' => 255, 'null' => false) + ); + + public $records = array( + array('id' => 1, 'username' => 'CakePHP'), + array('id' => 2, 'username' => 'Zend'), + array('id' => 3, 'username' => 'Symfony'), + array('id' => 4, 'username' => 'CodeIgniter') + ); +} From 1a9af1bc7b9bba7121f2d0d15b941cc06fd2bead Mon Sep 17 00:00:00 2001 From: euromark Date: Sat, 10 Dec 2011 02:06:11 +0100 Subject: [PATCH 33/37] Merge in 2.0 init commits from Dereuromark --- Model/Behavior/LinkableBehavior.php | 216 +++---- README.markdown | 66 ++ README.md | 114 ---- .../Model/Behavior/LinkableBehaviorTest.php | 2 +- Test/Case/models/behaviors/linkable.test.php | 568 ------------------ 5 files changed, 166 insertions(+), 800 deletions(-) create mode 100644 README.markdown delete mode 100644 README.md delete mode 100644 Test/Case/models/behaviors/linkable.test.php diff --git a/Model/Behavior/LinkableBehavior.php b/Model/Behavior/LinkableBehavior.php index b372ea9..dc39b83 100644 --- a/Model/Behavior/LinkableBehavior.php +++ b/Model/Behavior/LinkableBehavior.php @@ -1,22 +1,34 @@ 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 (not fully confirmed - test cases wont work) */ - -App::uses('ModelBehavior', 'Model'); class LinkableBehavior extends ModelBehavior { - + protected $_key = 'link'; - + protected $_options = array( 'type' => true, 'table' => true, 'alias' => true, 'conditions' => true, 'fields' => true, 'reference' => true, @@ -24,195 +36,165 @@ class LinkableBehavior extends ModelBehavior { ); protected $_defaults = array('type' => 'LEFT'); - - public $settings = array(); - -/** - * Initializes this behavior for the model $Model - * - * @param Model $Model - * @param array $settigs list of settings to be used for this model - * @return void - */ - public function setup(Model $Model, $settings = array()) { - if (!isset($this->settings[$Model->alias])) { - $this->settings[$Model->alias] = array( - 'type' => true, 'table' => true, 'alias' => true, - 'conditions' => true, 'fields' => true, 'reference' => true, - 'class' => true, 'defaults' => true - ); - } - $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], $settings); - } - + public function beforeFind(Model $Model, $query) { - if (isset($query[$this->_key])) { - + 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 { - // If containable is being used, then let it set the recursive! + } else { $query = am(array('joins' => array()), $query); } - $iterators[] = $query[$this->_key]; $cont = 0; - - do { + 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']); } - App::uses('ConnectionManager', 'Model'); - $sources = ConnectionManager::sourceList(); - - $_Model = ClassRegistry::init($options['class']); // the incoming model to be linked in query - $Reference = ClassRegistry::init($options['reference']); + + // the incoming model to be linked in query + $_Model = ClassRegistry::init($options['class']); // the already in query model that links to $_Model - $db = ConnectionManager::getDataSource($_Model->useDbConfig); - $associations = ConnectionManager::getDataSource($Reference->useDbConfig); + $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 (!empty($associations[$Reference->alias])) { + $association = $Reference->belongsTo[$_Model->alias]; + } else if (isset($associations[$Reference->alias])) { $type = $associations[$Reference->alias]; - $association = $_Model->{$type}[$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 (empty($options['conditions'])) { + + 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']); $referenceKey = $Reference->escapeField($Reference->primaryKey); - $options['conditions'] = "{$referenceKey} = {$modelKey}"; + $options['conditions'][] = "{$referenceKey} = {$modelKey}"; } elseif ($type === 'hasAndBelongsToMany') { if (isset($association['with'])) { - $Link =& $_Model->{$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'])}; + $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->getDataSource()->fullTableName($Link), + 'table' => $Link->table, //$Link->getDataSource()->fullTableName($Link), 'conditions' => "{$referenceLink} = {$referenceKey}", 'type' => 'LEFT' ); - $modelKey = $_Model->escapeField(); - $options['conditions'] = "{$modelLink} = {$modelKey}"; + $options['conditions'][] = "{$modelLink} = {$modelKey}"; } else { $referenceKey = $Reference->escapeField($association['foreignKey']); $modelKey = $_Model->escapeField($_Model->primaryKey); - $options['conditions'] = "{$modelKey} = {$referenceKey}"; - } + $options['conditions'][] = "{$modelKey} = {$referenceKey}"; + } } - + if (empty($options['table'])) { - $options['table'] = $db->fullTableName($_Model, true); + $options['table'] = $_Model->table; } - - 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); - } - // Leave COUNT() queries alone - elseif($options['fields'] != 'COUNT(*) AS `count`') - { - $options['fields'] = $db->fields($_Model, null, $options['fields']); - } - - if (is_array($query['fields'])) - { - $query['fields'] = array_merge($query['fields'], $options['fields']); + + // 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']); + } + + if (is_array($query['fields'])) + $query['fields'] = array_merge($query['fields'], $options['fields']); + else + $query['fields'] = array_merge($db->fields($Model), $options['fields']); } - // Leave COUNT() queries alone - elseif($query['fields'] != 'COUNT(*) AS `count`') + else if (!isset($options['fields']) || (isset($options['fields']) && !is_array($options['fields']))) { - $query['fields'] = array_merge($db->fields($Model), $options['fields']); - } - } - else - { - if (!empty($association['fields'])) { - $options['fields'] = $db->fields($_Model, null, $association['fields']); - } else { - $options['fields'] = $db->fields($_Model); - } - - if (is_array($query['fields'])) { - $query['fields'] = array_merge($query['fields'], $options['fields']); - } // Leave COUNT() queries alone - elseif($query['fields'] != 'COUNT(*) AS `count`') { - $query['fields'] = array_merge($db->fields($Model), $options['fields']); + if (!empty($association['fields'])) { + $options['fields'] = $db->fields($_Model, null, $association['fields']); + } else { + $options['fields'] = $db->fields($_Model); + } + + 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']))); } - - $options['conditions'] = array($options['conditions']); - $query['joins'][] = array_intersect_key($options, array('type' => true, 'alias' => true, 'table' => true, 'conditions' => true)); + + $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; } -} +} \ No newline at end of file diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..af6ad78 --- /dev/null +++ b/README.markdown @@ -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 diff --git a/README.md b/README.md deleted file mode 100644 index 643b6a5..0000000 --- a/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# Linkable Plugin -CakePHP plugin, PHP 5 - -## Introduction ## - -Linkable is a lightweight approach for data mining on deep relations between models. Joins tables based on model relations to easily enable right to left find operations. - -## Requirements ## -- CakePHP 1.2.x or 1.3.x -- PHP 5 - -## Installation ## - -1. [Download] the latest release for your version of CakePHP or clone the Github repository - -2. Place the files in a directory called 'linkable' inside the *app/plugins* directory of your CakePHP project. - -3. Add the LinkableBehavior to a model or your AppModel: - - var $actsAs = array('Linkable.Linkable'); - -## Usage ## - -Use it as a option to a find call. For example, getting a Post record with their associated (belongsTo) author User record: - - $this->Post->find('first', array( - 'link' => array( - 'User' - ) - )); - -This returns a Post record with it's associated User data. However, this isn't much different from what you can do Containable, and with the same amount of queries. Things start to change when linking hasMany or hasAndBelongsToMany associations. - -Because Linkable uses joins instead of seperate queries to get associated models, it is possible to apply conditions that operate from right to left (Tag -> Post) on hasMany and hasAndBelongsToMany associations. - -For example, finding all posts with a specific tag (hasAndBelongsToMany assocation): - - $this->Post->find('all', array( - 'conditions' => array( - 'Tag.name' => 'CakePHP' - ), - 'link' => array( - 'Tag' - ) - )); - -But what if you would still like all associated tags for the posts, while still applying the condition from the previous example? Fortunately, Linkable works well together with Containable. This example also shows some of the options Linkable has: - - $this->Post->find('all', array( - 'conditions' => array( - 'TagFilter.name' => 'CakePHP' - ), - 'link' => array( - 'PostsTag' => array( - 'TagFilter' => array( - 'class' => 'Tag', - 'conditions' => 'TagFilter.id = PostsTag.tag_id', // Join condition (LEFT JOIN x ON ...) - 'fields' => array( - 'TagFilter.id' - ) - ) - ) - ), - 'contain' => array( - 'Tag' - ) - )); - -If you're thinking: yeesh, that is a lot of code, then I agree with you ;). Linkable's automagical handling of associations with non-standard names has room for improvement. Please, feel free to contribute to the project via GitHub. - -### Pagination ### - -As a last example, pagination. This will find and paginate all posts with the tag 'CakePHP': - - $this->paginate = array( - 'fields' => array( - 'title' - ), - 'conditions' => array( - 'Tag.name' => 'CakePHP' - ), - 'link' => array( - 'Tag' - ), - 'limit' => 10 - ); - - $this->paginate('Post'); - -### Notes ## - -When fetching data in right to left operations, meaning in "one to many" relations (hasMany, hasAndBelongsToMany), it should be used in the opposite direction ("many to one"), i.e: - -To fetch all Users assigned to a Project: - - $this->Project->find('all', array('link' => 'User', 'conditions' => 'project_id = 1')); -This won't produce the desired result as only a single user will be returned. - - $this->User->find('all', array('link' => 'Project', 'conditions' => 'project_id = 1')); -This will fetch all users related to the specified project in one query. - -## Authors ## -- Originally authored by: Rafael Bandeira (rafaelbandeira3 (at) gmail (dot) com), http://rafaelbandeira3.wordpress.com -- Maintained by: Arjen Verstoep (terr (at) terr (dot) nl), https://github.com/Terr -- giulianob, https://github.com/giulianob -- Chad Jablonski, https://github.com/cjab -- Nathan Porter, https://github.com/n8man - -## License ## - -Licensed under The MIT License -Redistributions of files must retain the above copyright notice. - -[Download]: https://github.com/Terr/linkable/downloads diff --git a/Test/Case/Model/Behavior/LinkableBehaviorTest.php b/Test/Case/Model/Behavior/LinkableBehaviorTest.php index 0536f70..97c6fd6 100644 --- a/Test/Case/Model/Behavior/LinkableBehaviorTest.php +++ b/Test/Case/Model/Behavior/LinkableBehaviorTest.php @@ -465,7 +465,7 @@ public function _testAliasedBelongsToWithSameModelAsHasMany() class TestModel extends CakeTestModel { - public $useDbConfig = 'test'; + //public $useDbConfig = 'test'; public $recursive = 0; diff --git a/Test/Case/models/behaviors/linkable.test.php b/Test/Case/models/behaviors/linkable.test.php deleted file mode 100644 index e9fae43..0000000 --- a/Test/Case/models/behaviors/linkable.test.php +++ /dev/null @@ -1,568 +0,0 @@ - array( - 'className' => 'Tag', - 'foreignKey' => 'parent_id' - ) - ); -} - -class LegacyProduct extends TestModel -{ - public $primaryKey = 'product_id'; - - public $belongsTo = array( - 'Maker' => array( - 'className' => 'LegacyCompany', - 'foreignKey' => 'the_company_that_builds_it_id' - ), - 'Transporter' => array( - 'className' => 'LegacyCompany', - 'foreignKey' => 'the_company_that_delivers_it_id' - ) - ); -} - -class LegacyCompany extends TestModel -{ - public $primaryKey = 'company_id'; - - public $hasMany = array( - 'ProductsMade' => array( - 'className' => 'LegacyProduct', - 'foreignKey' => 'the_company_that_builds_it_id' - ) - ); -} - -class Shipment extends TestModel -{ - public $belongsTo = array( - 'OrderItem' - ); -} - -class OrderItem extends TestModel -{ - public $hasMany = array( - 'Shipment' - ); - - public $belongsTo = array( - 'ActiveShipment' => array( - 'className' => 'Shipment', - 'foreignKey' => 'active_shipment_id', - ), - ); -} - -class LinkableTestCase extends CakeTestCase -{ - public $fixtures = array( - 'plugin.linkable.user', - 'plugin.linkable.profile', - 'plugin.linkable.generic', - 'plugin.linkable.comment', - 'plugin.linkable.post', - 'plugin.linkable.posts_tag', - 'plugin.linkable.tag', - 'plugin.linkable.user', - 'plugin.linkable.legacy_product', - 'plugin.linkable.legacy_company', - 'plugin.linkable.shipment', - 'plugin.linkable.order_item', - ); - - public $Post; - - public function startTest() - { - $this->User =& ClassRegistry::init('User'); - } - - public function testBelongsTo() - { - $arrayExpected = array( - 'User' => array('id' => 1, 'username' => 'CakePHP'), - 'Profile' => array ('id' => 1, 'user_id' => 1, 'biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.') - ); - - $arrayResult = $this->User->find('first', array( - 'contain' => array( - 'Profile' - ) - )); - $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Containable: %s'); - $this->assertEqual($arrayResult, $arrayExpected, 'belongsTo association via Containable: %s'); - - // Same association, but this time with Linkable - $arrayResult = $this->User->find('first', array( - 'fields' => array( - 'id', - 'username' - ), - 'contain' => false, - 'link' => array( - 'Profile' => array( - 'fields' => array( - 'id', - 'user_id', - 'biography' - ) - ) - ) - )); - - $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Linkable: %s'); - $this->assertTrue(!empty($arrayResult['Profile']), 'belongsTo association via Linkable: %s'); - $this->assertEqual($arrayResult, $arrayExpected, 'belongsTo association via Linkable: %s'); - - // Linkable association, no field lists - $arrayResult = $this->User->find('first', array( - 'contain' => false, - 'link' => array( - 'Profile' - ) - )); - - $this->assertTrue(isset($arrayResult['Profile']), 'belongsTo association via Linkable (automatic fields): %s'); - $this->assertEqual($arrayResult, $arrayExpected, 'belongsTo association via Linkable (automatic fields): %s'); - - // On-the-fly association via Linkable - $arrayExpected = array( - 'User' => array('id' => 1, 'username' => 'CakePHP'), - 'Generic' => array('id' => 1, 'text' => '') - ); - - $arrayResult = $this->User->find('first', array( - 'contain' => false, - 'link' => array( - 'Generic' => array( - 'class' => 'Generic', - 'conditions' => 'User.id = Generic.id', - 'fields' => array( - 'id', - 'text' - ) - ) - ) - )); - - $this->assertTrue(isset($arrayResult['Generic']), 'On-the-fly belongsTo association via Linkable: %s'); - $this->assertEqual($arrayResult, $arrayExpected, 'On-the-fly belongsTo association via Linkable: %s'); - - // On-the-fly association via Linkable, with order on the associations' row - $arrayExpected = array( - 'User' => array('id' => 4, 'username' => 'CodeIgniter'), - 'Generic' => array('id' => 4, 'text' => '') - ); - - $arrayResult = $this->User->find('first', array( - 'contain' => false, - 'link' => array( - 'Generic' => array( - 'class' => 'Generic', - 'conditions' => 'User.id = Generic.id', - 'fields' => array( - 'id', - 'text' - ) - ) - ), - 'order' => 'Generic.id DESC' - )); - - $this->assertEqual($arrayResult, $arrayExpected, 'On-the-fly belongsTo association via Linkable, with order: %s'); - } - - public function testHasMany() - { - // hasMany association via Containable. Should still work when Linkable is loaded - $arrayExpected = array( - 'User' => array('id' => 1, 'username' => 'CakePHP'), - 'Comment' => array( - 0 => array( - 'id' => 1, - 'user_id' => 1, - 'body' => 'Text' - ), - 1 => array( - 'id' => 2, - 'user_id' => 1, - 'body' => 'Text' - ), - ) - ); - - $arrayResult = $this->User->find('first', array( - 'contain' => array( - 'Comment' - ), - 'order' => 'User.id ASC' - )); - $this->assertTrue(isset($arrayResult['Comment']), 'hasMany association via Containable: %s'); - $this->assertEqual($arrayResult, $arrayExpected, 'hasMany association via Containable: %s'); - - // Same association, but this time with Linkable - $arrayExpected = array( - 'User' => array('id' => 1, 'username' => 'CakePHP'), - 'Comment' => array( - 'id' => 1, - 'user_id' => 1, - 'body' => 'Text' - ) - ); - - $arrayResult = $this->User->find('first', array( - 'fields' => array( - 'id', - 'username' - ), - 'contain' => false, - 'link' => array( - 'Comment' => array( - 'fields' => array( - 'id', - 'user_id', - 'body' - ) - ) - ), - 'order' => 'User.id ASC', - 'group' => 'User.id' - )); - - $this->assertEqual($arrayResult, $arrayExpected, 'hasMany association via Linkable: %s'); - } - - public function testComplexAssociations() - { - $this->Post =& ClassRegistry::init('Post'); - - $arrayExpected = array( - 'Post' => array('id' => 1, 'title' => 'Post 1', 'user_id' => 1), - 'Tag' => array('name' => 'General'), - 'Profile' => array('biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.'), - 'MainTag' => array('name' => 'General'), - 'Generic' => array('id' => 1,'text' => ''), - 'User' => array('id' => 1, 'username' => 'CakePHP') - ); - - $arrayResult = $this->Post->find('first', array( - 'conditions' => array( - 'MainTag.id' => 1 - ), - 'link' => array( - 'User' => array( - 'conditions' => 'Post.user_id = User.id', - 'Profile' => array( - 'fields' => array( - 'biography' - ), - 'Generic' => array( - 'class' => 'Generic', - 'conditions' => 'User.id = Generic.id' - ) - ) - ), - 'Tag' => array( - 'table' => 'tags', - 'fields' => array( - 'name' - ) - ), - 'MainTag' => array( - 'class' => 'Tag', - 'conditions' => 'PostsTag.post_id = Post.id', - 'fields' => array( - 'MainTag.name' // @fixme Wants to use class name (Tag) instead of alias (MainTag) - ) - ) - ) - )); - - $this->assertEqual($arrayExpected, $arrayResult, 'Complex find: %s'); - - // Linkable and Containable combined - $arrayExpected = array( - 'Post' => array('id' => 1, 'title' => 'Post 1', 'user_id' => 1), - 'Tag' => array( - array('id' => 1, 'name' => 'General', 'parent_id' => null, 'PostsTag' => array('id' => 1, 'post_id' => 1, 'tag_id' => 1, 'main' => 0)), - array('id' => 2, 'name' => 'Test I', 'parent_id' => 1, 'PostsTag' => array('id' => 2, 'post_id' => 1, 'tag_id' => 2, 'main' => 1)) - ), - 'User' => array('id' => 1, 'username' => 'CakePHP') - ); - - $arrayResult = $this->Post->find('first', array( - 'contain' => array( - 'Tag' - ), - 'link' => array( - 'User' => array( - 'conditions' => 'User.id = Post.user_id' - ) - ) - )); - - $this->assertEqual($arrayResult, $arrayExpected, 'Linkable and Containable combined: %s'); - } - - public function testPagination() - { - $objController = new Controller(); - $objController->uses = array('User'); - $objController->constructClasses(); - $objController->params['url']['url'] = '/'; - - $objController->paginate = array( - 'fields' => array( - 'username' - ), - 'contain' => false, - 'link' => array( - 'Profile' => array( - 'fields' => array( - 'biography' - ) - ) - ), - 'limit' => 2 - ); - - $arrayResult = $objController->paginate('User'); - - $this->assertEqual($objController->params['paging']['User']['count'], 4, 'Paging: total records count: %s'); - - // Pagination with order on a row from table joined with Linkable - $objController->paginate = array( - 'fields' => array( - 'id' - ), - 'contain' => false, - 'link' => array( - 'Profile' => array( - 'fields' => array( - 'user_id' - ) - ) - ), - 'limit' => 2, - 'order' => 'Profile.user_id DESC' - ); - - $arrayResult = $objController->paginate('User'); - - $arrayExpected = array( - 0 => array( - 'User' => array( - 'id' => 4 - ), - 'Profile' => array ('user_id' => 4) - ), - 1 => array( - 'User' => array( - 'id' => 3 - ), - 'Profile' => array ('user_id' => 3) - ) - ); - - $this->assertEqual($arrayResult, $arrayExpected, 'Paging with order on join table row: %s'); - - // Pagination without specifying any fields - $objController->paginate = array( - 'contain' => false, - 'link' => array( - 'Profile' - ), - 'limit' => 2, - 'order' => 'Profile.user_id DESC' - ); - - $arrayResult = $objController->paginate('User'); - $this->assertEqual($objController->params['paging']['User']['count'], 4, 'Paging without any field lists: total records count: %s'); - } - - /** - * Series of tests that assert if Linkable can adapt to assocations that - * have aliases different from their standard model names - */ - public function testNonstandardAssociationNames() - { - $this->Tag =& ClassRegistry::init('Tag'); - - $arrayExpected = array( - 'Tag' => array( - 'name' => 'Test I' - ), - 'Parent' => array( - 'name' => 'General' - ) - ); - - $arrayResult = $this->Tag->find('first', array( - 'fields' => array( - 'name' - ), - 'conditions' => array( - 'Tag.id' => 2 - ), - 'link' => array( - 'Parent' => array( - 'fields' => array( - 'name' - ) - ) - ) - )); - - $this->assertEqual($arrayExpected, $arrayResult, 'Association with non-standard name: %s'); - - - $this->LegacyProduct =& ClassRegistry::init('LegacyProduct'); - - $arrayExpected = array( - 'LegacyProduct' => array( - 'name' => 'Velocipede' - ), - 'Maker' => array( - 'company_name' => 'Vintage Stuff Manufactory' - ), - 'Transporter' => array( - 'company_name' => 'Joe & Co Crate Shipping Company' - ) - ); - - $arrayResult = $this->LegacyProduct->find('first', array( - 'fields' => array( - 'name' - ), - 'conditions' => array( - 'LegacyProduct.product_id' => 1 - ), - 'link' => array( - 'Maker' => array( - 'fields' => array( - 'company_name' - ) - ), - 'Transporter' => array( - 'fields' => array( - 'company_name' - ) - ) - ) - )); - - $this->assertEqual($arrayExpected, $arrayResult, 'belongsTo associations with custom foreignKey: %s'); - - $arrayExpected = array( - 'ProductsMade' => array( - 'name' => 'Velocipede' - ), - 'Maker' => array( - 'company_name' => 'Vintage Stuff Manufactory' - ) - ); - - $arrayResult = $this->LegacyProduct->Maker->find('first', array( - 'fields' => array( - 'company_name' - ), - 'conditions' => array( - 'Maker.company_id' => 1 - ), - 'link' => array( - 'ProductsMade' => array( - 'fields' => array( - 'name' - ) - ) - ) - )); - - $this->assertEqual($arrayExpected, $arrayResult, 'hasMany association with custom foreignKey: %s'); - } - - public function testAliasedBelongsToWithSameModelAsHasMany() - { - $this->OrderItem =& ClassRegistry::init('OrderItem'); - - $arrayExpected = array( - 0 => array( - 'OrderItem' => array( - 'id' => 50, - 'active_shipment_id' => 320 - ), - 'ActiveShipment' => array( - 'id' => 320, - 'ship_date' => '2011-01-07', - 'order_item_id' => 50 - ) - ) - ); - - $arrayResult = $this->OrderItem->find('all', array( - 'recursive' => -1, - 'conditions' => array( - 'ActiveShipment.ship_date' => date('2011-01-07'), - ), - 'link' => array('ActiveShipment'), - )); - - $this->assertEqual($arrayExpected, $arrayResult, 'belongsTo association with alias (requested), with hasMany to the same model without alias: %s'); - } -} From 08809f7c6b5f0cf62073d2633f0a8c217462f698 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 15 Dec 2011 15:39:53 +0000 Subject: [PATCH 34/37] make test cases run - test model was using test_suite connection now uses default conection for tests --- Model/Behavior/LinkableBehavior.php | 2 +- Test/Case/Model/Behavior/LinkableBehaviorTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Model/Behavior/LinkableBehavior.php b/Model/Behavior/LinkableBehavior.php index dc39b83..8be1ba5 100644 --- a/Model/Behavior/LinkableBehavior.php +++ b/Model/Behavior/LinkableBehavior.php @@ -23,7 +23,7 @@ * * @version 1.2: * @modified Mark Scherer - * - works with cakephp2.0 (not fully confirmed - test cases wont work) + * - works with cakephp2.0 (89.84 test coverage) */ class LinkableBehavior extends ModelBehavior { diff --git a/Test/Case/Model/Behavior/LinkableBehaviorTest.php b/Test/Case/Model/Behavior/LinkableBehaviorTest.php index 97c6fd6..3007340 100644 --- a/Test/Case/Model/Behavior/LinkableBehaviorTest.php +++ b/Test/Case/Model/Behavior/LinkableBehaviorTest.php @@ -465,7 +465,6 @@ public function _testAliasedBelongsToWithSameModelAsHasMany() class TestModel extends CakeTestModel { - //public $useDbConfig = 'test'; public $recursive = 0; From b1b9b299a1e7700b5680db22bc90353b57e91887 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 15 Dec 2011 15:44:01 +0000 Subject: [PATCH 35/37] remove old format fixtures --- Test/Fixture/comment_fixture.php | 20 -------------------- Test/Fixture/generic_fixture.php | 18 ------------------ Test/Fixture/legacy_company_fixture.php | 17 ----------------- Test/Fixture/legacy_product_fixture.php | 18 ------------------ Test/Fixture/order_item_fixture.php | 15 --------------- Test/Fixture/post_fixture.php | 17 ----------------- Test/Fixture/posts_tag_fixture.php | 20 -------------------- Test/Fixture/profile_fixture.php | 19 ------------------- Test/Fixture/shipment_fixture.php | 18 ------------------ Test/Fixture/tag_fixture.php | 19 ------------------- Test/Fixture/user_fixture.php | 18 ------------------ 11 files changed, 199 deletions(-) delete mode 100644 Test/Fixture/comment_fixture.php delete mode 100644 Test/Fixture/generic_fixture.php delete mode 100644 Test/Fixture/legacy_company_fixture.php delete mode 100644 Test/Fixture/legacy_product_fixture.php delete mode 100644 Test/Fixture/order_item_fixture.php delete mode 100644 Test/Fixture/post_fixture.php delete mode 100644 Test/Fixture/posts_tag_fixture.php delete mode 100644 Test/Fixture/profile_fixture.php delete mode 100644 Test/Fixture/shipment_fixture.php delete mode 100644 Test/Fixture/tag_fixture.php delete mode 100644 Test/Fixture/user_fixture.php diff --git a/Test/Fixture/comment_fixture.php b/Test/Fixture/comment_fixture.php deleted file mode 100644 index ea17cd9..0000000 --- a/Test/Fixture/comment_fixture.php +++ /dev/null @@ -1,20 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'user_id' => array('type' => 'integer'), - 'body' => array('type' => 'string', 'length' => 255, 'null' => false) - ); - - var $records = array( - array('id' => 1, 'user_id' => 1, 'body' => 'Text'), - array('id' => 2, 'user_id' => 1, 'body' => 'Text'), - array('id' => 3, 'user_id' => 2, 'body' => 'Text'), - array('id' => 4, 'user_id' => 3, 'body' => 'Text'), - array('id' => 5, 'user_id' => 4, 'body' => 'Text') - ); -} diff --git a/Test/Fixture/generic_fixture.php b/Test/Fixture/generic_fixture.php deleted file mode 100644 index 3d062a3..0000000 --- a/Test/Fixture/generic_fixture.php +++ /dev/null @@ -1,18 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'text' => array('type' => 'string', 'length' => 255, 'null' => false) - ); - - var $records = array( - array ('id' => 1, 'text' => ''), - array ('id' => 2, 'text' => ''), - array ('id' => 3, 'text' => ''), - array ('id' => 4, 'text' => '') - ); -} diff --git a/Test/Fixture/legacy_company_fixture.php b/Test/Fixture/legacy_company_fixture.php deleted file mode 100644 index d5cd598..0000000 --- a/Test/Fixture/legacy_company_fixture.php +++ /dev/null @@ -1,17 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'company_name' => array('type' => 'string', 'length' => 255, 'null' => false), - ); - - var $records = array( - array('company_id' => 1, 'company_name' => 'Vintage Stuff Manufactory'), - array('company_id' => 2, 'company_name' => 'Modern Steam Cars Inc.'), - array('company_id' => 3, 'company_name' => 'Joe & Co Crate Shipping Company') - ); -} diff --git a/Test/Fixture/legacy_product_fixture.php b/Test/Fixture/legacy_product_fixture.php deleted file mode 100644 index 2207878..0000000 --- a/Test/Fixture/legacy_product_fixture.php +++ /dev/null @@ -1,18 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'name' => array('type' => 'string', 'length' => 255, 'null' => false), - 'the_company_that_builds_it_id' => array('type' => 'integer'), - 'the_company_that_delivers_it_id' => array('type' => 'integer') - ); - - var $records = array( - array('product_id' => 1, 'name' => 'Velocipede', 'the_company_that_builds_it_id' => 1, 'the_company_that_delivers_it_id' => 3), - array('product_id' => 2, 'name' => 'Oruktor Amphibolos', 'the_company_that_builds_it_id' => 2, 'the_company_that_delivers_it_id' => 2), - ); -} diff --git a/Test/Fixture/order_item_fixture.php b/Test/Fixture/order_item_fixture.php deleted file mode 100644 index 2fcf97f..0000000 --- a/Test/Fixture/order_item_fixture.php +++ /dev/null @@ -1,15 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'active_shipment_id' => array('type' => 'integer'), - ); - - var $records = array( - array ('id' => 50, 'active_shipment_id' => 320) - ); -} diff --git a/Test/Fixture/post_fixture.php b/Test/Fixture/post_fixture.php deleted file mode 100644 index 19c132d..0000000 --- a/Test/Fixture/post_fixture.php +++ /dev/null @@ -1,17 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'title' => array('type' => 'string', 'length' => 255, 'null' => false), - 'user_id' => array('type' => 'integer'), - ); - - var $records = array( - array ('id' => 1, 'title' => 'Post 1', 'user_id' => 1), - array ('id' => 2, 'title' => 'Post 2', 'user_id' => 2) - ); -} diff --git a/Test/Fixture/posts_tag_fixture.php b/Test/Fixture/posts_tag_fixture.php deleted file mode 100644 index c041019..0000000 --- a/Test/Fixture/posts_tag_fixture.php +++ /dev/null @@ -1,20 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'post_id' => array('type' => 'integer'), - 'tag_id' => array('type' => 'integer'), - 'main' => array('type' => 'integer') - ); - - var $records = array( - array ('id' => 1, 'post_id' => 1, 'tag_id' => 1, 'main' => 0), - array ('id' => 2, 'post_id' => 1, 'tag_id' => 2, 'main' => 1), - array ('id' => 3, 'post_id' => 2, 'tag_id' => 3, 'main' => 0), - array ('id' => 4, 'post_id' => 2, 'tag_id' => 4, 'main' => 0), - ); -} \ No newline at end of file diff --git a/Test/Fixture/profile_fixture.php b/Test/Fixture/profile_fixture.php deleted file mode 100644 index 8e50e98..0000000 --- a/Test/Fixture/profile_fixture.php +++ /dev/null @@ -1,19 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'user_id' => array('type' => 'integer'), - 'biography' => array('type' => 'string', 'length' => 255, 'null' => false) - ); - - var $records = array( - array ('id' => 1, 'user_id' => 1, 'biography' => 'CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.'), - array ('id' => 2, 'user_id' => 2, 'biography' => ''), - array ('id' => 3, 'user_id' => 3, 'biography' => ''), - array ('id' => 4, 'user_id' => 4, 'biography' => '') - ); -} diff --git a/Test/Fixture/shipment_fixture.php b/Test/Fixture/shipment_fixture.php deleted file mode 100644 index ef2d9ef..0000000 --- a/Test/Fixture/shipment_fixture.php +++ /dev/null @@ -1,18 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'ship_date' => array('type' => 'date'), - 'order_item_id' => array('type' => 'integer') - ); - - var $records = array( - array ('id' => 320, 'ship_date' => '2011-01-07', 'order_item_id' => 50), - array ('id' => 319, 'ship_date' => '2011-01-07', 'order_item_id' => 50), - array ('id' => 310, 'ship_date' => '2011-01-07', 'order_item_id' => 50) - ); -} diff --git a/Test/Fixture/tag_fixture.php b/Test/Fixture/tag_fixture.php deleted file mode 100644 index eca4f6d..0000000 --- a/Test/Fixture/tag_fixture.php +++ /dev/null @@ -1,19 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'name' => array('type' => 'string', 'length' => 255, 'null' => false), - 'parent_id' => array('type' => 'integer') - ); - - var $records = array( - array ('id' => 1, 'name' => 'General', 'parent_id' => null), - array ('id' => 2, 'name' => 'Test I', 'parent_id' => 1), - array ('id' => 3, 'name' => 'Test II', 'parent_id' => null), - array ('id' => 4, 'name' => 'Test III', 'parent_id' => null) - ); -} diff --git a/Test/Fixture/user_fixture.php b/Test/Fixture/user_fixture.php deleted file mode 100644 index 33eb994..0000000 --- a/Test/Fixture/user_fixture.php +++ /dev/null @@ -1,18 +0,0 @@ - array('type' => 'integer', 'key' => 'primary'), - 'username' => array('type' => 'string', 'length' => 255, 'null' => false) - ); - - var $records = array( - array('id' => 1, 'username' => 'CakePHP'), - array('id' => 2, 'username' => 'Zend'), - array('id' => 3, 'username' => 'Symfony'), - array('id' => 4, 'username' => 'CodeIgniter') - ); -} From fbfe43ab26c7efab42493d25a1333370406dd576 Mon Sep 17 00:00:00 2001 From: Dean Sofer Date: Mon, 27 Feb 2012 04:28:57 -0800 Subject: [PATCH 36/37] Fixed the @fixme. Aliased models now have the Alias in fields --- Model/Behavior/LinkableBehavior.php | 24 +++++++++---------- .../Model/Behavior/LinkableBehaviorTest.php | 2 +- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Model/Behavior/LinkableBehavior.php b/Model/Behavior/LinkableBehavior.php index 8be1ba5..d16c4d3 100644 --- a/Model/Behavior/LinkableBehavior.php +++ b/Model/Behavior/LinkableBehavior.php @@ -156,12 +156,7 @@ public function beforeFind(Model $Model, $query) { $options['fields'] = $db->fields($_Model); } else { $options['fields'] = $db->fields($_Model, null, $options['fields']); - } - - if (is_array($query['fields'])) - $query['fields'] = array_merge($query['fields'], $options['fields']); - else - $query['fields'] = array_merge($db->fields($Model), $options['fields']); + } } else if (!isset($options['fields']) || (isset($options['fields']) && !is_array($options['fields']))) { @@ -170,13 +165,16 @@ public function beforeFind(Model $Model, $query) { } else { $options['fields'] = $db->fields($_Model); } - - 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']); - } + } + + 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']); } } diff --git a/Test/Case/Model/Behavior/LinkableBehaviorTest.php b/Test/Case/Model/Behavior/LinkableBehaviorTest.php index 3007340..220951d 100644 --- a/Test/Case/Model/Behavior/LinkableBehaviorTest.php +++ b/Test/Case/Model/Behavior/LinkableBehaviorTest.php @@ -225,7 +225,7 @@ public function testComplexAssociations() 'class' => 'Tag', 'conditions' => array('exactly' => 'PostsTag.post_id = Post.id'), 'fields' => array( - 'MainTag.name' // @fixme Wants to use class name (Tag) instead of alias (MainTag) + 'MainTag.name' ) ) ) From 2d6f635c6037fa3e39f8b211a79248a65718edd0 Mon Sep 17 00:00:00 2001 From: Dean Sofer Date: Sat, 3 Mar 2012 03:54:24 -0800 Subject: [PATCH 37/37] Fixed aliasing of join conditions when passing a 'class' param --- Model/Behavior/LinkableBehavior.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Model/Behavior/LinkableBehavior.php b/Model/Behavior/LinkableBehavior.php index d16c4d3..823be4e 100644 --- a/Model/Behavior/LinkableBehavior.php +++ b/Model/Behavior/LinkableBehavior.php @@ -106,6 +106,7 @@ public function beforeFind(Model $Model, $query) { } 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') { @@ -134,14 +135,16 @@ public function beforeFind(Model $Model, $query) { 'type' => 'LEFT' ); $modelKey = $_Model->escapeField(); - $options['conditions'][] = "{$modelLink} = {$modelKey}"; + $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; }