Type hinting $request->getAttribute("woof") / data from middleware

what is the preferred way to pass data from the middleware down to the route handler?

the 3 options i can think of:

  1. middleware+ ->withAttribute("woof",new WoofObject(...)) and in route /** @var $woof WoofObject */ $woof->getAttribute("woof");
    (ie type hinting the variable holding ->getAttribute wherever its used with /** @var - its usually not just a simple new object, im just using it as an eg here)

  2. not use the middleware in a traditional sense but to have an object that accepts the full response and returns the desired object.

  3. use the middleware to load up a container item then use DI in the controller to retrieve it

options 2 and 3 dont seem very nice to me. but im including them as options

what is you guy’s preferred way to pass down objects from middleware to controller? and how do you generally get type hinting on it?

(im currently using option 1, but just wondering if theres a better way to do it)

I would say for simple values (pure data), the $request->withAttribute(…) method is fine. For other, complexer “scenarios”, dependency injection would be a good approach. It always depends on the context.

1 Like

for instance wanting to add a middleware like ServiceMiddleware

class ServiceMiddleware {
    public function __construct(
        protected ServicesRepository $servicesRepository
    ) { }

    public function __invoke(Request $request, RequestHandler $handler): Response {
        $id = $request->getAttribute("PARAMS.service_id");
        if ($id) {
            $request = $request->withAttribute("SERVICE", $this->servicesRepository->get($id));
        }
        
        return $handler->handle($request);

    }
}

(there are more complex ones)

this way every route that needs access to the service just needs a @service_id and middleware and this will be applied.

is “dynamically” adding to the DI container an advised approach? like in the route / groups middleware get the container and add service to it?

im mostly using the container for sort of “global” items that need initializing like the user / tenant / db / config etc.

the getAttribute with a @var above it is pretty solid i think. was just wondering how the rest of the community / pro’s handle it.

The DI container belongs to the outer layer of the application and is “invisible” to your core application (services classes). A middleware belongs to the HTTP layer and usually performs only HTTP-specific tasks.

According to your example, it’s not clear for me what this “service” is actually responsible for. Can you add more context please.

Doesn’t Slim or usage of PHP_DI inject request attributes into the handler?

So, you’d have WoofObject $woof in the signature, and make it nullable if you’re not sure it would be set.

I use thus method, but the again, I’m still on 7.2 fir the project that is using Slim 4 and haven’t played with the type options heavily.

sorry, not too clear on what you mean.

just incase its the DI stuff then sure…

function __construct(
        protected System $system,
        protected Config $config,
        protected Profiler $profiler,
        protected Responder $responder,
        protected JobsRepository $jobsRepository,
        protected HandlersRepository $handlersRepository
    ) {
    }


    public function __invoke(Request $request, Response $response): Response {
        /** @var $job JobModel */
        $job = $request->getAttribute("JOB");
...

using DI either loads up a container instance or creates a new instance if its not in the container for all those items in the __constructor

if you add the “JOB” attribute in via a middleware:

class JobMiddleware {
    public function __construct(
        protected Profiler $profiler,
        protected System $system,
        protected JobsRepository $jobsRepository
    ) {
    }
    public function __invoke(Request $request, RequestHandler $handler): Response {
        $id = $request->getAttribute("PARAMS.job_id");
        if ($id) {
            $request = $request->withAttribute("JOB", $this->jobsRepository->get($id));
        }
        return $handler->handle($request);
    }
}

you arent really looking for either of those.

my OP was more a case of how do others use it?

 /** @var $job JobModel */
 $job = $request->getAttribute("JOB");

does work nicely. was just wondering if there was some super global dockblock or put something in the middleware to auto define type or whatever.

“typing” is definitely <3<3<3<3 php is a whole other language with it in.

Consider this slightly modified middleware from the last comment.

class JobMiddleware {
    public function __construct(
        protected Profiler $profiler,
        protected System $system,
        protected JobsRepository $jobsRepository
    ) {
    }
    public function __invoke(Request $request, RequestHandler $handler): Response {
        $id = $request->getAttribute("PARAMS.job_id");
        if ($id) {
            $request = $request->withAttribute("job", $this->jobsRepository->get($id));
        }
        return $handler->handle($request);
    }
}

Then the controller can be modified as

function __construct(
        protected System $system,
        protected Config $config,
        protected Profiler $profiler,
        protected Responder $responder,
        protected JobsRepository $jobsRepository,
        protected HandlersRepository $handlersRepository
    ) {
    }


    public function __invoke(Request $request, Response $response, JobModel $job): Response {
        // do stuff
        return $response;
     }
...

In the same way that route variables can be injected into the controller method, request attributes can also be injected into the controller method.

does that even work? that would be SOOO cool if it did. but as far as i know the 3rd item in the controller callback is the “params” array.

App\Pages\Jobs\Controllers\JobsController::__invoke(): Argument #3 ($job) must be of type App\Domain\Job\Models\JobModel, array given, called in W:\Projects\portal-admin\api\vendor\slim\slim\Slim\Handlers\Strategies\RequestResponse.php on line 38

hmmmmmmmmmmm which got me digging… theres a RequestResponseNamedArgs strategy… let me try this


edit… php-di slim-bridge seems to be the answer

riiiiiiiight… so i was a total dumbass it seems.

for anyone else… USE THE BRIDGE (if using php-DI)

and my setup… (because knowledge is power right)

Application.php


$containerBuilder = new ContainerBuilder();

$containerBuilder->addDefinitions(__DIR__ . '/Container.php');
$containerBuilder->addDefinitions(__DIR__ . '/Config.php');

$containerBuilder->useAutowiring(true);
$containerBuilder->useAnnotations(false);

$container = $containerBuilder->build();

$app = \DI\Bridge\Slim\Bridge::create($container);

$container->set(ResponseFactoryInterface::class,$app->getResponseFactory());
$container->set(RouteParserInterface::class,$app->getRouteCollector()->getRouteParser());
$container->set(RouteCollectorInterface::class,$app->getRouteCollector());

in the past i had the

App::class => function(
        ContainerInterface $container
    ) {
        AppFactory::setContainer($container);

        return AppFactory::create();
    },

inside the container and in Application.php just $app = $container->get(App::class); but it didnt seem to wanna work… so now ive moved the entire “setup” of slim to the Application.php file and setting up the slim containers here to.

and thats pretty much it… you’re setup and ready for the awesome.

in the middleware

$request = $request->withAttribute("myvariable", new MyVariableObject());

and in the route’s controller

 public function __invoke(Request $request, Response $response, MyVariableObject $myvariable): Response {

somethings to note:

  • the $variable you use in the controller must match the withAttribute("variable"...
  • the typing part doesnt matter other than ide type hinting. so if you did $request = $request->withAttribute("strr", "abc"); you can do public function __invoke(Request $request, Response $response, $strr): Response {. in this case $strr would have the value of “abc”
  • this works with “parameters” as well. so $group->get("[/{job_id}]", Controllers\JobsController::class) and public function __invoke(Request $request, Response $response, $job_id): Response { would give you the @job_id in the $job_id variable

mr @reuben.helms you sir… deserve a beer!

@odan i think you were pointing me in this direction, but i failed to understand it. sorry :frowning:

and i know its probably obvious to others how to use the above and all… oops :frowning: thanks for the help!

Awesome.

The bridge is a part of the furniture for me. I didn’t consider that it might not be installed.

Having to create a middleware for each route is a bit cumbersome and will create unnecessary overhead, right? I think i would prefer a little more complex middleware that uses simple RequestData classes to extract the object for each request.

does anyone have a suggestion how to turn this JobsMiddleware into a more generic middleware so i can use it on any route i want?

to do something like

$app->post('/login', App\Auth\LoginController::class)
    ->setName('login')
    ->addMiddleware(CustomRequestMiddleware(LoginData::class));

and in the controller

public function __invoke(
    Request $request,
    Response $response,
    LoginData $data
): Response
{}

…or maybe there is a different/better way?

you can either add middleware to:

  • your application (every page gets this)
    $app->add(ReplaceMiddleware::class);
  • a group of routes
$section->group("/accounts", function(
            RouteCollectorProxy $group
        ) {
            $group->get("", Controllers\AccountsController::class);
        })
            ->add(AuthMiddleware::class)
        ;
  • an individual route
    like you have / $group->get("", Controllers\AccountsController::class)->add(AuthMiddleware::class)

i have a container for my user like:

AuthToken::class => function(
        ContainerInterface $container
    ) {
        // get the token used for the user
        $profiler = $container->get(Profiler::class)
            ->start(AuthToken::class)
        ;
        $token = new AuthToken();
        $profiler->stop();
        return $token;
    },
    User::class => function(
        ContainerInterface $container
    ) {
        $token = $container->get(AuthToken::class);
        $user = $container->get(CurrentUserRepository::class)
            ->getUserByToken($token->token)
        ;
        return $user;
    },

since i need the user for more than 90% of the pages. then i add the user to the construct of my cotnroller instead

the __invoke (methods) on a controller (when using slim-bridge) gets in the attributes set by middleware

each middleware can add attributes to the request. so like having a middleware for instance AccountMiddleware on the application can add an attribute to the request for instance $request = $request->withAttribute("account", $account);

the controllers method lookup (for an attribute set) happens on the “name” and not the type like the constructor with DI.

so with the above

public function __invoke(
    ...
    LoginData $account
): Response
{}

is the same as

public function __invoke(
    ...
    Something $account
): Response
{}

(other than some type errors obviously). but both of them will return the value of the $request->withAttribute("account", $account);

with middleware you cant use 1 middleware’s result to setup[ another middleware. you can only pass attributes through the stack. and attributes aren’t type set. basically just a multi dimensional array that gets handed to each slice of the onion

so to answer your Q (i think)

  • add all the “application state” stuff to the container. (stuff not necessarily applicable to routes)
    • tenant
    • settings
    $containerBuilder->addDefinitions([
       Profiler::class => function(
            ContainerInterface $container
        ) {
            return new ProfilerCollection();
        },
    ])
    
  • add application middleware (stuff to run on every route) to the app middleware
    • cors
    • inputs
    • replace
    $app->add(ReplaceMiddleware::class);
    $app->add(CorsMiddleware::class);
    $app->add(InputsMiddleware::class);
    
  • add group specific middleware to route groups / per route
    $group->group("/{ACCOUNT}", function(
        RouteCollectorProxy $group
    ){
       $group->get("",...)
    })->add(AccountMiddleware::class)->add(SecondMiddleware::class)
    

i dont think using 1 middleware to call another is the best idea around tbh (or if its even really possible).

this is all AFAIK

1 Like

Just to be clear: I dont want to pass middleware to the middleware. I just want to pass a simple class that i can use inside the middleware to:

  • extract specific expected values from the request.
  • throw an error if its not what i expect.
  • Use those values to build my object from the class passed to the middleware.
  • And finally pass my new shiny shaped data object (which my IDE can understand) along to the controller.

but you say there is no way to pass a simple class to the middleware when adding middleware to a route. This means I will need to have a different middleware class for each and every route i want to use this on.

Something like this (as simplified/pseudo code for now):

The Midlleware

class LoginRequestMiddleware
{
    public function __construct(
        protected Profiler $profiler,
        protected System $system
    ) {}

    public function __invoke(Request $request, RequestHandler $handler): Response
    {
        $data = LoginData::fromRequest($request);

        $request = $request->withAttribute("data", $data);

        return $handler->handle($request);
    }
}

The Data class

class LoginData
{
    public function __construct(
        public string $email,
        public string $password,
    ) {}
    
    public static function fromRequest(Request $request)
    {
        $parsedBody = $request->getParsedBody();
        
        if (!in_array('email', $parsedBody) || !in_array('password', $parsedBody)) {
            throw new \Exception("Invalid request");
        }
        
        return new self(
            $request->getParsedBody()['email'],
            $request->getParsedBody()['password'],
        );
    }
}

The Controller

class LoginController
{
    public function __invoke(
        Request $request,
        ResponseInterface $response,
        LoginData $data,
    ): ResponseInterface
    {
        $email = $data->email; // IDE knows this property exists and is a string
    }
}

But maybe there is another way to simplify the middleware, so i dont need to write a new one for every route i want my custom data in? Preferably i only want to write a simple class like LoginData for every route i want to use this setup in.

ps. I am already using the DI-PHP bridge

EDIT:

ooor maybe something more basic like this:

class LoginController
{
    public function __invoke(
        Request $request,
        ResponseInterface $response,
    ): ResponseInterface
    {
        $parsedBody = $request->getParsedBody();
        $data = LoginData::fromRequest($request);
        
        $email = $data->email; // IDE knows this exists and is a string
    }
}

ok i was making things way too complicated. this is what i have now which seems to be the most simple way to get what i want. It doesnt require any middleware or special setup. just some simple classes:

Controller

final class LoginController
{
    public function __construct(
        private LoginUserAction $LoginUserAction,
    ) {}

    public function __invoke(LoginUserRequest $request, ResponseInterface $response): ResponseInterface
    {
            $data = LoginData::fromRequest($request);

            $user = ($this->LoginUserAction)($data);

            return $response->withJson($user->token);
    }
}

LoginData

class LoginData
{
    public function __construct(
        public string $email,
        public string $password,
    ) {}

    public static function fromRequest(RequestInterface $request): LoginData
    {
        $parsedBody = $request->getParsedBody();

        if (!isset($parsedBody['email']) || !isset($parsedBody['password'])) {
            throw new \Exception('Email and password are required');
        }

        return new self(
            $parsedBody['email'],
            $parsedBody['password'],
        );
    }
}

EDIT:

im pretty happy with this. but maybe someone can show me a way to hide

$data = LoginData::fromRequest($request);

in a generic way to the controller invocation like this:

public function __invoke(LoginUserRequest $request, ResponseInterface $response, LoginData $data)

somehow? that I can just change the type of $data and the object gets automatically generated? :thinking: