How can I get ServerRequestInterface with attributes in Factory?

When initializing some services in the factory, I need to get data from the request attributes.
How can i do this?

1 Like

As I know, can’t get ServerRequestInterface in Container.
Only get from PSR-15.

You need refactor your service

I will try to talk about my situation.
I have a vendor service which locale is required in the constructor.
And we get the locale from the previous middleware and save it in request attribute.

Next, I need to initialize the service and transfer the locale there. Locale I can get from the request attribute.

There are still other examples when you need to get data in a factory from previous middleware.

Maybe I don’t fully understand the problem. :smile:
Can’t you just call a setHeader() method in your service (or whatever you like to name it) from within the middleware you injected the service into?

The fact is that this is not my service, this is a service installed by the composer.

Therefore, I need to write an adapter to change its initialization approach. I would like to avoid this.

Have the DI container return a factory that will create the service, instead of the service itself. This way, you can pass artifacts from the request to the factory to create the service.

If the service then needs to persist so other middleware/handlers can use it, pass the service as a request attribute as well.

As an example of what I mean by having a service return a factory:

use Psr\Container\ContainerInterface;

class LocaleFactoryFactory
{
    public function __invoke(ContainerInterface $container) : callable
    {
        return function (string $locale) use ($container) {
            return new Locale($locale/* and any other services needed from the container */);
        };
    }
}

You would compose the factory in your middleware:

class SeedLocaleMiddleware implements MiddlewareInterface
{
    private $localeFactory;

    public function __construct(callable $localeFactory)
    {
        $this->localeFactory = $localeFactory;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        return $handler->handle(
            $request->withAttribute(Locale::class, ($this->localeFactory)($request->getAttribute('locale')))
        );
    }
}

We cover this approach in our swoole documentation as well. It’s something you need to do whenever your services depend on request information as well.

4 Likes

not bad. Is that even legal? =)
thank you very much.

@matthew

Does the fact to use the request object as a service registry through attributes container is not considered a bad practice? With zend expressive, I’ve seen that pattern used many times already (session atribute, flash attribute, prg attribute…). All are services… What the difference with a global registry approach or a per domain registry approach exactly? Of couse, when passing services as request attributes, those are close to them but in my eyes, this look a bit strange…

ACK. Pulling this one out again: https://igor.io/2013/03/31/stateless-services.html

2 Likes

Morning @ocramius

Could you please elaborate a bit more and not just post a link without giving the reason for it? That would be much appreciated. We are not robots… nor spider…

Thank you very much.

It’s a full article: read it (all of it) :stuck_out_tongue:

@ocramius

I do read it now. But my question is: Does that link is an answer to my question regarding the fact to use the request object as a service registry? I know you’re really close to DDD world and so on but it would be great when you try teach us more explicitely. That’s you’re intent after all :wink:

Thank you.

@ocramius

I’ve read it full. However, this doesn’t really answer my question… The article is mostly talking about pass-in a request object to service at construction time making them not stateless. My question is: Does it is not a bad practice to use a request object as a service registry. For intance, the session middleware add the session service as an attribute of the request object to make it available for the request handler… This look a bit strange for me.

Thank you.

Essentially, there are two different levels of services to consider:

  • Services that your middleware and handlers will consume in order to perform work. These should be stateless. In other words, calls made to these services should not change internal state of the service.
  • Services that depend on the current request state.

Why would you worry about the internal state of a service? There are several reasons:

  • When it comes to unit testing, if state in the service changes, it’s incredibly difficult, if not impossible, to test all permutations, which leads to brittle code.
  • A state change that happens under one middleware will affect later middleware and/or the final handler. These changes may be useful, but could also be unexpected, which could lead to unwanted side effects.
  • If you are operating under an async environment, where the service instance will persist between requests, state changes will propagate both to parallel requests and subsequent requests, which often leads to unwanted side effects.

As such, you should have only stateless services in your DI container, as these will be pulled over and over again, both within the same request and in later requests. By making them stateless, you make the system easier to test and more predictable.

