Skip to content

Commit 3ce7a52

Browse files
authored
Merge pull request #5128 from kenjis/add-multiple-filters
Multiple filters for a route and classname filter
2 parents dc55361 + 3ee6632 commit 3ce7a52

File tree

11 files changed

+244
-23
lines changed

11 files changed

+244
-23
lines changed

app/Config/Feature.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Config;
4+
5+
use CodeIgniter\Config\BaseConfig;
6+
7+
/**
8+
* Enable/disable backward compatibility breaking features.
9+
*/
10+
class Feature extends BaseConfig
11+
{
12+
/**
13+
* Enable multiple filters for a route or not
14+
*
15+
* If you enable this:
16+
* - CodeIgniter\CodeIgniter::handleRequest() uses:
17+
* - CodeIgniter\Filters\Filters::enableFilters(), instead of enableFilter()
18+
* - CodeIgniter\CodeIgniter::tryToRouteIt() uses:
19+
* - CodeIgniter\Router\Router::getFilters(), instead of getFilter()
20+
* - CodeIgniter\Router\Router::handle() uses:
21+
* - property $filtersInfo, instead of $filterInfo
22+
* - CodeIgniter\Router\RouteCollection::getFiltersForRoute(), instead of getFilterForRoute()
23+
*
24+
* @var bool
25+
*/
26+
public $multipleFilters = false;
27+
}

phpstan.neon.dist

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

system/CodeIgniter.php

+17-4
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,15 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
364364
// If any filters were specified within the routes file,
365365
// we need to ensure it's active for the current request
366366
if ($routeFilter !== null) {
367-
$filters->enableFilter($routeFilter, 'before');
368-
$filters->enableFilter($routeFilter, 'after');
367+
$multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
368+
if ($multipleFiltersEnabled) {
369+
$filters->enableFilters($routeFilter, 'before');
370+
$filters->enableFilters($routeFilter, 'after');
371+
} else {
372+
// for backward compatibility
373+
$filters->enableFilter($routeFilter, 'before');
374+
$filters->enableFilter($routeFilter, 'after');
375+
}
369376
}
370377

371378
$uri = $this->determinePath();
@@ -690,7 +697,7 @@ public function displayPerformanceMetrics(string $output): string
690697
*
691698
* @throws RedirectException
692699
*
693-
* @return string|null
700+
* @return string|string[]|null
694701
*/
695702
protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
696703
{
@@ -719,7 +726,13 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
719726

720727
$this->benchmark->stop('routing');
721728

722-
return $this->router->getFilter();
729+
// for backward compatibility
730+
$multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
731+
if (! $multipleFiltersEnabled) {
732+
return $this->router->getFilter();
733+
}
734+
735+
return $this->router->getFilters();
723736
}
724737

725738
/**

system/Filters/Filters.php

+23-1
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ public function addFilter(string $class, ?string $alias = null, string $when = '
319319
* are passed to the filter when executed.
320320
*
321321
* @return Filters
322+
*
323+
* @deprecated Use enableFilters(). This method will be private.
322324
*/
323325
public function enableFilter(string $name, string $when = 'before')
324326
{
@@ -334,7 +336,9 @@ public function enableFilter(string $name, string $when = 'before')
334336
$this->arguments[$name] = $params;
335337
}
336338

337-
if (! array_key_exists($name, $this->config->aliases)) {
339+
if (class_exists($name)) {
340+
$this->config->aliases[$name] = $name;
341+
} elseif (! array_key_exists($name, $this->config->aliases)) {
338342
throw FilterException::forNoAlias($name);
339343
}
340344

@@ -352,6 +356,24 @@ public function enableFilter(string $name, string $when = 'before')
352356
return $this;
353357
}
354358

