Browse Source

Dynamically create plugin instances

When a plugin does not define a plugin class, it is more helpful to the
end user and plugin maintainers for us to dynamically create an
instance. This also adds consistency between Plugin::load() and
addPlugin(). All hooks will be enabled on the dynamic class to maintain
consistency for when a plugin does define a class.

I've added tests to help ensure that BasePlugin doesn't emit errors when
routes/bootstrap files are missing.
Mark Story 7 years ago
parent
commit
93f73c5456

+ 4 - 0
src/Core/PluginApplicationInterface.php

@@ -27,6 +27,10 @@ interface PluginApplicationInterface extends EventDispatcherInterface
     /**
      * Add a plugin to the loaded plugin set.
      *
+     * If the named plugin does not exist, or does not define a Plugin class, an
+     * instance of `Cake\Core\BasePlugin` will be used. This generated class will have
+     * all plugin hooks enabled.
+     *
      * @param string|\Cake\Core\PluginInterface $name The plugin name or plugin object.
      * @param array $config The configuration data for the plugin if using a string for $name
      * @return $this

+ 8 - 8
src/Http/BaseApplication.php

@@ -14,6 +14,7 @@
  */
 namespace Cake\Http;
 
+use Cake\Core\BasePlugin;
 use Cake\Core\ConsoleApplicationInterface;
 use Cake\Core\HttpApplicationInterface;
 use Cake\Core\Plugin;
@@ -120,16 +121,15 @@ abstract class BaseApplication implements
      */
     public function makePlugin($name, array $config)
     {
-        if (strpos($name, '\\') === false) {
-            $name = str_replace('/', '\\', $name) . '\\' . 'Plugin';
+        $className = $name;
+        if (strpos($className, '\\') === false) {
+            $className = str_replace('/', '\\', $className) . '\\' . 'Plugin';
         }
-        if (!class_exists($name)) {
-            throw new InvalidArgumentException(
-                "The plugin class `{$name}` cannot be found. " .
-                'Ensure your autoloader is correct.'
-            );
+        if (!class_exists($className)) {
+            $config['name'] = $name;
+            $className = BasePlugin::class;
         }
-        $plugin = new $name($config);
+        $plugin = new $className($config);
         if (!$plugin instanceof PluginInterface) {
             throw new InvalidArgumentException("The `{$name}` plugin does not implement Cake\Core\PluginInterface.");
         }

+ 23 - 0
tests/TestCase/Core/BasePluginTest.php

@@ -19,6 +19,8 @@ use Cake\Core\Configure;
 use Cake\Core\Plugin;
 use Cake\Core\PluginApplicationInterface;
 use Cake\Http\MiddlewareQueue;
+use Cake\Routing\RouteBuilder;
+use Cake\Routing\RouteCollection;
 use Cake\TestSuite\TestCase;
 use Company\TestPluginThree\Plugin as TestPluginThree;
 use TestPlugin\Plugin as TestPlugin;
@@ -111,6 +113,27 @@ class BasePluginTest extends TestCase
         $this->assertTrue(Configure::check('PluginTest.test_plugin.bootstrap'));
     }
 
+    /**
+     * No errors should be emitted when a plugin doesn't have a bootstrap file.
+     */
+    public function testBootstrapSkipMissingFile()
+    {
+        $app = $this->createMock(PluginApplicationInterface::class);
+        $plugin = new BasePlugin();
+        $this->assertNull($plugin->bootstrap($app));
+    }
+
+    /**
+     * No errors should be emitted when a plugin doesn't have a routes file.
+     */
+    public function testRoutesSkipMissingFile()
+    {
+        $app = $this->createMock(PluginApplicationInterface::class);
+        $plugin = new BasePlugin();
+        $routeBuilder = new RouteBuilder(new RouteCollection(), '/');
+        $this->assertNull($plugin->routes($routeBuilder));
+    }
+
     public function testConstructorArguments()
     {
         $plugin = new BasePlugin([

+ 13 - 4
tests/TestCase/Http/BaseApplicationTest.php

@@ -14,6 +14,7 @@
  */
 namespace Cake\Test\TestCase\Http;
 
+use Cake\Core\BasePlugin;
 use Cake\Core\Configure;
 use Cake\Core\Plugin;
 use Cake\Http\BaseApplication;
@@ -25,6 +26,7 @@ use Cake\Routing\RouteCollection;
 use Cake\Routing\Router;
 use Cake\TestSuite\TestCase;
 use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
 use TestPlugin\Plugin as TestPlugin;
 
 /**
@@ -69,20 +71,27 @@ class BaseApplicationTest extends TestCase
             'pass' => []
         ]);
 
-        $app = $this->getMockForAbstractClass('Cake\Http\BaseApplication', [$this->path]);
+        $app = $this->getMockForAbstractClass(BaseApplication::class, [$this->path]);
         $result = $app($request, $response, $next);
-        $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $result);
+        $this->assertInstanceOf(ResponseInterface::class, $result);
         $this->assertEquals('Hello Jane', '' . $result->getBody());
     }
 
+    /**
+     * Ensure that plugins with no plugin class can be loaded.
+     * This makes adopting the new API easier
+     */
     public function testAddPluginUnknownClass()
     {
-        $this->expectException(InvalidArgumentException::class);
-        $this->expectExceptionMessage('cannot be found');
         $app = $this->getMockForAbstractClass(BaseApplication::class, [$this->path]);
         $app->addPlugin('SomethingBad');
+        $plugin = $app->getPlugins()->get('SomethingBad');
+        $this->assertInstanceOf(BasePlugin::class, $plugin);
     }
 
+    /**
+     * Ensure that plugin interfaces are implemented.
+     */
     public function testAddPluginBadClass()
     {
         $this->expectException(InvalidArgumentException::class);