Hey list,

We are still in the process of forming a working group regarding a Service 
provider PSR.

I've had the chance to speak about this with several Symfony contributors, 
and while discussing about this idea, Nicolas Grekas 
<https://github.com/nicolas-grekas/> (from Symfony) came up with an 
alternative proposal. It's about having many containers working together, 
with a slightly different scope. First of all, I'd like to thank Nicolas 
for the time he is investing in researching this issue, and for all the 
feedback. We talked about his idea with Matthieu Napoli 
<https://github.com/mnapoli/> and Larry Garfield <https://github.com/crell> 
at the Paris ForumPHP in November. I'm now sharing this conversation with 
you.

I put this in a blog article that you can find here:

   https://thecodingmachine.io/psr-11-scope-of-universal-service-providers

I'm reposting the content of the article here, since it's directly related 
to PHP-FIG concerns. It's a bit long, but the topic is worth it :)

Stated goal

Each framework has it's own custom package format (bundles, packages, 
modules, etc...). What these package formats are doing is essentially 
always the same. They are used to put things in a container.

If the PHP-FIG could come up with a unique package format that could be 
supported by all frameworks, package developers could truly write classes 
that can be used in any framework more easily.

Hence, the stated goal of this PSR (let's call it PSR-X since it does not 
have a number yet) is to find a common way to *put things in a container*.

We (the container-interop group) have been working on this for quite some 
time and have come up with a solution that needs to be turned into a PSR 
<https://github.com/container-interop/service-provider/>. The idea is to 
build generic service providers.


Current proposal

The current proposal is named container-interop/service-provider 
<https://github.com/container-interop/service-provider/>. In this proposal, 
we create a ServiceProviderInterface interface that exposes a set of 
*factories*.


class MyServiceProvider implements ServiceProviderInterface{
    public function getFactories()
    {
        return [
            'my_service' => function(ContainerInterface $container) : MyService 
{
                $dependency = $container->get('my_other_service');
                return new MyService($dependency);
            }
        ];
    }

    // ...
}


In the example above, the 'my_service' service can be created by the 
container by executing the factory (the anonymous function).

Additionally, the ServiceProviderInterface let's you *modify* existing 
services stored in the container.


class MyServiceProvider implements ServiceProviderInterface{
    // ...

    public function getExtensions()
    {
        return [
            Twig_Environment::class => function(ContainerInterface $container, 
Twig_Environment $twig) : Twig_Environment {
                $twig->addExtension($container->get('my_extension'));
                return $twig;
            }
        ];
    }
}


In the example above, the service named "Twig_Environment" is modified. We 
register a new twig extension in it. This is very powerful. This can be 
used to create arrays and add elements to them, or this can be used to 
decorate an existing service (using the decorator pattern). Overall, this 
gives a lot of power to the service provider.

Right now, this interface has been tested. It has adapters in Symfony, 
Laravel, and there is a Pimple fork named Simplex that is also implementing 
it. You can view the complete list of implementations here 
<https://github.com/container-interop/service-provider#compatible-projects>.


The alternative proposal

Nicolas Grekas and the Symfony team came up with another proposal 
<https://github.com/symfony/symfony/pull/25707>.

Rather than standardizing service providers, he proposes that each package 
could provide it's own container. The container would have an interface to 
expose a list of services to your application's container.

The proposal goes like this:


interface ServiceProviderInterface extends ContainerInterface{
    /**
     * Returns an associative array of service types keyed by names provided by 
this object.
     *
     * Examples:
     *
     *  * array('logger' => 'Psr\Log\LoggerInterface') means the object 
provides service implementing Psr\Log\LoggerInterface
     *    under "logger" name
     *  * array('foo' => '?') means that object provides service of unknown 
type under 'foo' name
     *  * array('bar' => '?Bar\Baz') means that object provides service 
implementing Bar\Baz or null under 'bar' name
     *
     * @return string[] The provided service types, keyed by service names
     */
    public function getProvidedServices(): array;
}


