Browse Source

Merge branch 'master' into 3.next

Mark Story 6 years ago
parent
commit
5d24e860d5

+ 39 - 18
src/Core/PluginCollection.php

@@ -28,6 +28,9 @@ use Iterator;
  * This class implements the Iterator interface to allow plugins
  * to be iterated, handling the situation where a plugin's hook
  * method (usually bootstrap) loads another plugin during iteration.
+ *
+ * While its implementation supported nested iteration it does not
+ * support using `continue` or `break` inside loops.
  */
 class PluginCollection implements Iterator, Countable
 {
@@ -46,11 +49,18 @@ class PluginCollection implements Iterator, Countable
     protected $names = [];
 
     /**
-     * Iterator position.
+     * Iterator position stack.
+     *
+     * @var int[]
+     */
+    protected $positions = [];
+
+    /**
+     * Loop depth
      *
      * @var int
      */
-    protected $position = 0;
+    protected $loopDepth = -1;
 
     /**
      * Constructor
@@ -166,6 +176,9 @@ class PluginCollection implements Iterator, Countable
     public function clear()
     {
         $this->plugins = [];
+        $this->names = [];
+        $this->positions = [];
+        $this->loopDepth = -1;
 
         return $this;
     }
@@ -198,13 +211,25 @@ class PluginCollection implements Iterator, Countable
     }
 
     /**
+     * Implementation of Countable.
+     *
+     * Get the number of plugins in the collection.
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->plugins);
+    }
+
+    /**
      * Part of Iterator Interface
      *
      * @return void
      */
     public function next()
     {
-        $this->position++;
+        $this->positions[$this->loopDepth]++;
     }
 
     /**
@@ -214,7 +239,7 @@ class PluginCollection implements Iterator, Countable
      */
     public function key()
     {
-        return $this->names[$this->position];
+        return $this->names[$this->positions[$this->loopDepth]];
     }
 
     /**
@@ -224,7 +249,8 @@ class PluginCollection implements Iterator, Countable
      */
     public function current()
     {
-        $name = $this->names[$this->position];
+        $position = $this->positions[$this->loopDepth];
+        $name = $this->names[$position];
 
         return $this->plugins[$name];
     }
@@ -236,7 +262,8 @@ class PluginCollection implements Iterator, Countable
      */
     public function rewind()
     {
-        $this->position = 0;
+        $this->positions[] = 0;
+        $this->loopDepth += 1;
     }
 
     /**
@@ -246,19 +273,13 @@ class PluginCollection implements Iterator, Countable
      */
     public function valid()
     {
-        return $this->position < count($this->plugins);
-    }
+        $valid = isset($this->names[$this->positions[$this->loopDepth]]);
+        if (!$valid) {
+            array_pop($this->positions);
+            $this->loopDepth -= 1;
+        }
 
-    /**
-     * Implementation of Countable.
-     *
-     * Get the number of plugins in the collection.
-     *
-     * @return int
-     */
-    public function count()
-    {
-        return count($this->plugins);
+        return $valid;
     }
 
     /**

+ 5 - 3
src/ORM/Table.php

@@ -2538,9 +2538,11 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
         $association = $this->_associations->get($property);
         if (!$association) {
             throw new RuntimeException(sprintf(
-                'Table "%s" is not associated with "%s"',
-                get_class($this),
-                $property
+                'Undefined property `%s`. ' .
+                'You have not defined the `%s` association on `%s`.',
+                $property,
+                $property,
+                static::class
             ));
         }
 

+ 1 - 2
src/ORM/TableRegistry.php

@@ -41,8 +41,7 @@ use Cake\ORM\Locator\LocatorInterface;
  *
  * ### Getting instances
  *
- * You can fetch instances out of the registry through the `TableLocator`.
- * {@link TableLocator::get()}
+ * You can fetch instances out of the registry through `TableLocator::get()`.
  * One instance is stored per alias. Once an alias is populated the same
  * instance will always be returned. This reduces the ORM memory cost and
  * helps make cyclic references easier to solve.

+ 1 - 1
src/View/Form/FormContext.php

@@ -226,6 +226,6 @@ class FormContext implements ContextInterface
      */
     public function error($field)
     {
-        return array_values((array)Hash::get($this->_form->getErrors(), $field, []));
+        return (array)Hash::get($this->_form->getErrors(), $field, []);
     }
 }

+ 30 - 0
tests/TestCase/Core/PluginCollectionTest.php

@@ -130,6 +130,36 @@ class PluginCollectionTest extends TestCase
         $this->assertSame($pluginThree, $out[0]);
     }
 
+    /**
+     * Test that looping over the plugin collection during
+     * a with loop doesn't lose iteration state.
+     *
+     * This situation can happen when a plugin like bake
+     * needs to discover things inside other plugins.
+     *
+     * @return
+     */
+    public function testWithInnerIteration()
+    {
+        $plugins = new PluginCollection();
+        $plugin = new TestPlugin();
+        $pluginThree = new TestPluginThree();
+
+        $plugins->add($plugin);
+        $plugins->add($pluginThree);
+
+        $out = [];
+        foreach ($plugins->with('routes') as $p) {
+            foreach ($plugins as $i) {
+                // Do nothing, we just need to enumerate the collection
+            }
+            $out[] = $p;
+        }
+        $this->assertCount(2, $out);
+        $this->assertSame($plugin, $out[0]);
+        $this->assertSame($pluginThree, $out[1]);
+    }
+
     public function testWithInvalidHook()
     {
         $this->expectException(InvalidArgumentException::class);

+ 1 - 1
tests/TestCase/ORM/AssociationProxyTest.php

@@ -68,7 +68,7 @@ class AssociationProxyTest extends TestCase
     public function testGetBadAssociation()
     {
         $this->expectException(\RuntimeException::class);
-        $this->expectExceptionMessage('Table "Cake\ORM\Table" is not associated with "posts"');
+        $this->expectExceptionMessage('You have not defined');
         $articles = $this->getTableLocator()->get('articles');
         $articles->posts;
     }

+ 4 - 4
tests/TestCase/View/Form/FormContextTest.php

@@ -295,9 +295,9 @@ class FormContextTest extends TestCase
 
         $context = new FormContext($this->request, ['entity' => $form]);
         $this->assertEquals([], $context->error('empty'));
-        $this->assertEquals(['The provided value is invalid'], $context->error('email'));
-        $this->assertEquals(['The provided value is invalid'], $context->error('name'));
-        $this->assertEquals(['The provided value is invalid'], $context->error('pass.password'));
+        $this->assertEquals(['format' => 'The provided value is invalid'], $context->error('email'));
+        $this->assertEquals(['length' => 'The provided value is invalid'], $context->error('name'));
+        $this->assertEquals(['length' => 'The provided value is invalid'], $context->error('pass.password'));
         $this->assertEquals([], $context->error('Alias.name'));
         $this->assertEquals([], $context->error('nope.nope'));
 
@@ -307,7 +307,7 @@ class FormContextTest extends TestCase
         $form->validate([]);
         $context = new FormContext($this->request, ['entity' => $form]);
         $this->assertEquals(
-            ['should be an array, not a string'],
+            ['_required' => 'should be an array, not a string'],
             $context->error('key'),
             'This test should not produce a PHP warning from array_values().'
         );