diff --git a/Model/Behavior/LinkableBehavior.php b/Model/Behavior/LinkableBehavior.php new file mode 100644 index 0000000..823be4e --- /dev/null +++ b/Model/Behavior/LinkableBehavior.php @@ -0,0 +1,201 @@ +Post->find('first', array('link' => array('User' => array('conditions' => array('exactly' => 'User.last_post_id = Post.id'))))) ) + * This is usually required when doing on-the-fly joins since Linkable generally assumes a belongsTo relationship when no specific relationship is found and may produce invalid foreign key conditions. + * -Linkable will no longer break queries that use SQL COUNTs + * + * @version 1.2: + * @modified Mark Scherer + * - works with cakephp2.0 (89.84 test coverage) + */ +class LinkableBehavior extends ModelBehavior { + + protected $_key = 'link'; + + protected $_options = array( + 'type' => true, 'table' => true, 'alias' => true, + 'conditions' => true, 'fields' => true, 'reference' => true, + 'class' => true, 'defaults' => true + ); + + protected $_defaults = array('type' => 'LEFT'); + + public function beforeFind(Model $Model, $query) { + if (isset($query[$this->_key])) { + $optionsDefaults = $this->_defaults + array('reference' => $Model->alias, $this->_key => array()); + $optionsKeys = $this->_options + array($this->_key => true); + + // If containable is being used, then let it set the recursive! + if (empty($query['contain'])) { + $query = am(array('joins' => array()), $query, array('recursive' => -1)); + } else { + $query = am(array('joins' => array()), $query); + } + $iterators[] = $query[$this->_key]; + $cont = 0; + do { + $iterator = $iterators[$cont]; + $defaults = $optionsDefaults; + if (isset($iterator['defaults'])) { + $defaults = array_merge($defaults, $iterator['defaults']); + unset($iterator['defaults']); + } + $iterations = Set::normalize($iterator); + foreach ($iterations as $alias => $options) { + if (is_null($options)) { + $options = array(); + } + $options = am($defaults, compact('alias'), $options); + if (empty($options['alias'])) { + throw new InvalidArgumentException(sprintf('%s::%s must receive aliased links', get_class($this), __FUNCTION__)); + } + if (empty($options['table']) && empty($options['class'])) { + $options['class'] = $options['alias']; + } elseif (!empty($options['table']) && empty($options['class'])) { + $options['class'] = Inflector::classify($options['table']); + } + + // the incoming model to be linked in query + $_Model = ClassRegistry::init($options['class']); + // the already in query model that links to $_Model + $Reference = ClassRegistry::init($options['reference']); + $db = $_Model->getDataSource(); + $associations = $_Model->getAssociated(); + if (isset($Reference->belongsTo[$_Model->alias])) { + $type = 'hasOne'; + $association = $Reference->belongsTo[$_Model->alias]; + } else if (isset($associations[$Reference->alias])) { + $type = $associations[$Reference->alias]; + $association = $_Model->{$type}[$Reference->alias]; + } else { + $_Model->bindModel(array('belongsTo' => array($Reference->alias))); + $type = 'belongsTo'; + $association = $_Model->{$type}[$Reference->alias]; + $_Model->unbindModel(array('belongsTo' => array($Reference->alias))); + } + + if (!isset($options['conditions'])) { + $options['conditions'] = array(); + } else if (!is_array($options['conditions'])) { + // Support for string conditions + $options['conditions'] = array($options['conditions']); + } + + if (isset($options['conditions']['exactly'])) { + if (is_array($options['conditions']['exactly'])) + $options['conditions'] = reset($options['conditions']['exactly']); + else + $options['conditions'] = array($options['conditions']['exactly']); + } else { + if ($type === 'belongsTo') { + $modelKey = $_Model->escapeField($association['foreignKey']); + $modelKey = str_replace($_Model->alias, $options['alias'], $modelKey); + $referenceKey = $Reference->escapeField($Reference->primaryKey); + $options['conditions'][] = "{$referenceKey} = {$modelKey}"; + } elseif ($type === 'hasAndBelongsToMany') { + if (isset($association['with'])) { + $Link = $_Model->{$association['with']}; + if (isset($Link->belongsTo[$_Model->alias])) { + $modelLink = $Link->escapeField($Link->belongsTo[$_Model->alias]['foreignKey']); + } + if (isset($Link->belongsTo[$Reference->alias])) { + $referenceLink = $Link->escapeField($Link->belongsTo[$Reference->alias]['foreignKey']); + } + } else { + $Link = $_Model->{Inflector::classify($association['joinTable'])}; + } + if (empty($modelLink)) { + $modelLink = $Link->escapeField(Inflector::underscore($_Model->alias) . '_id'); + } + if (empty($referenceLink)) { + $referenceLink = $Link->escapeField(Inflector::underscore($Reference->alias) . '_id'); + } + $referenceKey = $Reference->escapeField(); + $query['joins'][] = array( + 'alias' => $Link->alias, + 'table' => $Link->table, //$Link->getDataSource()->fullTableName($Link), + 'conditions' => "{$referenceLink} = {$referenceKey}", + 'type' => 'LEFT' + ); + $modelKey = $_Model->escapeField(); + $modelKey = str_replace($_Model->alias, $options['alias'], $modelKey); + $options['conditions'][] = "{$modelLink} = {$modelKey}"; + } else { + $referenceKey = $Reference->escapeField($association['foreignKey']); + $modelKey = $_Model->escapeField($_Model->primaryKey); + $modelKey = str_replace($_Model->alias, $options['alias'], $modelKey); + $options['conditions'][] = "{$modelKey} = {$referenceKey}"; + } + } + + if (empty($options['table'])) { + $options['table'] = $_Model->table; + } + + // Decide whether we should mess with the fields or not + // If this query is a COUNT query then we just leave it alone + if (!isset($query['fields']) || is_array($query['fields']) || strpos($query['fields'], 'COUNT(*)') === FALSE) { + if (!empty($options['fields'])) { + if ($options['fields'] === true && !empty($association['fields'])) { + $options['fields'] = $db->fields($_Model, null, $association['fields']); + } elseif ($options['fields'] === true) { + $options['fields'] = $db->fields($_Model); + } else { + $options['fields'] = $db->fields($_Model, null, $options['fields']); + } + } + else if (!isset($options['fields']) || (isset($options['fields']) && !is_array($options['fields']))) + { + if (!empty($association['fields'])) { + $options['fields'] = $db->fields($_Model, null, $association['fields']); + } else { + $options['fields'] = $db->fields($_Model); + } + } + + if (!empty($options['class']) && $options['class'] !== $alias) { + $options['fields'] = str_replace($options['class'], $alias, $options['fields']); + } + if (is_array($query['fields'])) { + $query['fields'] = array_merge($query['fields'], $options['fields']); + } else { + // If user didn't specify any fields then select all fields by default (just as find would) + $query['fields'] = array_merge($db->fields($Model), $options['fields']); + } + } + + $options[$this->_key] = am($options[$this->_key], array_diff_key($options, $optionsKeys)); + $options = array_intersect_key($options, $optionsKeys); + if (!empty($options[$this->_key])) { + $iterators[] = $options[$this->_key] + array('defaults' => array_merge($defaults, array('reference' => $options['class']))); + } + + $query['joins'][] = array_intersect_key($options, array('type' => true, 'alias' => true, 'table' => true, 'conditions' => true)); + } + $cont++; + $notDone = isset($iterators[$cont]); + } while ($notDone); + } + + unset($query['link']); + + return $query; + } +} \ No newline at end of file diff --git a/README b/README deleted file mode 100644 index 38fc648..0000000 --- a/README +++ /dev/null @@ -1,26 +0,0 @@ -Linkable Plugin -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'. - -RafaelBandeira -http://rafaelbandeira3.wordpress.com - -Licensed under The MIT License -Redistributions of files must retain the above copyright notice. - -@version 1.0; \ 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/Test/Case/Model/Behavior/LinkableBehaviorTest.php b/Test/Case/Model/Behavior/LinkableBehaviorTest.php new file mode 100644 index 0000000..220951d --- /dev/null +++ b/Test/Case/Model/Behavior/LinkableBehaviorTest.php @@ -0,0 +1,563 @@ +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' + ) + ) + ) + )); + + $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 $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') + ); +} diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php deleted file mode 100644 index 5b57b8d..0000000 --- a/models/behaviors/linkable.php +++ /dev/null @@ -1,152 +0,0 @@ -find('all', array('link' => 'User', 'conditions' => 'project_id = 1')) - * - Won't produce the desired result as data came from users table will be lost. - * $User->find('all', array('link' => 'Project', 'conditions' => 'project_id = 1')) - * - Will fetch all users related to the specified project in one query - * - * - On data mining as a much lighter approach - can reduce 300+ query find operations - * in one single query with joins; "or your money back!" ;-) - * - * - Has the 'fields' param enabled to make it easy to replace Containable usage, - * only change the 'contain' param to 'link'. - * - * Linkable Behavior. Taking it easy in your DB. - * RafaelBandeira - * - * Licensed under The MIT License - * Redistributions of files must retain the above copyright notice. - * - * @version 1.0; - */ - -class LinkableBehavior extends ModelBehavior { - - protected $_key = 'link'; - - protected $_options = array( - 'type' => true, 'table' => true, 'alias' => true, - 'conditions' => true, 'fields' => true, 'reference' => true, - 'class' => true, 'defaults' => true - ); - - protected $_defaults = array('type' => 'LEFT'); - - public function beforeFind(&$Model, $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)); - $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 { - $_Model->bind($Reference->alias); - $type = 'belongsTo'; - $association = $_Model->{$type}[$Reference->alias]; - $_Model->unbindModel(array('belongsTo' => array($Reference->alias))); - } - - if (empty($options['conditions'])) { - if ($type === 'belongsTo') { - $modelKey = $_Model->escapeField($association['foreignKey']); - $referenceKey = $Reference->escapeField($Reference->primaryKey); - $options['conditions'] = "{$referenceKey} = {$modelKey}"; - } elseif ($type === 'hasAndBelongsToMany') { - if (isset($association['with'])) { - $Link =& $_Model->{$association['with']}; - if (isset($Link->belongsTo[$_Model->alias])) { - $modelLink = $Link->escapeField($Link->belongsTo[$_Model->alias]['foreignKey']); - } - if (isset($Link->belongsTo[$Reference->alias])) { - $referenceLink = $Link->escapeField($Link->belongsTo[$Reference->alias]['foreignKey']); - } - } else { - $Link =& $_Model->{Inflector::classify($association['joinTable'])}; - } - if (empty($modelLink)) { - $modelLink = $Link->escapeField(Inflector::underscore($_Model->alias) . '_id'); - } - if (empty($referenceLink)) { - $referenceLink = $Link->escapeField(Inflector::underscore($Reference->alias) . '_id'); - } - $referenceKey = $Reference->escapeField(); - $query['joins'][] = array( - 'alias' => $Link->alias, - 'table' => $Link->getDataSource()->fullTableName($Link), - '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}"; - } - } - 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']); - } elseif ($options['fields'] === true) { - $options['fields'] = $db->fields($_Model); - } else { - $options['fields'] = $db->fields($_Model, null, $options['fields']); - } - $query['fields'] = array_merge($query['fields'], $options['fields']); - } - - $options[$this->_key] = am($options[$this->_key], array_diff_key($options, $optionsKeys)); - $options = array_intersect_key($options, $optionsKeys); - if (!empty($options[$this->_key])) { - $iterators[] = $options[$this->_key] + array('defaults' => array_merge($defaults, array('reference' => $options['class']))); - } - $query['joins'][] = array_intersect_key($options, array('type' => true, 'alias' => true, 'table' => true, 'conditions' => true)); - } - ++$cont; - $notDone = isset($iterators[$cont]); - } while ($notDone); - } - return $query; - } -} \ No newline at end of file