DEV Community

david duymelinck
david duymelinck

Posted on

Laravel parameterized middleware a deep dive

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;
    }
Enter fullscreen mode Exit fullscreen mode

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()
                        ));
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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);
                    }
Enter fullscreen mode Exit fullscreen mode

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];
    }
Enter fullscreen mode Exit fullscreen mode

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.");
      }    
   }
}
Enter fullscreen mode Exit fullscreen mode

In the Pipeline carry method the checks could be changed from

if (is_callable($pipe)) {
// code
} elseif (! is_object($pipe)) {
// code
} else {
// code
}
Enter fullscreen mode Exit fullscreen mode

to

if($pipe instanceof PipelineClass){
// code
} elseif (is_callable($pipe)) {
// code
} elseif (is_string($pipe)) {
// code
} else {
// code
}
Enter fullscreen mode Exit fullscreen mode

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)