Merge pull request #5128 from kenjis/add-multiple-filters

Multiple filters for a route and classname filter
This commit is contained in:
Lonnie Ezell 2021-10-04 22:29:29 -05:00 committed by GitHub
commit 3ce7a52e77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 244 additions and 23 deletions

27
app/Config/Feature.php Normal file
View File

@ -0,0 +1,27 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* Enable/disable backward compatibility breaking features.
*/
class Feature extends BaseConfig
{
/**
* Enable multiple filters for a route or not
*
* If you enable this:
* - CodeIgniter\CodeIgniter::handleRequest() uses:
* - CodeIgniter\Filters\Filters::enableFilters(), instead of enableFilter()
* - CodeIgniter\CodeIgniter::tryToRouteIt() uses:
* - CodeIgniter\Router\Router::getFilters(), instead of getFilter()
* - CodeIgniter\Router\Router::handle() uses:
* - property $filtersInfo, instead of $filterInfo
* - CodeIgniter\Router\RouteCollection::getFiltersForRoute(), instead of getFilterForRoute()
*
* @var bool
*/
public $multipleFilters = false;
}

View File

@ -32,7 +32,7 @@ parameters:
- '#Access to an undefined property CodeIgniter\\Database\\BaseConnection::\$mysqli|\$schema#'
- '#Access to an undefined property CodeIgniter\\Database\\ConnectionInterface::(\$DBDriver|\$connID|\$likeEscapeStr|\$likeEscapeChar|\$escapeChar|\$protectIdentifiers|\$schema)#'
- '#Call to an undefined method CodeIgniter\\Database\\BaseConnection::_(disable|enable)ForeignKeyChecks\(\)#'
- '#Call to an undefined method CodeIgniter\\Router\\RouteCollectionInterface::(getDefaultNamespace|isFiltered|getFilterForRoute|getRoutesOptions)\(\)#'
- '#Call to an undefined method CodeIgniter\\Router\\RouteCollectionInterface::(getDefaultNamespace|isFiltered|getFilterForRoute|getFiltersForRoute|getRoutesOptions)\(\)#'
- '#Cannot access property [\$a-z_]+ on ((bool\|)?object\|resource)#'
- '#Cannot call method [a-zA-Z_]+\(\) on ((bool\|)?object\|resource)#'
- '#Method CodeIgniter\\Router\\RouteCollectionInterface::getRoutes\(\) invoked with 1 parameter, 0 required#'

View File

