Browse Source

Merge pull request #11638 from cakephp/3-next-get-association

Use a stricter get+has pattern for associations on tables.
Mark Story 8 years ago
parent
commit
cc48cfde5c

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

@@ -354,14 +354,14 @@ class BelongsToMany extends Association
         $junctionAlias = $junction->getAlias();
         $sAlias = $source->getAlias();
 
-        if (!$target->getAssociation($junctionAlias)) {
+        if (!$target->hasAssociation($junctionAlias)) {
             $target->hasMany($junctionAlias, [
                 'targetTable' => $junction,
                 'foreignKey' => $this->getTargetForeignKey(),
                 'strategy' => $this->_strategy,
             ]);
         }
-        if (!$target->getAssociation($sAlias)) {
+        if (!$target->hasAssociation($sAlias)) {
             $target->belongsToMany($sAlias, [
                 'sourceTable' => $target,
                 'targetTable' => $source,
@@ -391,7 +391,7 @@ class BelongsToMany extends Association
     protected function _generateSourceAssociations($junction, $source)
     {
         $junctionAlias = $junction->getAlias();
-        if (!$source->getAssociation($junctionAlias)) {
+        if (!$source->hasAssociation($junctionAlias)) {
             $source->hasMany($junctionAlias, [
                 'targetTable' => $junction,
                 'foreignKey' => $this->getForeignKey(),
@@ -421,13 +421,13 @@ class BelongsToMany extends Association
         $tAlias = $target->getAlias();
         $sAlias = $source->getAlias();
 
-        if (!$junction->getAssociation($tAlias)) {
+        if (!$junction->hasAssociation($tAlias)) {
             $junction->belongsTo($tAlias, [
                 'foreignKey' => $this->getTargetForeignKey(),
                 'targetTable' => $target
             ]);
         }
-        if (!$junction->getAssociation($sAlias)) {
+        if (!$junction->hasAssociation($sAlias)) {
             $junction->belongsTo($sAlias, [
                 'foreignKey' => $this->getForeignKey(),
                 'targetTable' => $source

+ 3 - 2
src/ORM/Marshaller.php

@@ -88,10 +88,9 @@ class Marshaller
                 $key = $nested;
                 $nested = [];
             }
-            $assoc = $this->_table->getAssociation($key);
             // If the key is not a special field like _ids or _joinData
             // it is a missing association that we should error on.
-            if (!$assoc) {
+            if (!$this->_table->hasAssociation($key)) {
                 if (substr($key, 0, 1) !== '_') {
                     throw new \InvalidArgumentException(sprintf(
                         'Cannot marshal data for "%s" association. It is not associated with "%s".',
@@ -101,6 +100,8 @@ class Marshaller
                 }
                 continue;
             }
+            $assoc = $this->_table->getAssociation($key);
+
             if (isset($options['forceNew'])) {
                 $nested['forceNew'] = $options['forceNew'];
             }

+ 2 - 2
src/ORM/Query.php

@@ -437,10 +437,10 @@ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface
     protected function _addAssociationsToTypeMap($table, $typeMap, $associations)
     {
         foreach ($associations as $name => $nested) {
-            $association = $table->getAssociation($name);
-            if (!$association) {
+            if (!$table->hasAssociation($name)) {
                 continue;
             }
+            $association = $table->getAssociation($name);
             $target = $association->getTarget();
             $primary = (array)$target->getPrimaryKey();
             if (empty($primary) || $typeMap->type($target->aliasField($primary[0])) === null) {

+ 2 - 2
src/ORM/Rule/ExistsIn.php

@@ -80,8 +80,7 @@ class ExistsIn
     public function __invoke(EntityInterface $entity, array $options)
     {
         if (is_string($this->_repository)) {
-            $repository = $options['repository']->getAssociation($this->_repository);
-            if (!$repository) {
+            if (!$options['repository']->hasAssociation($this->_repository)) {
                 throw new RuntimeException(sprintf(
                     "ExistsIn rule for '%s' is invalid. '%s' is not associated with '%s'.",
                     implode(', ', $this->_fields),
@@ -89,6 +88,7 @@ class ExistsIn
                     get_class($options['repository'])
                 ));
             }
+            $repository = $options['repository']->getAssociation($this->_repository);
             $this->_repository = $repository;
         }
 

+ 50 - 6
src/ORM/Table.php

@@ -954,17 +954,61 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     }
 
     /**
-     * Returns an association object configured for the specified alias if any
+     * Returns an association object configured for the specified alias if any.
      *
-     * @deprecated 3.6.0 Use getAssociation() instead.
+     * @deprecated 3.6.0 Use getAssociation() and Table::hasAssocation() instead.
      * @param string $name the alias used for the association.
      * @return \Cake\ORM\Association|null Either the association or null.
      */
     public function association($name)
     {
-        deprecationWarning('Use Table::getAssociation() instead.');
+        deprecationWarning('Use Table::getAssociation() and Table::hasAssocation() instead.');
+
+        return $this->findAssociation($name);
+    }
+
+    /**
+     * Returns an association object configured for the specified alias.
+     *
+     * The name argument also supports dot syntax to access deeper associations.
+     *
+     * ```
+     * $users = $this->getAssociation('Articles.Comments.Users');
+     * ```
+     *
+     * Note that this method requires the association to be present or otherwise
+     * throws an exception.
+     * If you are not sure, use hasAssociation() before calling this method.
+     *
+     * @param string $name The alias used for the association.
+     * @return \Cake\ORM\Association The association.
+     * @throws \InvalidArgumentException
+     */
+    public function getAssociation($name)
+    {
+        $association = $this->findAssociation($name);
+        if (!$association) {
+            throw new InvalidArgumentException("The {$name} association is not defined on {$this->getAlias()}.");
+        }
 
-        return $this->getAssociation($name);
+        return $association;
+    }
+
+    /**
+     * Checks whether a specific association exists on this Table instance.
+     *
+     * The name argument also supports dot syntax to access deeper associations.
+     *
+     * ```
+     * $hasUsers = $this->hasAssociation('Articles.Comments.Users');
+     * ```
+     *
+     * @param string $name The alias used for the association.
+     * @return bool
+     */
+    public function hasAssociation($name)
+    {
+        return $this->findAssociation($name) !== null;
     }
 
     /**
@@ -976,10 +1020,10 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * $users = $this->getAssociation('Articles.Comments.Users');
      * ```
      *
-     * @param string $name the alias used for the association.
+     * @param string $name The alias used for the association.
      * @return \Cake\ORM\Association|null Either the association or null.
      */
-    public function getAssociation($name)
+    protected function findAssociation($name)
     {
         if (strpos($name, '.') === false) {
             return $this->_associations->get($name);

+ 12 - 0
tests/TestCase/ORM/TableTest.php

@@ -645,6 +645,18 @@ class TableTest extends TestCase
     }
 
     /**
+     * Tests that the getAssociation() method throws an exception on non-existent ones.
+     *
+     * @return void
+     */
+    public function testGetAssociationNonExistent()
+    {
+        $this->expectException(InvalidArgumentException::class);
+
+        TableRegistry::get('Groups')->getAssociation('FooBar');
+    }
+
+    /**
      * Tests that belongsTo() creates and configures correctly the association
      *
      * @return void