359+
/**
360+
* Ensures that specific filters is on and enabled for the current request.
361+
*
362+
* Filters can have "arguments". This is done by placing a colon immediately
363+
* after the filter name, followed by a comma-separated list of arguments that
364+
* are passed to the filter when executed.
365+
*
366+
* @return Filters
367+
*/
368+
public function enableFilters(array $names, string $when = 'before')
369+
{
370+
foreach ($names as $filter) {
371+
$this->enableFilter($filter, $when);
372+
}
373+
374+
return $this;
375+
}
376+
355377
/**
356378
* Returns the arguments for a specified key, or all.
357379
*

system/Router/RouteCollection.php

+34-11
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ public function map(array $routes = [], ?array $options = null): RouteCollection
512512
* Example:
513513
* $routes->add('news', 'Posts::index');
514514
*
515-
* @param array|string $to
515+
* @param array|Closure|string $to
516516
*/
517517
public function add(string $from, $to, ?array $options = null): RouteCollectionInterface
518518
{
@@ -821,7 +821,7 @@ public function presenter(string $name, ?array $options = null): RouteCollection
821821
* Example:
822822
* $route->match( ['get', 'post'], 'users/(:num)', 'users/$1);
823823
*
824-
* @param array|string $to
824+
* @param array|Closure|string $to
825825
*/
826826
public function match(array $verbs = [], string $from = '', $to = '', ?array $options = null): RouteCollectionInterface
827827
{
@@ -841,7 +841,7 @@ public function match(array $verbs = [], string $from = '', $to = '', ?array $op
841841
/**
842842
* Specifies a route that is only available to GET requests.
843843
*
844-
* @param array|string $to
844+
* @param array|Closure|string $to
845845
*/
846846
public function get(string $from, $to, ?array $options = null): RouteCollectionInterface
847847
{
@@ -853,7 +853,7 @@ public function get(string $from, $to, ?array $options = null): RouteCollectionI
853853
/**
854854
* Specifies a route that is only available to POST requests.
855855
*
856-
* @param array|string $to
856+
* @param array|Closure|string $to
857857
*/
858858
public function post(string $from, $to, ?array $options = null): RouteCollectionInterface
859859
{
@@ -865,7 +865,7 @@ public function post(string $from, $to, ?array $options = null): RouteCollection
865865
/**
866866
* Specifies a route that is only available to PUT requests.
867867
*
868-
* @param array|string $to
868+
* @param array|Closure|string $to
869869
*/
870870
public function put(string $from, $to, ?array $options = null): RouteCollectionInterface
871871
{
@@ -877,7 +877,7 @@ public function put(string $from, $to, ?array $options = null): RouteCollectionI
877877
/**
878878
* Specifies a route that is only available to DELETE requests.
879879
*
880-
* @param array|string $to
880+
* @param array|Closure|string $to
881881
*/
882882
public function delete(string $from, $to, ?array $options = null): RouteCollectionInterface
883883
{
@@ -889,7 +889,7 @@ public function delete(string $from, $to, ?array $options = null): RouteCollecti
889889
/**
890890
* Specifies a route that is only available to HEAD requests.
891891
*
892-
* @param array|string $to
892+
* @param array|Closure|string $to
893893
*/
894894
public function head(string $from, $to, ?array $options = null): RouteCollectionInterface
895895
{
@@ -901,7 +901,7 @@ public function head(string $from, $to, ?array $options = null): RouteCollection
901901
/**
902902
* Specifies a route that is only available to PATCH requests.
903903
*
904-
* @param array|string $to
904+
* @param array|Closure|string $to
905905
*/
906906
public function patch(string $from, $to, ?array $options = null): RouteCollectionInterface
907907
{
@@ -913,7 +913,7 @@ public function patch(string $from, $to, ?array $options = null): RouteCollectio
913913
/**
914914
* Specifies a route that is only available to OPTIONS requests.
915915
*
916-
* @param array|string $to
916+
* @param array|Closure|string $to
917917
*/
918918
public function options(string $from, $to, ?array $options = null): RouteCollectionInterface
919919
{
@@ -925,7 +925,7 @@ public function options(string $from, $to, ?array $options = null): RouteCollect
925925
/**
926926
* Specifies a route that is only available to command-line requests.
927927
*
928-
* @param array|string $to
928+
* @param array|Closure|string $to
929929
*/
930930
public function cli(string $from, $to, ?array $options = null): RouteCollectionInterface
931931
{
@@ -1040,6 +1040,8 @@ public function isFiltered(string $search, ?string $verb = null): bool
10401040
* 'role:admin,manager'
10411041
*
10421042
* has a filter of "role", with parameters of ['admin', 'manager'].
1043+
*
1044+
* @deprecated Use getFiltersForRoute()
10431045
*/
10441046
public function getFilterForRoute(string $search, ?string $verb = null): string
10451047
{
@@ -1048,6 +1050,27 @@ public function getFilterForRoute(string $search, ?string $verb = null): string
10481050
return $options[$search]['filter'] ?? '';
10491051
}
10501052

1053+
/**
1054+
* Returns the filters that should be applied for a single route, along
1055+
* with any parameters it might have. Parameters are found by splitting
1056+
* the parameter name on a colon to separate the filter name from the parameter list,
1057+
* and the splitting the result on commas. So:
1058+
*
1059+
* 'role:admin,manager'
1060+
*
1061+
* has a filter of "role", with parameters of ['admin', 'manager'].
1062+
*/
1063+
public function getFiltersForRoute(string $search, ?string $verb = null): array
1064+
{
1065+
$options = $this->loadRoutesOptions($verb);
1066+
1067+
if (is_string($options[$search]['filter'])) {
1068+
return [$options[$search]['filter']];
1069+
}
1070+
1071+
return $options[$search]['filter'] ?? [];
1072+
}
1073+
10511074
/**
10521075
* Given a
10531076
*
@@ -1083,7 +1106,7 @@ protected function fillRouteParams(string $from, ?array $params = null): string
10831106
* the request method(s) that this route will work for. They can be separated
10841107
* by a pipe character "|" if there is more than one.
10851108
*
1086-
* @param array|string $to
1109+
* @param array|Closure|string $to
10871110
*/
10881111
protected function create(string $verb, string $from, $to, ?array $options = null)
10891112
{

system/Router/RouteCollectionInterface.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ interface RouteCollectionInterface
2828
/**
2929
* Adds a single route to the collection.
3030
*
31-
* @param array|string $to
32-
* @param array $options
31+
* @param array|Closure|string $to
32+
* @param array $options
3333
*
3434
* @return mixed
3535
*/

system/Router/Router.php

+29-1
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,19 @@ class Router implements RouterInterface
9999
* if the matched route should be filtered.
100100
*
101101
* @var string|null
102+
*
103+
* @deprecated Use $filtersInfo
102104
*/
103105
protected $filterInfo;
104106

107+
/**
108+
* The filter info from Route Collection
109+
* if the matched route should be filtered.
110+
*
111+
* @var string[]
112+
*/
113+
protected $filtersInfo = [];
114+
105115
/**
106116
* Stores a reference to the RouteCollection object.
107117
*
@@ -144,7 +154,13 @@ public function handle(?string $uri = null)
144154

145155
if ($this->checkRoutes($uri)) {
146156
if ($this->collection->isFiltered($this->matchedRoute[0])) {
147-
$this->filterInfo = $this->collection->getFilterForRoute($this->matchedRoute[0]);
157+
$multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
158+
if ($multipleFiltersEnabled) {
159+
$this->filtersInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
160+
} else {
161+
// for backward compatibility
162+
$this->filterInfo = $this->collection->getFilterForRoute($this->matchedRoute[0]);
163+
}
148164
}
149165

150166
return $this->controller;
@@ -166,12 +182,24 @@ public function handle(?string $uri = null)
166182
* Returns the filter info for the matched route, if any.
167183
*
168184
* @return string
185+
*
186+
* @deprecated Use getFilters()
169187
*/
170188
public function getFilter()
171189
{
172190
return $this->filterInfo;
173191
}
174192

193+
/**
194+
* Returns the filter info for the matched route, if any.
195+
*
196+
* @return string[]
197+
*/
198+
public function getFilters(): array
199+
{
200+
return $this->filtersInfo;
201+
}
202+
175203
/**
176204
* Returns the name of the matched controller.
177205
*

tests/system/CodeIgniterTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use CodeIgniter\Test\Mock\MockCodeIgniter;
1818
use Config\App;
1919
use Config\Modules;
20+
use Tests\Support\Filters\Customfilter;
2021

2122
/**
2223
* @backupGlobals enabled
@@ -172,6 +173,29 @@ public function testControllersCanReturnResponseObject()
172173
$this->assertStringContainsString("You want to see 'about' page.", $output);
173174
}
174175

176+
public function testControllersRunFilterByClassName()
177+
{
178+
$_SERVER['argv'] = ['index.php', 'pages/about'];
179+
$_SERVER['argc'] = 2;
180+
181+
$_SERVER['REQUEST_URI'] = '/pages/about';
182+
183+
// Inject mock router.
184+
$routes = Services::routes();
185+
$routes->add('pages/about', static function () {
186+
return Services::request()->url;
187+
}, ['filter' => Customfilter::class]);
188+
189+
$router = Services::router($routes, Services::request());
190+
Services::injectMock('router', $router);
191+
192+
ob_start();
193+
$this->codeigniter->useSafeOutput(true)->run();
194+
$output = ob_get_clean();
195+
196+
$this->assertStringContainsString('http://hellowworld.com', $output);
197+
}
198+
175199
public function testResponseConfigEmpty()
176200
{
177201
$_SERVER['argv'] = ['index.php', '/'];

0 commit comments

Comments
 (0)