@ -364,8 +364,15 @@ class CodeIgniter
// If any filters were specified within the routes file,
// we need to ensure it's active for the current request
if ($routeFilter !== null) {
$filters->enableFilter($routeFilter, 'before');
$filters->enableFilter($routeFilter, 'after');
$multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
if ($multipleFiltersEnabled) {
$filters->enableFilters($routeFilter, 'before');
$filters->enableFilters($routeFilter, 'after');
} else {
// for backward compatibility
$filters->enableFilter($routeFilter, 'before');
$filters->enableFilter($routeFilter, 'after');
}
}
$uri = $this->determinePath();
@ -690,7 +697,7 @@ class CodeIgniter
*
* @throws RedirectException
*
* @return string|null
* @return string|string[]|null
*/
protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
{
@ -719,7 +726,13 @@ class CodeIgniter
$this->benchmark->stop('routing');
return $this->router->getFilter();
// for backward compatibility
$multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
if (! $multipleFiltersEnabled) {
return $this->router->getFilter();
}
return $this->router->getFilters();
}
/**

View File

@ -319,6 +319,8 @@ class Filters
* are passed to the filter when executed.
*
* @return Filters
*
* @deprecated Use enableFilters(). This method will be private.
*/
public function enableFilter(string $name, string $when = 'before')
{
@ -334,7 +336,9 @@ class Filters
$this->arguments[$name] = $params;
}
if (! array_key_exists($name, $this->config->aliases)) {
if (class_exists($name)) {
$this->config->aliases[$name] = $name;
} elseif (! array_key_exists($name, $this->config->aliases)) {
throw FilterException::forNoAlias($name);
}
@ -352,6 +356,24 @@ class Filters
return $this;
}
/**
* Ensures that specific filters is on and enabled for the current request.
*
* Filters can have "arguments". This is done by placing a colon immediately
* after the filter name, followed by a comma-separated list of arguments that
* are passed to the filter when executed.
*
* @return Filters
*/
public function enableFilters(array $names, string $when = 'before')
{
foreach ($names as $filter) {
$this->enableFilter($filter, $when);
}
return $this;
}
/**
* Returns the arguments for a specified key, or all.
*

View File

@ -512,7 +512,7 @@ class RouteCollection implements RouteCollectionInterface
* Example:
* $routes->add('news', 'Posts::index');
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function add(string $from, $to, ?array $options = null): RouteCollectionInterface
{
@ -821,7 +821,7 @@ class RouteCollection implements RouteCollectionInterface
* Example:
* $route->match( ['get', 'post'], 'users/(:num)', 'users/$1);
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function match(array $verbs = [], string $from = '', $to = '', ?array $options = null): RouteCollectionInterface
{
@ -841,7 +841,7 @@ class RouteCollection implements RouteCollectionInterface
/**
* Specifies a route that is only available to GET requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function get(string $from, $to, ?array $options = null): RouteCollectionInterface
{
@ -853,7 +853,7 @@ class RouteCollection implements RouteCollectionInterface
/**
* Specifies a route that is only available to POST requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function post(string $from, $to, ?array $options = null): RouteCollectionInterface
{
@ -865,7 +865,7 @@ class RouteCollection implements RouteCollectionInterface
/**
* Specifies a route that is only available to PUT requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function put(string $from, $to, ?array $options = null): RouteCollectionInterface
{
@ -877,7 +877,7 @@ class RouteCollection implements RouteCollectionInterface
/**
* Specifies a route that is only available to DELETE requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function delete(string $from, $to, ?array $options = null): RouteCollectionInterface
{
@ -889,7 +889,7 @@ class RouteCollection implements RouteCollectionInterface
/**
* Specifies a route that is only available to HEAD requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function head(string $from, $to, ?array $options = null): RouteCollectionInterface
{
@ -901,7 +901,7 @@ class RouteCollection implements RouteCollectionInterface
/**
* Specifies a route that is only available to PATCH requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function patch(string $from, $to, ?array $options = null): RouteCollectionInterface
{
@ -913,7 +913,7 @@ class RouteCollection implements RouteCollectionInterface
/**
* Specifies a route that is only available to OPTIONS requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function options(string $from, $to, ?array $options = null): RouteCollectionInterface
{
@ -925,7 +925,7 @@ class RouteCollection implements RouteCollectionInterface
/**
* Specifies a route that is only available to command-line requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function cli(string $from, $to, ?array $options = null): RouteCollectionInterface
{
@ -1040,6 +1040,8 @@ class RouteCollection implements RouteCollectionInterface
* 'role:admin,manager'
*
* has a filter of "role", with parameters of ['admin', 'manager'].
*
* @deprecated Use getFiltersForRoute()
*/
public function getFilterForRoute(string $search, ?string $verb = null): string
{
@ -1048,6 +1050,27 @@ class RouteCollection implements RouteCollectionInterface
return $options[$search]['filter'] ?? '';
}
/**
* Returns the filters that should be applied for a single route, along
* with any parameters it might have. Parameters are found by splitting
* the parameter name on a colon to separate the filter name from the parameter list,
* and the splitting the result on commas. So:
*
* 'role:admin,manager'
*
* has a filter of "role", with parameters of ['admin', 'manager'].
*/
public function getFiltersForRoute(string $search, ?string $verb = null): array
{
$options = $this->loadRoutesOptions($verb);
if (is_string($options[$search]['filter'])) {
return [$options[$search]['filter']];
}
return $options[$search]['filter'] ?? [];
}
/**
* Given a
*
@ -1083,7 +1106,7 @@ class RouteCollection implements RouteCollectionInterface
* the request method(s) that this route will work for. They can be separated
* by a pipe character "|" if there is more than one.
*
* @param array|string $to
* @param array|Closure|string $to
*/
protected function create(string $verb, string $from, $to, ?array $options = null)
{

View File

@ -28,8 +28,8 @@ interface RouteCollectionInterface
/**
* Adds a single route to the collection.
*
* @param array|string $to
* @param array $options
* @param array|Closure|string $to
* @param array $options
*
* @return mixed
*/

View File

@ -99,9 +99,19 @@ class Router implements RouterInterface
* if the matched route should be filtered.
*
* @var string|null
*
* @deprecated Use $filtersInfo
*/
protected $filterInfo;
/**
* The filter info from Route Collection
* if the matched route should be filtered.
*
* @var string[]
*/
protected $filtersInfo = [];
/**
* Stores a reference to the RouteCollection object.
*
@ -144,7 +154,13 @@ class Router implements RouterInterface
if ($this->checkRoutes($uri)) {
if ($this->collection->isFiltered($this->matchedRoute[0])) {
$this->filterInfo = $this->collection->getFilterForRoute($this->matchedRoute[0]);
$multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
if ($multipleFiltersEnabled) {
$this->filtersInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
} else {
// for backward compatibility
$this->filterInfo = $this->collection->getFilterForRoute($this->matchedRoute[0]);
}
}
return $this->controller;
@ -166,12 +182,24 @@ class Router implements RouterInterface
* Returns the filter info for the matched route, if any.
*
* @return string
*
* @deprecated Use getFilters()
*/
public function getFilter()
{
return $this->filterInfo;
}
/**
* Returns the filter info for the matched route, if any.
*
* @return string[]
*/
public function getFilters(): array
{
return $this->filtersInfo;
}
/**
* Returns the name of the matched controller.
*

View File

@ -17,6 +17,7 @@ use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockCodeIgniter;
use Config\App;
use Config\Modules;
use Tests\Support\Filters\Customfilter;
/**
* @backupGlobals enabled
@ -172,6 +173,29 @@ final class CodeIgniterTest extends CIUnitTestCase
$this->assertStringContainsString("You want to see 'about' page.", $output);
}
public function testControllersRunFilterByClassName()
{
$_SERVER['argv'] = ['index.php', 'pages/about'];
$_SERVER['argc'] = 2;
$_SERVER['REQUEST_URI'] = '/pages/about';
// Inject mock router.
$routes = Services::routes();
$routes->add('pages/about', static function () {
return Services::request()->url;
}, ['filter' => Customfilter::class]);
$router = Services::router($routes, Services::request());
Services::injectMock('router', $router);
ob_start();
$this->codeigniter->useSafeOutput(true)->run();
$output = ob_get_clean();
$this->assertStringContainsString('http://hellowworld.com', $output);
}
public function testResponseConfigEmpty()
{
$_SERVER['argv'] = ['index.php', '/'];

View File

@ -16,6 +16,7 @@ use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Modules;
use Tests\Support\Filters\Customfilter;
/**
* @internal
@ -529,6 +530,39 @@ final class RouterTest extends CIUnitTestCase
$this->assertSame('api-auth', $router->getFilter());
}
public function testRouteWorksWithClassnameFilter()
{
$collection = $this->collection;
$collection->add('foo', 'TestController::foo', ['filter' => Customfilter::class]);
$router = new Router($collection, $this->request);
$router->handle('foo');
$this->assertSame('\TestController', $router->controllerName());
$this->assertSame('foo', $router->methodName());
$this->assertSame('Tests\Support\Filters\Customfilter', $router->getFilter());
}
public function testRouteWorksWithMultipleFilters()
{
$feature = config('Feature');
$feature->multipleFilters = true;
$collection = $this->collection;
$collection->add('foo', 'TestController::foo', ['filter' => ['filter1', 'filter2:param']]);
$router = new Router($collection, $this->request);
$router->handle('foo');
$this->assertSame('\TestController', $router->controllerName());
$this->assertSame('foo', $router->methodName());
$this->assertSame(['filter1', 'filter2:param'], $router->getFilters());
$feature->multipleFilters = false;
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1240
*/

View File

@ -352,15 +352,37 @@ can modify the generated routes, or further restrict them. The ``$options`` arra
Applying Filters
----------------
You can alter the behavior of specific routes by supplying a filter to run before or after the controller. This is especially handy during authentication or api logging::
You can alter the behavior of specific routes by supplying filters to run before or after the controller. This is especially handy during authentication or api logging.
The value for the filter can be a string or an array of strings:
* matching the aliases defined in ``app/Config/Filters.php``.
* filter classnames
See `Controller filters <filters.html>`_ for more information on setting up filters.
**Alias filter**
You specify an alias defined in ``app/Config/Filters.php`` for the filter value::
$routes->add('admin',' AdminController::index', ['filter' => 'admin-auth']);
The value for the filter must match one of the aliases defined within ``app/Config/Filters.php``. You may also supply arguments to be passed to the filter's ``before()`` and ``after()`` methods::
You may also supply arguments to be passed to the alias filter's ``before()`` and ``after()`` methods::
$routes->add('users/delete/(:segment)', 'AdminController::index', ['filter' => 'admin-auth:dual,noreturn']);
See `Controller filters <filters.html>`_ for more information on setting up filters.
**Classname filter**
You specify a filter classname for the filter value::
$routes->add('admin',' AdminController::index', ['filter' => \App\Filters\SomeFilter::class]);
**Multiple filters**
.. important:: *Multiple filters* is disabled by default. Because it breaks backward compatibility. If you want to use it, you need to configure. See *Multiple filters for a route* in :doc:`/installation/upgrade_415` for the details.
You specify an array for the filter value::
$routes->add('admin',' AdminController::index', ['filter' => ['admin-auth', \App\Filters\SomeFilter::class]]);
Assigning Namespace
-------------------

View File

@ -25,3 +25,31 @@ Update the definition of the session table. See the :doc:`/libraries/sessions` f
The change was introduced in v4.1.2. But due to `a bug <https://github.com/codeigniter4/CodeIgniter4/issues/4807>`_,
the DatabaseHandler Driver did not work properly.
**Multiple filters for a route**
A new feature to set multiple filters for a route.
.. important:: This feature is disabled by default. Because it breaks backward compatibility.
If you want to use this, you need to set the property ``$multipleFilters`` ``true`` in ``app/Config/Feature.php``.
If you enable it:
- ``CodeIgniter\CodeIgniter::handleRequest()`` uses
- ``CodeIgniter\Filters\Filters::enableFilters()``, instead of ``enableFilter()``
- ``CodeIgniter\CodeIgniter::tryToRouteIt()`` uses
- ``CodeIgniter\Router\Router::getFilters()``, instead of ``getFilter()``
- ``CodeIgniter\Router\Router::handle()`` uses
- the property ``$filtersInfo``, instead of ``$filterInfo``
- ``CodeIgniter\Router\RouteCollection::getFiltersForRoute()``, instead of ``getFilterForRoute()``
If you extended the above classes, then you need to change them.
The following methods and a property have been deprecated:
- ``CodeIgniter\Filters\Filters::enableFilter()``
- ``CodeIgniter\Router\Router::getFilter()``
- ``CodeIgniter\Router\RouteCollection::getFilterForRoute()``
- ``CodeIgniter\Router\RouteCollection``'s property ``$filterInfo``
See *Applying Filters* in :doc:`Routing </incoming/routing>` for the functionality.