Plugin Improvements - markstory/cakephp GitHub Wiki

Problem

Plugins have no central/single place to define their behavior around bootstrapping, routes and CLI commands. We rely on plugin users to copy the correct Plugin::load() call from plugin README files resulting in an error prone process.

With the creation of the Application classes the differences between applications and plugins has grown more visible. Plugins initialization has no easy way to leverage command classes, and aliased commands. Furthermore, it will be very difficult to amend plugins to use a dependency container in the future as plugins are created from snippet files and global statics.

Goals of the Solution

The new plugin initialization should aim to solve the above problems, and have the following properties:

  • Should work well with the Application class and be compatible with DI containers defined in the application, or by CakePHP in the future.
  • Should avoid adding new global static features.
  • Backwards compatibility with Plugin methods. This 'facade' class is a useful shortcut for accessing which plugins are loaded.
  • Enable loading plugins with a single method call.
  • Allow plugins to load configuration, routes, add console commands, and middleware automatically.
  • Allow application developers to disable all of the above and handle those resource types manually.

Plugin App Classes

Encapsulating the default behavior of a plugin can be supported by creating plugin 'application' classes. Plugin app classes can be added to the host application via a inside its bootstrap() hook.

Plugin classes can define the following hook methods:

  • bootstrap() Load plugin configuration, constants and global event listeners. Called after application bootstrapping is complete.
  • console(CommandCollection $commands) Add console commands for the plugin. Called after application commands are added to the CommandCollection.
  • middleware(MiddlewareStack $middleware) Add middleware for the plugin. Called after the application middleware hook is called.
  • routes(RouterBuilder $routes) Define routes for the plugin. Called after the application routes have been defined.

By implementing any of these hook methods, a plugin will have them automatically invoked by the application at the appropriate time.

Loading Plugins

Plugins are added into the application during the application bootstrap(). You can load plugins by object, or classname:

// In Application::bootstrap()

// By classname - Accepts all defaults.
$this->addPlugin(ExamplePlugin::class)

// By object, allows customization
$plugin = new OtherPlugin($this);
$this->addPlugin($plugin)

Once loaded application plugins are inserted into Cake\Core\Plugin for backwards compatibility. They are also added to $application->plugins which is a collection object similar Cake\Console\CommandCollection providing accessor methods and an iterator. It will also support fetching plugins that match given method support. For example plugins with routing enabled:

// In the scope of an application object
foreach ($this->plugins->with('routes') as $plugin) {
    $plugin->routes($routeBuilder);
}

Taking Control of Plugin Setup

The PluginApp base class provides a suite of configuration methods that allow the application to disable specific hooks on a plugin as needed:

$plugin = new ExamplePlugin($application);

// Disable hooks
$plugin->disableRoutes();
$plugin->disableMiddleware();
$plugin->disableConsole();
$plugin->disableBootstrap();

$application->addPlugin($plugin);

Plugins also accept an array of options in their constructor to disable hooks:

$plugin = new ExamplePlugin($this, [
    'routes' => false,
    'middleware' => false,
    'console' => false
]);

Customizing Plugin Routes

Sometimes applications need to apply additional routing scopes around plugin routes, or place them higher in the routes file. After disabling automatic route loading with disableRoutes() a plugin's routes can be loaded in the application routes.

// In Application::routes() or config/routes.php

$routes->scope('/plugs', function ($routes) {
    $routes->loadPlugin('Example');
});

This leverages the existing plugin route loading method on RouteBuilder, and the static Plugin class to gain access to the correct plugin and invoke it's routes hook. In 4.x the RouteBuilder can be provided the collection of loaded plugins.