Notice how the ServiceProviderInterface extends the PSR-11 
ContainerInterface <https://www.php-fig.org/psr/psr-11/>.

Here, there is a single function getProvidedServices that provides the 
names of the provided services as keys, along the type of the service as 
values.

When your application's container is asked for a service that is part of a 
"service provider", it would simply call the get method of the service 
provider (since a service provider IS a container) and retrieve the service.

There is no way for a service provider to modify services in the 
application's container (this is a design decision).

While talking about this interface, we also mentioned another interface. A 
service provider can need dependencies stored in another container. It 
could therefore publish the list of services it is expecting to find in the 
main container. Therefore, Nicolas proposed an additional interface: 
ServiceSubscriberInterface, providing a getSubscribedServices method.


class TwigContainer implement ServiceProviderInterface, ContainerInterface, 
ServiceSubscriberInterface {
    //...

    public function getSubscribedServices()
    {
        // The TwigContainer needs 2 services to be defined:
        //  - "debug" (this is an optionnal bool value)
        //  - "twig_extensions" (this is an optionnal array of objects 
implementing TwigExtentionInterface)
        return [
            'debug' => '?bool',
            'twig_extensions' => '?'.TwigExtentionInterface::class.'[]',
        ];
    }
}


Notice that the 2 interfaces can be considered independently. The 
ServiceSubscriberInterface allows to add an additional check at container 
build time (vs getting a runtime exception if a service is lacking a 
container entry or if the provided container entry is of the wrong type).


Comparing of the 2 proposalsRegarding performance

Regarding performance, the 2 proposals have very different properties.


*In container-interop/service-providers*:

The service provider is largely considered as *dumb*. It is *the 
responsibility of the container* to optimize the calls.

Actually, it is possible to get excellent performances if the service 
provider is providing the factories as public static functions.

class MyServiceProvider implements ServiceProviderInterface{
    public function getFactories()
    {
        return [
            Twig_Environment::class => [ self::class, 'createTwig' ] 
        ];
    }

    public static function createTwig(ContainerInterface $container, 
Twig_Environment $twig) : Twig_Environment {
        $twig->addExtension($container->get('my_extension'));
        return $twig;
    }

    // ...
}

In this case, a compiled container could directly call the factory, without 
having to instantiate the service provider class nor call the getFactories 
method. This is definitely the best performance you can get (but is still 
to the good-will of the service-provider author that must use public static 
methods instead of closures).


*In Symfony's proposal*:

The service provider is an actual container. *The service provider is 
therefore in charge of the performance of delivered services*.

It probably cannot beat the direct call to a public static function (since 
you have to call at least the service provider constructor and the get 
function of the service provider), but can still be quite optimized. The 
important part is that the performance is delegated to the service provider.

Dealing with service names

*In container-interop/service-providers*:

The idea is that service providers should respect some kind of convention.

If you are writing a service provider for Monolog, the service creating the 
Monolog\Logger class should be named Monolog\Logger. This will allow 
containers using *auto-wiring* to automatically find the service.

Additionally, you can create an *alias* for your service on the 
Psr\Log\LoggerInterface, if you want to auto-wire the LoggerInterface to 
the Monolog\Logger service.

The code would therefore look like this:


class MonologServiceProvider implements ServiceProviderInterface{
    public function getFactories()
    {
        return [
            \Psr\Log\LoggerInterface::class => [ self::class, 'createAlias' ],
            \Monolog\Logger::class => [ self::class, 'createLogger' ],
        ];
    }

    public static function createLogger(): \Monolog\Logger
    {
        return new \Monolog\Logger('default');
    }

    public static function createAlias(ContainerInterface $container): 
\Monolog\Logger
    {
        return $container->get('\Monolog\Logger');
    }

    // ...
}


*In Symfony's proposal*:

I must admit I'm not 100% clear on Nicolas thought here. There are really 2 
solutions. Either we adopt a convention (just like with 
container-interop/service-provider), either we can decide that the 
container can be "clever". After all, using the getProvidedServices class, 
a container can know the type of all provided services, so if it could 
decide to autowire them by its own.

For instance, if a call to getProvidedServices returns:

[
    'logger' => '\Monolog\Logger'
]

the container could decide on its own that the 'logger' service is a good 
fit to auto-wire '\Monolog\Logger'.

At this stage, the decision is delegated to the container. The service 
provider is more "dumb". It does not know and does not decide what gets 
auto-wired. The container does (this means there is probably some 
configuration required in the container).

Dealing with list of services

It is pretty common to want to add a service to a list of services. In 
containers, this is usually done by using "tags". None of the 2 proposals 
supports the notion of tags directly. But both have workarounds.


*In container-interop/service-providers*:

The idea is to create an entry in the container that is actually an array 
of services. Each service provider can then modify the array to register 
its own service in it.

class MonologHandlerServiceProvider implements ServiceProviderInterface{
    // ...

    public function getExtensions()
    {
        return [
            HandlerInterface::class.'[]' => function(ContainerInterface 
$container, array $handlers = []) : array {
                $handlers[] = new MyMonologHandler();
                return $handlers;
            }
        ];
    }
}


*In Symfony's proposal*:

The PR does not state it, but we could imagine allowing types with '[]' at 
the end.

For instance, if a call to getProvidedServices returns:

[
    'monologHandlers' => HandlerInterface::class.'[]'
]

then the container might decide to automatically append the services 
returned by 'monologHandlers' to services with the same name in the main 
container.

Said otherwise, the container calls get('monologHandlers') on all the 
service providers and concatenates those.

Dealing with list of services with priorities

Sometimes, you are adding a service in a list that must be ordered.

Let's take an example. You just wrote a PSR-15 middleware that is an error 
handler (like the Whoops middleware <https://github.com/middlewares/whoops>). 
This middleware must absolutely be the first to be executed in the list of 
middlewares (because it will catch any exception that might be thrown by 
other middlewares).

Some containers allow to tag with priorities. But we don't have this notion 
in our interfaces.

How can we deal with that?
Do we need this? Discussing with Matthieu Napoli, I know that Matthieu 
thinks this can be out of scope of the PSR. In Matthieu's view, it is not 
the responsibility of the service provider to decide where a service is 
inserted in a list. I personnally feel this is quite an important feature. 
An error handling middleware knows it must be at the very beginning so I 
think we (the service providers authors) should do all what we can to help 
the developer using our middleware to put it at the right spot. For the 
author of the Whoops middleware service provider, it is quite obvious that 
the middleware must go first. For the average PHP developer that is not an 
expert in middleware architectures, it might be far less obvious. 


*In container-interop/service-providers*:

The idea is to create an entry in the container that is a priority queue. 
For instance, PHP has the great \SplPriorityQueue.

class WhoopsMiddlewareServiceProvider implements ServiceProviderInterface{
    // ...

    public function getExtensions()
    {
        return [
            'middlewareList' => function(ContainerInterface $container, 
\SplPriorityQueue $middlewares) : \SplPriorityQueue {
                $middlewares->insert(new WhoopsMiddleware(), -9999);
                // Note: we should replace the -9999 by a constant like 
MiddlewarePriorities::VERY_EARLY
                return $middlewares;
            }
        ];
    }
}


*In Symfony's proposal*:

How to deal with this in Symfony's proposal is quite unclear to me.

We could decide this is out of scope.

We could also decide that we have many unsorted list, like 
'earlyMiddlewares', 'utilityMiddlewares', 'routerMiddlewares'... that are 
concatenated by the middleware service provider and fed to the middleware 
pipe.

Miscellaneous 1: introspection

Symfony's proposal has 2 wonderful features that 
container-interop/service-provider does not have. They are not directly 
necessary to our stated goal, but are quite nice:

   - the ServiceProviderInterface is actually an introspection interface 
   into any container implementing it. This gives us a lot of room to write 
   cross-framework tools that can scan containers and analyze them. Pretty 
   cool. 
   - the fact that a service provider can publish the list of dependencies 
   it needs (the ServiceSubscriberInterface) is in my opinion a very good 
   idea. A service provider offers some entries but can also require some 
   entries. By publishing its requirements, we get: 
      - automated documentation 
      - the possibility to do static analysis 
      - the possibility to write tool chains that help the developer set up 
      service providers (think about a huge online database of all service 
      providers available on Packagist with what they offer and what they 
require 
      :) ) 
   