Any service that derives state based on the current request should be passed as a request-specific service, and thus via request attributes. This includes the authenticated user, session details, authorizations, etc., as they are all products of calculations based on the request. By keeping them in the request, we make it clear that these instances are based on the current request, and are thus transient.

2 Likes

@matthew

Thank you for your great answer. That’s much appreciated. I think that I’ll now switch to swoole. This will force me to make my services stateless when they should.

@matthew

Seem that there is a lot of work to do to make zend components working with swoole. For instance: https://github.com/froschdesign/zend-expressive-navigation/issues/6

Basically here, there is the navigation service that composes pages (containers), which are created only once. Problem is that the pages keep their state throughout subsequent requests. I would be curious to know how you would solve this issue: an object graph which should be created once for performance reasons but for which some of bound objects should be stateless… Creating the containers only once make sense from my point of view because those can be pulled many times by view helpers… This so means that pages should keep their state in the scope of the current request.

@nuxwin — Please note that zend-expressive-navigation is not an official component of the project yet. Raise issues on that repository noting the design issue so that @froschdesign can address it; you can point him back to this thread if needed.

We are definitely aware some of the components need some updates. In particular:

  • You shouldn’t add default parameters to the template renderer instance within your middleware or handlers, only during initialization (we provide a solution in our Swoole documentation).
  • The metadata in zend-expressive-hal is stateful with regards to route and query parameters. You should either clone it or reset any route or query parameters completely each time they are relevant to the current request.

Some of these designs initially made sense, but as we started working more with async systems, we discovered the problems. Over time, we will address all of them; in the meantime, we are documenting them as well as workarounds for addressing them.

1 Like

@matthew

I don’t totally agreed with you regarding the template renderer. You could add default parameters for the layout too through pipeline middleware, and those data should be scoped at the full application rather derivated from the current request. Let’s say some branding data pulled from a config file that live outside of the application. Course, you’ll surely say me that such data should be set at initialization (through a factory so…) but I don’t really like the idea to seed data from there…

@matthew

To be more explicite:

class BrandingMiddleware implements MiddlewareInterface
{
    /** @var TemplateRendererInterface */
    private $template;

    /** @var ConfigHandlerInterface */
    private $config;

    /**
     * BrandingMiddleware constructor.
     *
     * @param TemplateRendererInterface $template
     * @param ConfigHandlerInterface $config
     */
    public function __construct(TemplateRendererInterface $template, ConfigHandlerInterface $config)
    {
        $this->template = $template;
        $this->config = $config;
    }

    /**
     * @inheritdoc
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $noBranding = $this->config['noBranding'] ?? false;
        if (!$noBranding) {
            $this->setBrandingData($this->template, $this->config);
        }

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

    /**
     * Sets branding data
     *
     * @param TemplateRendererInterface $template
     * @param ConfigHandlerInterface $config
     */
    private function setBrandingData(TemplateRendererInterface $template, ConfigHandlerInterface $config): void
    {
        $template->addDefaultParam('layout::simple', 'appBranding', [
            'name'      => 'internet Multi Server Control Panel',
            'copyright' => '© 2010-' . date('Y') . ' Laurent Declercq (i-MSCP™)<br>All Rights Reserved',
            'site_url'  => 'https://www.i-mscp.net/'
        ]);

        $template->addDefaultParam('layout::ui', 'appBranding', [
            'version'  => 'i-MSCP ' . ($config['Version'] ?: 'Unknown'),
            'build'    => 'Build   : ' . ($config['Build'] ?: 'none'),
            'codename' => 'Codename: ' . ($config['CodeName'] ?: 'Unknown')
        ]);
    }
}

That information is request specific. The problem with calling addDefaultParam() within middleware is that this value is then true for any parallel or subsequent requests. If another request should not be changing the default, or would display something differently based on one of these template parameters, you’re now potentially creating a security issue. As an example, if you were to switch the layout to an “admin” version, and another request comes in that would normally have a user-facing layout, that user now is seeing the wrong screen.

Please read that document I linked in the Swoole documentation, as it details a different approach to handling request-specific template parameters that is safe for such situations.