Browse Source

Merge pull request #6929 from cakephp/3.0-association-binding-key

3.0 association binding key
José Lorenzo Rodríguez 10 years ago
parent
commit
fe2c02573d

+ 36 - 4
src/ORM/Association.php

@@ -100,6 +100,13 @@ abstract class Association
     protected $_className;
 
     /**
+     * The field name in the owning side table that is used to match with the foreignKey
+     *
+     * @var string|array
+     */
+    protected $_bindingKey;
+
+    /**
      * The name of the field representing the foreign key to the table to load
      *
      * @var string|array
@@ -196,6 +203,7 @@ abstract class Association
             'conditions',
             'dependent',
             'finder',
+            'bindingKey',
             'foreignKey',
             'joinType',
             'propertyName',
@@ -317,6 +325,30 @@ abstract class Association
     }
 
     /**
+     * Sets the name of the field representing the binding field with the target table.
+     * When not manually specified the primary key of the owning side table is used.
+     *
+     * If no parameters are passed the current field is returned
+     *
+     * @param string|null $key the table field to be used to link both tables together
+     * @return string|array
+     */
+    public function bindingKey($key = null)
+    {
+        if ($key !== null) {
+            $this->_bindingKey = $key;
+        }
+
+        if ($this->_bindingKey === null) {
+            $this->_bindingKey = $this->isOwningSide($this->source()) ?
+                $this->source()->primaryKey() :
+                $this->target()->primaryKey();
+        }
+
+        return $this->_bindingKey;
+    }
+
+    /**
      * Sets the name of the field representing the foreign key to the target table.
      * If no parameters are passed the current field is returned
      *
@@ -749,20 +781,20 @@ abstract class Association
         $tAlias = $this->target()->alias();
         $sAlias = $this->source()->alias();
         $foreignKey = (array)$options['foreignKey'];
-        $primaryKey = (array)$this->_sourceTable->primaryKey();
+        $bindingKey = (array)$this->bindingKey();
 
-        if (count($foreignKey) !== count($primaryKey)) {
+        if (count($foreignKey) !== count($bindingKey)) {
             $msg = 'Cannot match provided foreignKey for "%s", got "(%s)" but expected foreign key for "(%s)"';
             throw new RuntimeException(sprintf(
                 $msg,
                 $this->_name,
                 implode(', ', $foreignKey),
-                implode(', ', $primaryKey)
+                implode(', ', $bindingKey)
             ));
         }
 
         foreach ($foreignKey as $k => $f) {
-            $field = sprintf('%s.%s', $sAlias, $primaryKey[$k]);
+            $field = sprintf('%s.%s', $sAlias, $bindingKey[$k]);
             $value = new IdentifierExpression(sprintf('%s.%s', $tAlias, $f));
             $conditions[$field] = $value;
         }

+ 7 - 7
src/ORM/Association/BelongsTo.php

@@ -143,7 +143,7 @@ class BelongsTo extends Association
 
         $properties = array_combine(
             (array)$this->foreignKey(),
-            $targetEntity->extract((array)$table->primaryKey())
+            $targetEntity->extract((array)$this->bindingKey())
         );
         $entity->set($properties, ['guard' => false]);
         return $entity;
@@ -164,20 +164,20 @@ class BelongsTo extends Association
         $tAlias = $this->target()->alias();
         $sAlias = $this->_sourceTable->alias();
         $foreignKey = (array)$options['foreignKey'];
-        $primaryKey = (array)$this->_targetTable->primaryKey();
+        $bindingKey = (array)$this->bindingKey();
 
-        if (count($foreignKey) !== count($primaryKey)) {
+        if (count($foreignKey) !== count($bindingKey)) {
             $msg = 'Cannot match provided foreignKey for "%s", got "(%s)" but expected foreign key for "(%s)"';
             throw new RuntimeException(sprintf(
                 $msg,
                 $this->_name,
                 implode(', ', $foreignKey),
-                implode(', ', $primaryKey)
+                implode(', ', $bindingKey)
             ));
         }
 
         foreach ($foreignKey as $k => $f) {
-            $field = sprintf('%s.%s', $tAlias, $primaryKey[$k]);
+            $field = sprintf('%s.%s', $tAlias, $bindingKey[$k]);
             $value = new IdentifierExpression(sprintf('%s.%s', $sAlias, $f));
             $conditions[$field] = $value;
         }
@@ -193,7 +193,7 @@ class BelongsTo extends Association
         $links = [];
         $name = $this->alias();
 
-        foreach ((array)$this->target()->primaryKey() as $key) {
+        foreach ((array)$this->bindingKey() as $key) {
             $links[] = sprintf('%s.%s', $name, $key);
         }
 
@@ -210,7 +210,7 @@ class BelongsTo extends Association
     protected function _buildResultMap($fetchQuery, $options)
     {
         $resultMap = [];
-        $key = (array)$this->target()->primaryKey();
+        $key = (array)$this->bindingKey();
 
         foreach ($fetchQuery->all() as $result) {
             $values = [];

+ 8 - 8
src/ORM/Association/BelongsToMany.php

@@ -348,11 +348,11 @@ class BelongsToMany extends Association
             return true;
         }
         $foreignKey = (array)$this->foreignKey();
-        $primaryKey = (array)$this->source()->primaryKey();
+        $bindingKey = (array)$this->bindingKey();
         $conditions = [];
 
-        if ($primaryKey) {
-            $conditions = array_combine($foreignKey, $entity->extract((array)$primaryKey));
+        if ($bindingKey) {
+            $conditions = array_combine($foreignKey, $entity->extract($bindingKey));
         }
 
         $table = $this->junction();
@@ -533,7 +533,7 @@ class BelongsToMany extends Association
         $foreignKey = (array)$this->foreignKey();
         $assocForeignKey = (array)$belongsTo->foreignKey();
         $targetPrimaryKey = (array)$target->primaryKey();
-        $sourcePrimaryKey = (array)$source->primaryKey();
+        $bindingKey = (array)$this->bindingKey();
         $jointProperty = $this->_junctionProperty;
         $junctionAlias = $junction->alias();
 
@@ -545,7 +545,7 @@ class BelongsToMany extends Association
 
             $joint->set(array_combine(
                 $foreignKey,
-                $sourceEntity->extract($sourcePrimaryKey)
+                $sourceEntity->extract($bindingKey)
             ), ['guard' => false]);
             $joint->set(array_combine($assocForeignKey, $e->extract($targetPrimaryKey)), ['guard' => false]);
             $saved = $junction->save($joint, $options);
@@ -718,10 +718,10 @@ class BelongsToMany extends Association
      */
     public function replaceLinks(EntityInterface $sourceEntity, array $targetEntities, array $options = [])
     {
-        $primaryKey = (array)$this->source()->primaryKey();
-        $primaryValue = $sourceEntity->extract($primaryKey);
+        $bindingKey = (array)$this->bindingKey();
+        $primaryValue = $sourceEntity->extract($bindingKey);
 
-        if (count(array_filter($primaryValue, 'strlen')) !== count($primaryKey)) {
+        if (count(array_filter($primaryValue, 'strlen')) !== count($bindingKey)) {
             $message = 'Could not find primary key value for source entity';
             throw new InvalidArgumentException($message);
         }

+ 2 - 2
src/ORM/Association/DependentDeleteTrait.php

@@ -40,8 +40,8 @@ trait DependentDeleteTrait
         }
         $table = $this->target();
         $foreignKey = (array)$this->foreignKey();
-        $primaryKey = (array)$this->source()->primaryKey();
-        $conditions = array_combine($foreignKey, $entity->extract($primaryKey));
+        $bindingKey = (array)$this->bindingKey();
+        $conditions = array_combine($foreignKey, $entity->extract($bindingKey));
 
         if ($this->_cascadeCallbacks) {
             $query = $this->find('all')->where($conditions);

+ 1 - 1
src/ORM/Association/HasMany.php

@@ -98,7 +98,7 @@ class HasMany extends Association
 
         $properties = array_combine(
             (array)$this->foreignKey(),
-            $entity->extract((array)$this->source()->primaryKey())
+            $entity->extract((array)$this->bindingKey())
         );
         $target = $this->target();
         $original = $targetEntities;

+ 1 - 1
src/ORM/Association/HasOne.php

@@ -123,7 +123,7 @@ class HasOne extends Association
 
         $properties = array_combine(
             (array)$this->foreignKey(),
-            $entity->extract((array)$this->source()->primaryKey())
+            $entity->extract((array)$this->bindingKey())
         );
         $targetEntity->set($properties, ['guard' => false]);
 

+ 4 - 4
src/ORM/Association/SelectableAssociationTrait.php

@@ -25,8 +25,8 @@ trait SelectableAssociationTrait
 {
 
     /**
-     * Returns true if the eager loading process will require a set of parent table's
-     * primary keys in order to use them as a filter in the finder query.
+     * Returns true if the eager loading process will require a set of the owning table's
+     * binding keys in order to use them as a filter in the finder query.
      *
      * @param array $options The options containing the strategy to be used.
      * @return bool true if a list of keys will be required
@@ -234,7 +234,7 @@ trait SelectableAssociationTrait
             $filterQuery->offset(null);
         }
 
-        $keys = (array)$this->source()->primaryKey();
+        $keys = (array)$this->bindingKey();
 
         if ($this->type() === $this::MANY_TO_ONE) {
             $keys = (array)$this->foreignKey();
@@ -271,7 +271,7 @@ trait SelectableAssociationTrait
         $sAlias = $source->alias();
         $keys = $this->type() === $this::MANY_TO_ONE ?
             $this->foreignKey() :
-            $source->primaryKey();
+            $this->bindingKey();
 
         $sourceKeys = [];
         foreach ((array)$keys as $key) {

+ 1 - 1
src/ORM/EagerLoader.php

@@ -636,7 +636,7 @@ class EagerLoader
             $source = $instance->source();
             $keys = $instance->type() === Association::MANY_TO_ONE ?
                 (array)$instance->foreignKey() :
-                (array)$source->primaryKey();
+                (array)$instance->bindingKey();
 
             $alias = $source->alias();
             $pkFields = [];

+ 2 - 3
tests/Fixture/AuthUsersFixture.php

@@ -44,10 +44,9 @@ class AuthUsersFixture extends TestFixture
      */
     public $records = [
         ['username' => 'mariano', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'],
-        ['username' => 'nate', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'created' => '2007-03-17 01:18:23', 'updated' => '2007-03-17 01:20:31'],
         ['username' => 'larry', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'created' => '2007-03-17 01:20:23', 'updated' => '2007-03-17 01:22:31'],
-        ['username' => 'garrett', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'created' => '2007-03-17 01:22:23', 'updated' => '2007-03-17 01:24:31'],
         ['username' => 'chartjes', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'created' => '2007-03-17 01:22:23', 'updated' => '2007-03-17 01:24:31'],
-
+        ['username' => 'garrett', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'created' => '2007-03-17 01:22:23', 'updated' => '2007-03-17 01:24:31'],
+        ['username' => 'nate', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'created' => '2007-03-17 01:18:23', 'updated' => '2007-03-17 01:20:31'],
     ];
 }

+ 47 - 0
tests/TestCase/ORM/AssociationTest.php

@@ -121,6 +121,53 @@ class AssociationTest extends TestCase
     }
 
     /**
+     * Tests the bindingKey method as a setter/getter
+     *
+     * @return void
+     */
+    public function testBindingKey()
+    {
+        $this->association->bindingKey('foo_id');
+        $this->assertEquals('foo_id', $this->association->bindingKey());
+    }
+
+    /**
+     * Tests the bindingKey() method when called with its defaults
+     *
+     * @return void
+     */
+    public function testBindingKeyDefault()
+    {
+        $this->source->primaryKey(['id', 'site_id']);
+        $this->association
+            ->expects($this->once())
+            ->method('isOwningSide')
+            ->will($this->returnValue(true));
+        $result = $this->association->bindingKey();
+        $this->assertEquals(['id', 'site_id'], $result);
+    }
+
+    /**
+     * Tests the bindingKey() method when the association source is not the
+     * owning side
+     *
+     * @return void
+     */
+    public function testBindingDefaultNoOwningSide()
+    {
+        $target = new Table;
+        $target->primaryKey(['foo', 'site_id']);
+        $this->association->target($target);
+
+        $this->association
+            ->expects($this->once())
+            ->method('isOwningSide')
+            ->will($this->returnValue(false));
+        $result = $this->association->bindingKey();
+        $this->assertEquals(['foo', 'site_id'], $result);
+    }
+
+    /**
      * Tests that name() returns the correct configured value
      *
      * @return void

+ 137 - 0
tests/TestCase/ORM/BindingKeyTest.php

@@ -0,0 +1,137 @@
+<?php
+/**
+ * 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         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\ORM;
+
+use Cake\ORM\TableRegistry;
+use Cake\TestSuite\TestCase;
+
+/**
+ * Integration tests for usinge the bindingKey in associations
+ */
+class BindingKeyTest extends TestCase
+{
+
+    /**
+     * Fixture to be used
+     *
+     * @var array
+     */
+    public $fixtures = [
+        'core.users',
+        'core.auth_users',
+        'core.site_authors'
+    ];
+
+    /**
+     * Data provider for the two types of strategies BelongsTo and HasOne implements
+     *
+     * @return void
+     */
+    public function strategiesProviderJoinable()
+    {
+        return [['join'], ['select']];
+    }
+
+    /**
+     * Data provider for the two types of strategies HasMany and BelongsToMany implements
+     *
+     * @return void
+     */
+    public function strategiesProviderExternal()
+    {
+        return [['subquery'], ['select']];
+    }
+
+    /**
+     * Tests that bindingKey can be used in belongsTo associations
+     *
+     * @dataProvider strategiesProviderJoinable
+     * @return void
+     */
+    public function testBelongsto($strategy)
+    {
+        $users = TableRegistry::get('Users');
+        $users->belongsTo('AuthUsers', [
+            'bindingKey' => 'username',
+            'foreignKey' => 'username',
+            'strategy' => $strategy
+        ]);
+
+        $result = $users->find()
+            ->contain(['AuthUsers']);
+
+        $expected = ['mariano', 'nate', 'larry', 'garrett'];
+        $expected = array_combine($expected, $expected);
+        $this->assertEquals(
+            $expected,
+            $result->combine('username', 'auth_user.username')->toArray()
+        );
+
+        $expected = [1 => 1, 2 => 5, 3 => 2, 4 => 4];
+        $this->assertEquals(
+            $expected,
+            $result->combine('id', 'auth_user.id')->toArray()
+        );
+    }
+
+    /**
+     * Tests that bindingKey can be used in hasOne associations
+     *
+     * @dataProvider strategiesProviderJoinable
+     * @return void
+     */
+    public function testHasOne($strategy)
+    {
+        $users = TableRegistry::get('Users');
+        $users->hasOne('SiteAuthors', [
+            'bindingKey' => 'username',
+            'foreignKey' => 'name',
+            'strategy' => $strategy
+        ]);
+
+        $users->updateAll(['username' => 'jose'], ['username' => 'garrett']);
+        $result = $users->find()
+            ->contain(['SiteAuthors'])
+            ->where(['username' => 'jose'])
+            ->first();
+
+        $this->assertEquals(3, $result->site_author->id);
+    }
+
+    /**
+     * Tests that bindingKey can be used in hasOne associations
+     *
+     * @dataProvider strategiesProviderExternal
+     * @return void
+     */
+    public function testHasMany($strategy)
+    {
+        $users = TableRegistry::get('Users');
+        $authors = $users->hasMany('SiteAuthors', [
+            'bindingKey' => 'username',
+            'foreignKey' => 'name',
+            'strategy' => $strategy
+        ]);
+
+        $authors->updateAll(['name' => 'garrett'], ['id >' => 2]);
+        $result = $users->find()
+            ->contain(['SiteAuthors'])
+            ->where(['username' => 'garrett']);
+
+        $expected = [3, 4];
+        $result = $result->extract('site_authors.{*}.id')->toList();
+        $this->assertEquals($expected, $result);
+    }
+}