Miscellaneous 2: factory services

PSR-11 recommends that 2 successive calls to get should return the same 
entry:

Two successive calls to get with the same identifier SHOULD return the same 
value.

Indeed, a container contains services. It should not act as a factory. Yet, 
it does not forbid containers to act as a factory (we used "SHOULD" and not 
"MUST" in PSR-11). *container-interop/service-provider* on the other end is 
very explicit. The service provider provides factories, and the container 
MUST cache the provided service. So for services provided by 
*container-interop/service-provider*, 2 successive calls to the container 
MUST return the same object. I don't see this as a problem, rather as a 
feature. Yet, with Symfony's proposal, since calls to "get" are delegated 
to the service provider (that is a container itself), we could write a 
service provider that provides a new service on each call to get. Symfony's 
proposal is more flexible in that regard.

Summary / TL;DR

That table below summarizes the differences between the 2 proposals:


*container-interop* *Symfony* 
Performance Container is in charge Service provider is in charge 
Service names By convention Can be deduced from types 
Static analysis No Possible 
Modifying services Yes (powerful service providers) No (dumb service 
providers) 
Tagged services Yes, via modified arrays Yes 
Tagged services with priorities Yes, via modified SplPriorityQueues No (out 
of scope?) 


My thoughts
This section highlights my current opinions. Others might completely 
disagree and I think it is important we have a discussion about what we 
want to achieve.

By standardizing service providers, we are shifting the responsibility of 
writing the "glue code" from the framework developer to the package 
developer. For instance, if you consider Doctrine ORM, it is likely that 
the Doctrine service provider would be written by the Doctrine authors 
(rather than the Symfony/Zend developers). It is therefore in my opinion 
important to empower the package developer with an interface that gives 
him/her some control over what gets stored in the container.


Existing packaging systems (like Symfony bundles or Laravel service 
providers) have already this capability and I believe we should aim for 
this in the PSR.


Taking the "PSR-15 Whoops middleware" example, it is for me very important 
that the service provider author can decide where in the middleware pipe 
the middleware is added. This means being able to add a service at a given 
position in a list (or having tags with priorities). This, in my opinion, 
should be in the scope of the PSR.

Said otherwise, while registering the service provider in the container, 
the user should be able to write:


$container->register(new WhoopsMiddlewareServiceProvider());


instead of something like: 


$container->register(new WhoopsMiddlewareServiceProvider(), [
    'priority' => [
        WhoopsMiddleware::class => -999
    ]
]);


In this regard, I feel the *container-interop/service-provider* proposal is 
better suited (because it allows to modify an existing service and that is 
all we need).

That being said, the proposal of Nicolas has plenty of advantages I can 
also very well see:

   - container introspection 
   - better maintainability/documentation through better tooling 

I have a gut feeling that there is something that can be done to merge the 
2 proposals and get the best of both worlds. Or maybe we can have the 2 
proposals live side by side (one for service providers and the other for 
container introspection?)


What do you think?

What should be the scope of the PSR?

For you, is it important to give service provider some control over the 
container or should they be "dumb" and just provide instances (with the 
controller keeping the control on how the instances are managed)?


++

David

Twitter: @david_negrier

Github: @moufmouf

-- 
You received this message because you are subscribed to the Google Groups "PHP 
Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/php-fig/780540cc-d0b0-49a0-863a-743c79efbbda%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to