Skip to content

Commit

Permalink
Fix duplicate route errors when using loadPlugin()
Browse files Browse the repository at this point in the history
Use the plugin object to load plugin routes. This allows us to disable
the hook method so that it is not triggered again.

There is still a scenario where a plugin loads routes from another
plugin after that plugin's routes() hook has already been called.
I'm going to count that as an unlikely scenario.

Refs #12130
  • Loading branch information
markstory committed Jun 16, 2018
1 parent 920aee3 commit d8bba8b
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 17 deletions.
39 changes: 28 additions & 11 deletions src/Routing/RouteBuilder.php
Expand Up @@ -602,28 +602,45 @@ protected function _methodRoute($method, $template, $target, $name)
* the current RouteBuilder instance.
*
* @param string $name The plugin name
* @param string $file The routes file to load. Defaults to `routes.php`
* @param string $file The routes file to load. Defaults to `routes.php`. This parameter
* is deprecated and will be removed in 4.0
* @return void
* @throws \Cake\Core\Exception\MissingPluginException When the plugin has not been loaded.
* @throws \InvalidArgumentException When the plugin does not have a routes file.
*/
public function loadPlugin($name, $file = 'routes.php')
{
if (!Plugin::loaded($name)) {
$plugins = Plugin::getCollection();
if (!$plugins->has($name)) {
throw new MissingPluginException(['plugin' => $name]);
}
$plugin = $plugins->get($name);

$path = Plugin::configPath($name) . DIRECTORY_SEPARATOR . $file;
if (!file_exists($path)) {
throw new InvalidArgumentException(sprintf(
'Cannot load routes for the plugin named %s. The %s file does not exist.',
$name,
$path
));
// @deprecated This block should be removed in 4.0
if ($file !== 'routes.php') {
deprecationWarning(
'Loading plugin routes now uses the routes() hook method on the plugin class. ' .
'Loading non-standard files will be removed in 4.0'
);

$path = $plugin->getConfigPath() . DIRECTORY_SEPARATOR . $file;
if (!file_exists($path)) {
throw new InvalidArgumentException(sprintf(
'Cannot load routes for the plugin named %s. The %s file does not exist.',
$name,
$path
));
}

$routes = $this;
include $path;

return;
}
$plugin->routes($this);

$routes = $this;
include $path;
// Disable the routes hook to prevent duplicate route issues.
$plugin->disable('routes');
}

/**
Expand Down
15 changes: 10 additions & 5 deletions tests/TestCase/Routing/RouteBuilderTest.php
Expand Up @@ -1206,11 +1206,13 @@ public function testLoadPluginBadPlugin()
*/
public function testLoadPluginBadFile()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot load routes for the plugin named TestPlugin.');
Plugin::load('TestPlugin');
$routes = new RouteBuilder($this->collection, '/');
$routes->loadPlugin('TestPlugin', 'nope.php');
$this->deprecated(function () {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot load routes for the plugin named TestPlugin.');
Plugin::load('TestPlugin');
$routes = new RouteBuilder($this->collection, '/');
$routes->loadPlugin('TestPlugin', 'nope.php');
});
}

/**
Expand All @@ -1225,6 +1227,9 @@ public function testLoadPlugin()
$routes->loadPlugin('TestPlugin');
$this->assertCount(1, $this->collection->routes());
$this->assertNotEmpty($this->collection->parse('/test_plugin', 'GET'));

$plugin = Plugin::getCollection()->get('TestPlugin');
$this->assertFalse($plugin->isEnabled('routes'), 'Hook should be disabled preventing duplicate routes');
}

/**
Expand Down
19 changes: 19 additions & 0 deletions tests/TestCase/TestSuite/IntegrationTestCaseTest.php
Expand Up @@ -15,6 +15,7 @@
namespace Cake\Test\TestCase\TestSuite;

use Cake\Core\Configure;
use Cake\Core\Plugin;
use Cake\Event\EventManager;
use Cake\Http\Response;
use Cake\Routing\DispatcherFactory;
Expand Down Expand Up @@ -52,6 +53,7 @@ public function setUp()
Router::$initialized = true;

$this->useHttpServer(true);
$this->configApplication(Configure::read('App.namespace') . '\Application', null);
DispatcherFactory::clear();
}

Expand Down Expand Up @@ -276,6 +278,23 @@ public function testGetLegacy()
});
}

/**
* Test sending get request and using default `test_app/config/routes.php`.
*
* @return void
*/
public function testGetUsingApplicationWithPluginRoutes()
{
// first clean routes to have Router::$initailized === false
Router::reload();
Plugin::unload();

$this->configApplication(Configure::read('App.namespace') . '\ApplicationWithPluginRoutes', null);

$this->get('/test_plugin');
$this->assertResponseOk();
}

/**
* Test sending get request and using default `test_app/config/routes.php`.
*
Expand Down
6 changes: 5 additions & 1 deletion tests/test_app/Plugin/TestPlugin/config/routes.php
Expand Up @@ -4,5 +4,9 @@
Configure::write('PluginTest.test_plugin.routes', 'loaded plugin routes');

if (isset($routes)) {
$routes->get('/test_plugin', ['controller' => 'TestPlugin', 'plugin' => 'TestPlugin', 'action' => 'index']);
$routes->get(
'/test_plugin',
['controller' => 'TestPlugin', 'plugin' => 'TestPlugin', 'action' => 'index'],
'test_plugin:index'
);
}
48 changes: 48 additions & 0 deletions tests/test_app/TestApp/ApplicationWithPluginRoutes.php
@@ -0,0 +1,48 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.6.6
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace TestApp;

use Cake\Http\BaseApplication;
use Cake\Routing\Middleware\RoutingMiddleware;

class ApplicationWithPluginRoutes extends BaseApplication
{
public function bootstrap()
{
parent::bootstrap();
$this->addPlugin('TestPlugin');
}

public function middleware($middleware)
{
$middleware->add(new RoutingMiddleware($this));

return $middleware;
}

/**
* Routes hook, used for testing with RoutingMiddleware.
*
* @param \Cake\Routing\RouteBuilder $routes
* @return void
*/
public function routes($routes)
{
$routes->scope('/app', function ($routes) {
$routes->connect('/articles', ['controller' => 'Articles']);
});
$routes->loadPlugin('TestPlugin');
}
}

0 comments on commit d8bba8b

Please sign in to comment.