I was reading an article about parameterized middleware, and I was thinking what a strange syntax for parameters. It looks like a typo in a static method call, SomeClass::class.'::someMethod'
.
And what if the parameter is a bit more complicated than a string, for example a basic enum.
Getting into the water
So I looked at the code of the middleware method.
/**
* Get or set the middlewares attached to the route.
*
* @param array|string|null $middleware
* @return $this|array
*/
public function middleware($middleware = null)
{
if (is_null($middleware)) {
return (array) ($this->action['middleware'] ?? []);
}
if (! is_array($middleware)) {
$middleware = func_get_args();
}
foreach ($middleware as $index => $value) {
$middleware[$index] = (string) $value;
}
$this->action['middleware'] = array_merge(
(array) ($this->action['middleware'] ?? []), $middleware
);
return $this;
}
This showed me that whatever argument you add to the method, your middleware definition always gets converted to a string.
Why is this happening?
Putting on the oxygen tank and a diving mask
I turned on xdebug and added a breakpoint in my middleware class handle method. When I browsed through the call stack, I found the Router runRouteWithinStack method that creates a Pipeline.
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(fn ($request) => $this->prepareResponse(
$request, $route->run()
));
The then method of the Pipeline class loops over the added middleware.
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
And in the carry method there is a check that allows the split of the class and the parameters.
} elseif (! is_object($pipe)) {
[$name, $parameters] = $this->parsePipeString($pipe);
// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
}
So now we arrived at the method that does the actual splitting, parsePipeString.
protected function parsePipeString($pipe)
{
[$name, $parameters] = array_pad(explode(':', $pipe, 2), 2, []);
if (is_string($parameters)) {
$parameters = explode(',', $parameters);
}
return [$name, $parameters];
}
The extra condition that this method brings is that the parameters are identified with a comma. So SomeClass::class.':[a,b]'
will result in ['[a','b]']
.
During this dive I found three places where the string type is enforced
- the Route middleware method
- the Pipeline carry method, because it implicitly expect a string
- the Pipeline parsePipeString method
Back on dry land
What if the middleware method did accept a class like;
final readonly class PipeLineClass {
public function __constructor(public string $class, public mixed $options) {
$this->validate($class);
}
private function validate(string $class) {
if(!is_callable($class)) {
throw new \Exception("$class is not callable.");
}
}
}
In the Pipeline carry method the checks could be changed from
if (is_callable($pipe)) {
// code
} elseif (! is_object($pipe)) {
// code
} else {
// code
}
to
if($pipe instanceof PipelineClass){
// code
} elseif (is_callable($pipe)) {
// code
} elseif (is_string($pipe)) {
// code
} else {
// code
}
There are a few other places in the code where the class and parameters syntax is checked. So they need to change too.
I'm wondering if the parameter syntax is enough for the people that use the feature, or are they creating their own parameter syntax to make it possible to provide more information?
Top comments (0)