Welcome to Admin Junkies, Guest — join our community!

Register or log in to explore all our content and services for free on Admin Junkies.

PHP PSR-11

Tyrsson

Retired Staff
Retired Staff
Joined
Apr 29, 2023
Messages
402
Website
github.com
Credits
1,098
In previous post I've touched on PSR-11 but I thought it might help some of the newer folks if I covered it in a little more depth.

So what is it and why should we use it?

What it is:
PSR-11 is an Interface for which the implementation facilitates the retrivale of objects and parameters in Php applications. Why is this useful when we can just create a new instance whenever we want? There is several reason why this is extremely useful and beneficial. For a moment consider that the application has a Logger. If the container supports caching that object once its created, then we will always get the same instance of this object, if we pull it from the container. Which brings me to the next point. When using a container, we only create that instance when it's needed. Even during bootstrapping, we do not have to create the instance ahead of time. Furthermore it provides a pattern for object creation within the application so the developer always knows where to obtain an instance and knows rarely should new be used in userland for a service.

So, what are the details of using a container in our applications? It's actually very simple how they work. Usually you will provide the container with an array of configuration that will tie an identifier which must be a string to a factory. The factory class is responsible for creating the object instance and returning it to the container. In most implementers the class itself can serve as its own factory in the event that it implements __invoke(). These are commonly known as Invokables. So how do all the pieces come together to be useful? I mean that is the whole point right!

The interface provides two methods, yes, two methods provide so much flexibility. It's hard to imagine, but it's true. Lets see how to use them.

I will be referencing laminas/laminas-servicemanager for the following implementation. Its hands down the best implementation I have found. I use it even when I am working outside of Laminas MVC or Mezzio.

The exposed methods are as follows:
Psr\Container\ContainerInterface
PHP:
    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
     * @throws ContainerExceptionInterface Error while retrieving the entry.
     *
     * @return mixed Entry.
     */
    public function get(string $id);

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @return bool
     */
    public function has(string $id);

As we already know, the devil is in the details, which in this case will fall to the implementation, as always lol. So lets take a look at how ServiceManager implements these two.
For brevity here I am going to link the respective github file and line and then just cover the important details.

public function get($name)

Basically does the following
  • Checks to see if an instance is already known to the container and returns it if found.
  • Checks if the service should be shared
  • If its shared it caches the object - This is how we always get the same instance, less memory is used and its faster
  • It also checks to see if a service has been aliased - We will not be covering service aliasing in this write up (maybe in part two)
  • At this point, if an instance is not known ServiceManager calls doCreate
private function doCreate(string $resolvedName, ?array $options = null)

This is where the object instance is actually created. So lets take a look at how that occurs.
PHP:
    /**
     * Create a new instance with an already resolved name
     *
     * This is a highly performance sensitive method, do not modify if you have not benchmarked it carefully
     *
     * @return object
     * @throws ServiceNotFoundException If unable to resolve the service.
     * @throws ServiceNotCreatedException If an exception is raised when creating a service.
     * @throws ContainerExceptionInterface If any other error occurs.
     */
    private function doCreate(string $resolvedName, ?array $options = null)
    {
        try {
            if (! isset($this->delegators[$resolvedName])) {
                // start relevant, we are not covering delegated service creation
                // Let's create the service by fetching the factory
                $factory = $this->getFactory($resolvedName); // This is is very important
                $object  = $factory($this->creationContext, $resolvedName, $options); // object creation via factory
                // end relevant
            } else {
                $object = $this->createDelegatorFromName($resolvedName, $options);
            }
        } catch (ContainerExceptionInterface $exception) {
            throw $exception;
        } catch (Exception $exception) {
            throw new ServiceNotCreatedException(sprintf(
                'Service with name "%s" could not be created. Reason: %s',
                $resolvedName,
                $exception->getMessage()
            ), (int) $exception->getCode(), $exception);
        }

        foreach ($this->initializers as $initializer) {
            $initializer($this->creationContext, $object);
        }

        return $object;
    }
So what exactly is going on there? Well the code simply calls getFactory($name) which will return the factory registered for the service. How is this done? It's simply configuration. This is an excerpt from an application I am currently working on. The service name (which is the container identifier) is the key, the factory is the value. Also, notice that both use the FQCN as both the key and the value. This is the recommended way to create the container configuration.
PHP:
'factories' => [
    Handler\PageHandler::class => Handler\PageHandlerFactory::class,
    Storage\PageRepository::class => Storage\PageRepositoryFactory::class,
    Storage\SavePageCommandHandler::class => Storage\SavePageCommandHandlerFactory::class,
],

So how does this really help us? Well lets take a look at the PageHandler and its factory in detail to see.

PageHandler.php
PHP:
// notice that this class has one dependency TemplateRendererInterface
public function __construct(
    private ?TemplateRendererInterface $template = null
) {
}

So the question becomes how does that class get its dependency if we are pulling it from the container? The answer. Its factory.
PHP:
class PageHandlerFactory
{
    public function __invoke(ContainerInterface $container): RequestHandlerInterface
    {
        /**
         * This is where we really begind to see the power and flexibilty of using the
         * container, The TemplateRendererInterface::class identifies the applications
         * template implementation, Since it has its own factory that handles its creation
         * in this factory all we have to do is call the instance from the container
         * and we can inject the required instance into the PageHandler to allow
         * its creation.
         */
        $template = $container->has(TemplateRendererInterface::class)
            ? $container->get(TemplateRendererInterface::class)
            : null;
        assert($template instanceof TemplateRendererInterface || null === $template);

        return new PageHandler(
            $template
        );
    }
}
Since all configuration is read and merged during bootstrapping of the application that means that all factories are available by the time the container can be queried for an instance. Which means EVERY service that is registered in the configuration should be available to the application via the container. Simple right?

One thing however that you should NOT do is inject the Container implementation itself into a class so that the class can pull its own dependencies from the container. This is known as the Service Locator pattern and should generally be avoided.

Advanced Usage Tip:
Since the configuration is, or should be merged during bootstrapping and prior to running the application itself. This means that you can actually replace a service by overriding its key in the array and providing your custom factory as its value. You just have to make sure in which order your configuration files are loaded and merged. ServiceManager also supports delegated services which I have covered in a previous post. For those implementations that support aliasing a service in the container, you can also override Invokables by creating an alias using the original services FQCN and then pointing it to your custom implementation.

Take away:
By using the PSR-11 implementation we can add extreme flexibility and consistency to our applications throughout their entire life cycles. This helps in more ways than I can really details here. Developer onboarding, Unit/Integration testing, static analysis (this one can be tough though) just to name a few. Not to mention the power of delegators which allows a behavioral change in the application without having to modify existing code. It also allows us to use libraries and components from nearly any other PSR-11 framework/library because it provides a means for service creation that is not tied to the application logic itself per-say.
 
Last edited:

Log in or register to unlock full forum benefits!

Log in or register to unlock full forum benefits!

Register

Register on Admin Junkies completely free.

Register now
Log in

If you have an account, please log in

Log in
Activity
So far there's no one here

Users who are viewing this thread

Would You Rather #9

  • Start a forum in a popular but highly competitive niche

    Votes: 5 21.7%
  • Initiate a forum within a limited-known niche with zero competition

    Votes: 18 78.3%
Win this space by entering the Website of The Month Contest

Theme editor

Theme customizations

Graphic Backgrounds

Granite Backgrounds