Skip to content

Commit

Permalink
Adding a simplistic implementation of contains, only for belongsTo and
Browse files Browse the repository at this point in the history
hasOne associations
  • Loading branch information
lorenzo committed May 4, 2013
1 parent 013b246 commit 1f7f87d
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 2 deletions.
113 changes: 113 additions & 0 deletions lib/Cake/ORM/Query.php
Expand Up @@ -3,11 +3,14 @@
namespace Cake\ORM;

use Cake\Database\Query as DatabaseQuery;
use Cake\Utility\Inflector;

class Query extends DatabaseQuery {

protected $_table;

protected $_containments;

public function repository(Table $table = null) {
if ($table === null) {
return $this->_table;
Expand All @@ -17,6 +20,19 @@ public function repository(Table $table = null) {
return $this;
}

public function contain($associations = null) {
if ($this->_containments === null) {
$this->_containments = new \ArrayObject;
}
if ($associations === null) {
return $this->_containments;
}
foreach ($associations as $table => $options) {
$this->_containments[$table] = $options;
}
return $this;
}

public function execute() {
return new ResultSet($this, parent::execute());
}
Expand All @@ -43,10 +59,107 @@ protected function _transformQuery() {
return parent::_transformQuery();
}

$this->_addContainments();
$this->_aliasFields();
return parent::_transformQuery();
}

protected function _addContainments() {
if (empty($this->_containments)) {
return;
}

$contain = [];
foreach ($this->_containments as $table => $options) {
$contain[$table] = $this->_normalizeContain(
$this->repository(),
$table,
$options
);
}

$firstLevelJoins = $this->_resolveFirstLevel($contain);
foreach ($firstLevelJoins as $table => $options) {
$this->_addJoin($table, $options);
}
}

protected function _normalizeContain(Table $parent, $alias, $options) {
$defaults = [
'table' => 1,
'associationType' => 1,
'associations' => 1,
'foreignKey' => 1,
'conditions' => 1,
'fields' => 1
];
$table = Table::build($alias);

if (is_string($options)) {
//TODO: finish extracting
$options = $table->associated($options);
}

$extra = array_diff_key($options, $defaults);
$config = array_diff_key($options, $extra) + [
'associations' => [],
'table' => $table->table(),
'type' => 'left'
];
$config = $this->_resolveForeignKeyConditions($table, $parent, $config);

foreach ($extra as $t => $assoc) {
$config['associations'][$t] = $this->_normalizeContain($table, $t, $assoc);
}
return $config;
}

protected function _resolveForeignKeyConditions(Table $table, Table $parent, array $config) {
if (!isset($config['foreignKey']) || $config['foreignKey'] !== false) {
$target = $config['associationType'] === 'belongsTo' ? $table : $parent;
$config['foreignKey'] = Inflector::underscore($target->alias()) . '_id';
}

if (!empty($config['foreignKey'])) {
if ($config['associationType'] === 'belongsTo') {
$config['conditions'][] = sprintf('%s.%s = %s.%s',
$table->alias(),
'id',
$parent->alias(),
$config['foreignKey']
);
}
if ($config['associationType'] === 'hasOne') {
$config['conditions'][] = sprintf('%s.%s = %s.%s',
$table->alias(),
$config['foreignKey'],
$parent->alias(),
'id'
);
}
}
return $config;
}

protected function _resolveFirstLevel($associations) {
$result = [];
foreach ($associations as $table => $options) {
foreach (['belongsTo', 'hasOne'] as $type) {
if ($options['associationType'] === $type) {
$result += [$table => array_diff_key($options, ['associations' => 1])];
$result += $this->_resolveFirstLevel($options['associations']);
}
}
}
return $result;
}

protected function _addJoin($alias, $options) {
$joinOptions = ['table' => 1, 'conditions' => 1, 'type' => 1];
$options = array_intersect_key($options, $joinOptions);
$this->join([$alias => $options]);
}

protected function _aliasFields() {
$select = $this->clause('select');
$schema = $this->repository()->schema();
Expand Down
16 changes: 15 additions & 1 deletion lib/Cake/ORM/Table.php
Expand Up @@ -16,6 +16,8 @@
*/
namespace Cake\ORM;

use Cake\Utility\Inflector;

class Table {

protected static $_instances = [];
Expand Down Expand Up @@ -58,13 +60,25 @@ public static function build($alias, array $options = []) {
}

$options = ['alias' => $alias] + $options;

if (empty($options['table'])) {
$options['table'] = Inflector::tableize($alias);
}

if (empty($options['className'])) {
$options['className'] = get_called_class();
}

return static::$_instances[$alias] = new $options['className']($options);
}

public static function instance($alias, self $object = null) {
if ($object === null) {
return isset(static::$_instances[$alias]) ? static::$_instances[$alias] : null;
}
return static::$_instances[$alias] = $object;
}

public static function map($alias = null, array $options = null) {
if ($alias === null) {
return static::$_tablesMap;
Expand All @@ -79,7 +93,7 @@ public static function map($alias = null, array $options = null) {
static::$_tablesMap[$alias] = $options;
}

public function clearRegistry() {
public static function clearRegistry() {
static::$_instances = [];
static::$_tablesMap = [];
}
Expand Down
125 changes: 125 additions & 0 deletions lib/Cake/Test/TestCase/ORM/QueryTest.php
@@ -0,0 +1,125 @@
<?php
/**
* PHP Version 5.4
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since CakePHP(tm) v 3.0.0
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
namespace Cake\Test\TestCase\ORM;

use Cake\Core\Configure;
use Cake\Database\Connection;
use Cake\ORM\Query;
use Cake\ORM\Table;

/**
* Tests Query class
*
*/
class QueryTest extends \Cake\TestSuite\TestCase {

public function setUp() {
$this->connection = new Connection(Configure::read('Datasource.test'));
}

/**
* Tests that fully defined belongsTo and hasOne relationships are joined correctly
*
* @return void
**/
public function testContainToJoins() {
$contains = ['client' => [
'associationType' => 'belongsTo',
'table' => 'clients',
'order' => [
'associationType' => 'hasOne',
'orderType' => ['associationType' => 'belongsTo'],
'stuff' => [
'associationType' => 'hasOne', 'table' => 'things',
'stuffType' => ['associationType' => 'belongsTo']
]
],
'company' => [
'associationType' => 'belongsTo',
'table' => 'organizations',
'foreignKey' => 'organization_id',
'category' => ['associationType' => 'belongsTo']
]
]];

$table = Table::build('foo', ['schema' => ['id' => ['type' => 'integer']]]);
$query = $this->getMock('\Cake\ORM\Query', ['join'], [$this->connection]);

$query->expects($this->at(0))->method('join')
->with(['client' => [
'table' => 'clients',
'type' => 'left',
'conditions' => ['client.id = foo.client_id']
]])
->will($this->returnValue($query));

$query->expects($this->at(1))->method('join')
->with(['order' => [
'table' => 'orders',
'type' => 'left',
'conditions' => ['order.client_id = client.id']
]])
->will($this->returnValue($query));

$query->expects($this->at(2))->method('join')
->with(['orderType' => [
'table' => 'order_types',
'type' => 'left',
'conditions' => ['orderType.id = order.order_type_id']
]])
->will($this->returnValue($query));

$query->expects($this->at(3))->method('join')
->with(['stuff' => [
'table' => 'things',
'type' => 'left',
'conditions' => ['stuff.order_id = order.id']
]])
->will($this->returnValue($query));


$query->expects($this->at(4))->method('join')
->with(['stuffType' => [
'table' => 'stuff_types',
'type' => 'left',
'conditions' => ['stuffType.id = stuff.stuff_type_id']
]])
->will($this->returnValue($query));

$query->expects($this->at(5))->method('join')
->with(['company' => [
'table' => 'organizations',
'type' => 'left',
'conditions' => ['company.id = client.company_id']
]])
->will($this->returnValue($query));

$query->expects($this->at(6))->method('join')
->with(['category' => [
'table' => 'categories',
'type' => 'left',
'conditions' => ['category.id = company.category_id']
]])
->will($this->returnValue($query));

$query
->select('foo.id')
->repository($table)
->contain($contains)->sql();
}

}
34 changes: 33 additions & 1 deletion lib/Cake/Test/TestCase/ORM/TableTest.php
Expand Up @@ -20,6 +20,14 @@
use Cake\Database\Connection;
use Cake\ORM\Table;

/**
* Used to test correct class is instantiated when using Table::build();
*
**/
class DatesTable extends Table {

}

/**
* Tests Table class
*
Expand All @@ -33,6 +41,7 @@ public function setUp() {
public function tearDown() {
$this->connection->execute('DROP TABLE IF EXISTS things');
$this->connection->execute('DROP TABLE IF EXISTS dates');
Table::clearRegistry();
}

/**
Expand Down Expand Up @@ -106,12 +115,35 @@ public function testMapAndBuild() {
$this->assertEquals(['things' => $options], $map);
$this->assertEquals($options, Table::map('things'));

$options += ['schema' => ['id' => ['rubbish']]];
$schema = ['id' => ['rubbish']];
$options += ['schema' => $schema];
Table::map('things', $options);

$table = Table::build('foo', ['table' => 'things']);
$this->assertInstanceOf('Cake\ORM\Table', $table);
$this->assertEquals('things', $table->table());
$this->assertEquals('foo', $table->alias());
$this->assertSame($this->connection, $table->connection());
$this->assertEquals($schema, $table->schema());

Table::clearRegistry();
$this->assertEmpty(Table::map());

$options['className'] = __NAMESPACE__ . '\DatesTable';
Table::map('dates', $options);
$table = Table::build('foo', ['table' => 'dates']);
$this->assertInstanceOf(__NAMESPACE__ . '\DatesTable', $table);
$this->assertEquals('dates', $table->table());
$this->assertEquals('foo', $table->alias());
$this->assertSame($this->connection, $table->connection());
$this->assertEquals($schema, $table->schema());
}

public function testInstance() {
$this->assertNull(Table::instance('things'));
$table = new Table(['table' => 'things']);
Table::instance('things', $table);
$this->assertSame($table, Table::instance('things'));
}

public function testFindAllNoFields() {
Expand Down

0 comments on commit 1f7f87d

Please sign in to comment.