Skip to content

Commit

Permalink
Fix recursive plugin loading.
Browse files Browse the repository at this point in the history
Plugins can load other plugins. Support this use case by implementing
the `Iterator` interface instead of `IteratorAggregate`. Using the basic
interface lets us extend the collection as it is being iterated, which
`IteratorAggregate` cannot do.

Refs #11787
  • Loading branch information
markstory committed Mar 16, 2018
1 parent d3833ca commit 3fb7afa
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 9 deletions.
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -65,6 +65,7 @@
"TestPluginTwo\\": "tests/test_app/Plugin/TestPluginTwo/src/",
"Company\\TestPluginThree\\": "tests/test_app/Plugin/Company/TestPluginThree/src/",
"Company\\TestPluginThree\\Test\\": "tests/test_app/Plugin/Company/TestPluginThree/tests/",
"ParentPlugin\\": "tests/test_app/Plugin/ParentPlugin/src/",
"PluginJs\\": "tests/test_app/Plugin/PluginJs/src/"
}
},
Expand Down
72 changes: 66 additions & 6 deletions src/Core/PluginCollection.php
Expand Up @@ -17,7 +17,7 @@
use Cake\Core\Exception\MissingPluginException;
use Countable;
use InvalidArgumentException;
use IteratorAggregate;
use Iterator;
use RuntimeException;

/**
Expand All @@ -26,8 +26,12 @@
* Holds onto plugin objects loaded into an application, and
* provides methods for iterating, and finding plugins based
* on criteria.
*
* 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.
*/
class PluginCollection implements IteratorAggregate, Countable
class PluginCollection implements Iterator, Countable
{
/**
* Plugin list
Expand All @@ -36,6 +40,18 @@ class PluginCollection implements IteratorAggregate, Countable
*/
protected $plugins = [];

/**
* Names of plugins
*
* @var array
*/
protected $names = [];

/**
* @var null|string
*/
protected $position = 0;

/**
* Constructor
*
Expand All @@ -60,6 +76,7 @@ public function add(PluginInterface $plugin)
{
$name = $plugin->getName();
$this->plugins[$name] = $plugin;
$this->names = array_keys($this->plugins);

return $this;
}
Expand All @@ -73,6 +90,7 @@ public function add(PluginInterface $plugin)
public function remove($name)
{
unset($this->plugins[$name]);
$this->names = array_keys($this->plugins);

return $this;
}
Expand Down Expand Up @@ -105,13 +123,55 @@ public function get($name)
}

/**
* Implementation of IteratorAggregate.
* Part of Iterator Interface
*
* @return void
*/
public function next()
{
$this->position += 1;
}

/**
* Part of Iterator Interface
*
* @return string
*/
public function key()
{
return $this->names[$this->position];
}

/**
* Part of Iterator Interface
*
* @return \Cake\Core\PluginInterface
*/
public function current()
{
$name = $this->names[$this->position];

return $this->plugins[$name];
}

/**
* Part of Iterator Interface
*
* @return \ArrayIterator
* @return void
*/
public function rewind()
{
$this->position = 0;
}

/**
* Part of Iterator Interface
*
* @return bool
*/
public function getIterator()
public function valid()
{
return new ArrayIterator($this->plugins);
return $this->position < count($this->plugins);
}

/**
Expand Down
15 changes: 12 additions & 3 deletions tests/TestCase/Core/PluginTest.php
Expand Up @@ -301,7 +301,10 @@ public function testClassPathNotFound()
public function testLoadAll()
{
Plugin::loadAll();
$expected = ['Company', 'PluginJs', 'TestPlugin', 'TestPluginFour', 'TestPluginTwo', 'TestTheme'];
$expected = [
'Company', 'ParentPlugin', 'PluginJs', 'TestPlugin',
'TestPluginFour', 'TestPluginTwo', 'TestTheme'
];
$this->assertEquals($expected, Plugin::loaded());
}

Expand Down Expand Up @@ -338,7 +341,10 @@ public function testLoadAllWithDefaults()
{
$defaults = ['bootstrap' => true, 'ignoreMissing' => true];
Plugin::loadAll([$defaults]);
$expected = ['Company', 'PluginJs', 'TestPlugin', 'TestPluginFour', 'TestPluginTwo', 'TestTheme'];
$expected = [
'Company', 'ParentPlugin', 'PluginJs', 'TestPlugin',
'TestPluginFour', 'TestPluginTwo', 'TestTheme'
];
$this->assertEquals($expected, Plugin::loaded());
$this->assertEquals('loaded js plugin bootstrap', Configure::read('PluginTest.js_plugin.bootstrap'));
$this->assertEquals('loaded plugin bootstrap', Configure::read('PluginTest.test_plugin.bootstrap'));
Expand All @@ -360,7 +366,10 @@ public function testLoadAllWithDefaultsAndOverride()
]);
Plugin::routes();

$expected = ['Company', 'PluginJs', 'TestPlugin', 'TestPluginFour', 'TestPluginTwo', 'TestTheme'];
$expected = [
'Company', 'ParentPlugin', 'PluginJs', 'TestPlugin',
'TestPluginFour', 'TestPluginTwo', 'TestTheme'
];
$this->assertEquals($expected, Plugin::loaded());
$this->assertEquals('loaded js plugin bootstrap', Configure::read('PluginTest.js_plugin.bootstrap'));
$this->assertEquals('loaded plugin routes', Configure::read('PluginTest.test_plugin.routes'));
Expand Down
29 changes: 29 additions & 0 deletions tests/TestCase/Http/BaseApplicationTest.php
Expand Up @@ -182,4 +182,33 @@ public function testPluginBootstrapInteractWithPluginLoad()
'Key should not be set, as plugin has already had bootstrap run'
);
}

/**
* Test that plugins loaded with addPlugin() can load additional
* plugins.
*
* @return void
*/
public function testPluginBootstrapRecursivePlugins()
{
$app = $this->getMockForAbstractClass(
BaseApplication::class,
[$this->path]
);
$app->addPlugin('ParentPlugin');
$app->pluginBootstrap();

$this->assertTrue(
Configure::check('ParentPlugin.bootstrap'),
'Plugin bootstrap should be run'
);
$this->assertTrue(
Configure::check('PluginTest.test_plugin.bootstrap'),
'Nested plugin should have bootstrap run'
);
$this->assertTrue(
Configure::check('PluginTest.test_plugin_two.bootstrap'),
'Nested plugin should have bootstrap run'
);
}
}
18 changes: 18 additions & 0 deletions tests/test_app/Plugin/ParentPlugin/src/Plugin.php
@@ -0,0 +1,18 @@
<?php
namespace ParentPlugin;

use Cake\Core\BasePlugin;
use Cake\Core\Configure;
use Cake\Core\PluginApplicationInterface;
use Cake\Core\Plugin as CorePlugin;

class Plugin extends BasePlugin
{
public function bootstrap(PluginApplicationInterface $app)
{
Configure::write('ParentPlugin.bootstrap', true);

CorePlugin::load('TestPluginTwo', ['bootstrap' => true]);
$app->addPlugin('TestPlugin', ['bootstrap' => true]);
}
}

0 comments on commit 3fb7afa

Please sign in to comment.