Service provider PSR: discussing the scope

174 views
Skip to first unread message

David Négrier

unread,
Mar 1, 2018, 12:16:29 PM3/1/18
to PHP Framework Interoperability Group
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 (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 and Larry Garfield 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:


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. The idea is to build generic service providers.


Current proposal

The current proposal is named 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.


The alternative proposal

Nicolas Grekas and the Symfony team came up with another proposal.

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.

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 proposals

Regarding 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). 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

Rasmus Schultz

unread,
Mar 2, 2018, 2:05:18 AM3/2/18
to PHP Framework Interoperability Group
Version 0.4 of the ongoing original proposal looks pretty solid to me by now.

The alternative proposal involves strings with PHP-like type-hints that need to be parsed and checked at run-time. Seems a bit brittle, more complex than I'd like, and doesn't really seem necessary.

The only thing missing for me, from the first proposal, is the ability for providers to depend on providers, somehow. These past few years, I've been part of building a big system at work - and we have so many providers now, with so many dependencies, one of the most common problems is "hey, this provider gave me some component that needs a dependency X, which other provider is supposed to provide me with X?"

The second proposal seems to address the need to check for missing dependencies up front - but what we'd really like, is a declarative means of saying "this provider depends on that provider", since, almost 100% of the time, in an environment where literally everything is bootstrapped by providers, the answer to "X is missing" is going to be "Add the provider of X", rather than "bootstrap X yourself".

Another common problem is the order in which providers get bootstrapped - if one provider needs to override a registration of another, the order matters.

One idea to solve both of these issues, is to introduce a provider ID of some sort. The provider's own class name would be an obvious candidate, though the Composer package name might be a safer choice, since ::class might not provide static analysis when a provider isn't installed.

So basically two methods:

function getProviderID(): string;
function listRequiredProviders(): string[]

The container can now get the full list of providers, use a topological sort to figure out the correct order in which to bootstrap these providers, check for missing providers, etc.

This enables "abstract" providers as well - for example, a provider might depend on "psr/cache", which might be the Provider ID of numerous packages with an agreed-upon ID and bootstrapping scope. This would build on the idea of abstract Composer packages and make that concept more useful - since merely installing a package (with correct version and dependcies etc) is all that Composer can guarantee, this lets us guarantee that it's provider has also been bootstrapped.

I think, at this point, that's the only missing feature for me.

David Négrier

unread,
Mar 2, 2018, 8:23:49 AM3/2/18
to PHP Framework Interoperability Group
Hey Rasmus,

Thanks for the feedback!


Version 0.4 of the ongoing original proposal looks pretty solid to me by now.

The alternative proposal involves strings with PHP-like type-hints that need to be parsed and checked at run-time. Seems a bit brittle, more complex than I'd like, and doesn't really seem necessary.


Both proposals do not have the same maturity level. Maybe you should not focus too much on the implementation details of the Symfony proposal but on the general idea.
For instance, we could propose to replace the "PHP-like type-hints"  strings by proper classes. Like:

return [
    "my_service" => new Type(MyService::class, $isOptional)
];

Or for the "ServiceSubscriberInterface" (the interface that publishes the list of required dependencies of a given service provider), it could be interesting to replace the type by a "ValidatorInterface".

Something like:

interface ValidatorInterface {
    public function validateDependency($entry);
}

class MyServiceProvider implements ..., ServiceSubscriberInterface
{
    public function getSubscribedServices()
    {
        return [
            'debug' => new CanBeNull(new(MustBeBool()),
            'twig_extensions' => new CanBeNull(new MustBeArrayOf(TwigExtentionInterface::class)),
        ];
    }
}

This could allow very interesting possibilities (we could do more than only checking the type of the dependencies).

Also, as Nicoas noted, both proposals are not mutually exclusive.

@Nicolas, what do you think?

The only thing missing for me, from the first proposal, is the ability for providers to depend on providers, somehow. These past few years, I've been part of building a big system at work - and we have so many providers now, with so many dependencies, one of the most common problems is "hey, this provider gave me some component that needs a dependency X, which other provider is supposed to provide me with X?"

The second proposal seems to address the need to check for missing dependencies up front - but what we'd really like, is a declarative means of saying "this provider depends on that provider", since, almost 100% of the time, in an environment where literally everything is bootstrapped by providers, the answer to "X is missing" is going to be "Add the provider of X", rather than "bootstrap X yourself".

Another common problem is the order in which providers get bootstrapped - if one provider needs to override a registration of another, the order matters.

One idea to solve both of these issues, is to introduce a provider ID of some sort. The provider's own class name would be an obvious candidate, though the Composer package name might be a safer choice, since ::class might not provide static analysis when a provider isn't installed.

So basically two methods:

    function getProviderID(): string;
    function listRequiredProviders(): string[]

The container can now get the full list of providers, use a topological sort to figure out the correct order in which to bootstrap these providers, check for missing providers, etc.

This enables "abstract" providers as well - for example, a provider might depend on "psr/cache", which might be the Provider ID of numerous packages with an agreed-upon ID and bootstrapping scope. This would build on the idea of abstract Composer packages and make that concept more useful - since merely installing a package (with correct version and dependcies etc) is all that Composer can guarantee, this lets us guarantee that it's provider has also been bootstrapped.

I think, at this point, that's the only missing feature for me.


I completely agree that we need to discuss the way we handle the dependencies between service providers. We never really had a discussion on this topic, and I'm facing the same issue as you are.

I'm not completely sold to the idea of adding a notion of "providerID" as you present it. It's not a notion that exists in any existing framework. Also, making it "explicit" adds another layer of complexity to the solution. I'd rather have a service-provider declares that it requires a "Psr\Log\LoggerInterface" entry rather than having a service-provider declares that it requires a "psr3-service-provider" service provider (at the class level)

That being said, I agree that we could use the notion of "virtual packages" already available in Composer to carry that meaning (at the package level)

Let's take an example with 2 service providers.

Service provider "myService" provides a "MyService" entry that requires a "Psr\Log\LoggerInterface" entry to be available in the container.
Service provider "monolog" provides a "Psr\Log\LoggerInterface" entry.

In the composer.json of the package containing the MonologServiceProvider, we could add:

{
    "provide": { "psr-3-service-provider": "1.0.0" }
}

In the composer.json of the package containing the "myService" service provider, we could add:

{
    "require": { "psr-3-service-provider": "^1" }
}

That way, it would be impossible to install the service provider of "myService" without having a package that provides a service provider with a PSR-3 logger in it (quite similar to your "providerID".

If we can find some way to automatically register service-providers in our container (like Symfony Flex is doing with bundles), we are pretty much ok. Composer can guarantee we have the necessary classes in our workspace and the "autodiscovery" of service providers (we need to figure that out) takes care of registering the service providers in the container. Problem solved.

++
David.

David Lundgren

unread,
Mar 2, 2018, 2:18:32 PM3/2/18
to PHP Framework Interoperability Group

If the problem to solve is "what's a common way to put things in a container?" wouldn't the simplest solution be a `set($id, $value)` method on the container?

Most container implementations already have a method of this sort. While a few have shared/concrete/protected concepts baked in, they could make separate methods for changing it  based on the $id.

Dave

Larry Garfield

unread,
Mar 2, 2018, 5:02:49 PM3/2/18
to php...@googlegroups.com
The reason a simple set() won't work is that the "thing" put into the
container is generally not a value but instructions for how to produce a value
on-demand, and the value is typically a service object. How to encode "here's
how to build the thing" is the main question to answer.

--Larry Garfield

On Friday, March 2, 2018 1:18:32 PM CST David Lundgren wrote:
> If the problem to solve is "what's a common way to put things in a
> container?" wouldn't the simplest solution be a `set($id, $value)` method
> on the container?
>
> Most container implementations already have a method of this sort. While a
> few have shared/concrete/protected concepts baked in, they could make
> separate methods for changing it based on the $id.
>
> Dave
>
> On Thursday, March 1, 2018 at 11:16:29 AM UTC-6, David Négrier wrote:
> > 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/>.
> > 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*
> > 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
signature.asc

David Négrier

unread,
Mar 3, 2018, 8:07:29 AM3/3/18
to PHP Framework Interoperability Group
To build upon Larry's answer, the container does not only contains services. It knows how to build them.

If we ended up "setting" in a container every service, well first... we could use arrays instead of containers :) and then, the performance of the container would degrade proportionally to the number of entries in the container. You would have to instantiate and store every service on every request, even if the service does not end up being called. So basically, the bigest your application, the slowest.

This is why both proposals carefully avoid the use of a "set" method.

++
David.

Oscar Otero

unread,
Mar 3, 2018, 4:18:52 PM3/3/18
to php...@googlegroups.com
To me, the problem with these proposals is they have different responsabilities in the same class. A ServiceProvider implementation includes the callables to create instances of different services, defines the key used to store each of these services and even include two methods: one to get factories and other for extensions, so in many times you have to create a method returning an empty array (see https://github.com/container-interop/service-provider/issues/43), (Interface segregation principle).

I proposed a different approach (https://github.com/container-interop/service-provider/issues/45) consisting in creating a factory for each service. The proposal is just a sketch to illustrate the main concept (each object has just one responsabiity: create a service) and surely can be improved.



-- 
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 php-fig+u...@googlegroups.com.
To post to this group, send email to php...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/89c8e62f-b72f-459d-995f-cf0e365ff13c%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Woody Gilk

unread,
Mar 3, 2018, 7:43:58 PM3/3/18
to php...@googlegroups.com
I think that Oscar's PR proves the point that we need a config PSR before service providers will make sense. Using the container for configuration is not (imho) the purpose of containers and makes it harder to standardize on using FQCN for container identifiers.



For more options, visit https://groups.google.com/d/optout.
--

Matthieu Napoli

unread,
Mar 4, 2018, 3:10:06 PM3/4/18
to PHP Framework Interoperability Group
Using the container for configuration is not (imho) the purpose of containers and makes it harder to standardize on using FQCN for container identifiers.

I don't think it's limited to configuration. You'd have the same (non-)issue with config-less containers.

Matthieu

Alessandro Lai

unread,
Mar 5, 2018, 2:48:02 AM3/5/18
to PHP Framework Interoperability Group
I'm very happy that this is gaining traction, thanks David for spinning this up!
Can I suggest you to focus on defining the scope of your proposal? In this way you could start gathering a sponsor and a working group, and start a PSR!

Reach out to me or one of the other secretaries if you need help or more details.

David Négrier

unread,
Mar 5, 2018, 9:55:11 AM3/5/18
to php...@googlegroups.com
Hey Woody,

I'm really not sure we need that config PSR.

My reasoning goes like this:

What do we need for a config PSR? We cannot standardize a configuration format. Each framework has its own format (YAML, PHP array files, NEON, etc...)

However, we can standardize how we "get" a config value from the application.

So we would certainly have an interface like a `ConfigFetcherInterface` with a "get" (and maybe a "has" method):

interface ConfigFetcherInterface
{
    public function get(string $name) : mixed;
    public function has(string $name) : mixed;
}

Whoops... It is identical to PSR-11!
You can argue it serves a different purpose, but PSR-11 already states that a container can contain anything (including scalars like strings, ints, etc...)
So we could use PSR-11 for configuration fetching.

We could however discuss if we want to split configuration from services in the function signature of factories.

Current signature for a factory is:

function(ContainerInterface $container)

where $container contains both configuration and services.

We could change the signature to something like:

function(ContainerInterface $configContainer, ContainerInterface $serviceContainer)

I'm not really a big fan of this, but I can understand it rings a bell for people we are making a clear distinction between config and services (like in Symfony).

What do you think?

David.



You received this message because you are subscribed to a topic in the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/php-fig/Up0JATOb0-w/unsubscribe.
To unsubscribe from this group and all its topics, send an email to php-fig+u...@googlegroups.com.

To post to this group, send email to php...@googlegroups.com.

Woody Gilk

unread,
Mar 5, 2018, 10:37:30 AM3/5/18
to PHP Framework Interoperability Group
I think it was a mistake that PSR-11 allowed the container to contain
scalar values.

The value in a config PSR is that it would greatly improve type safety
(container contains objects, config contains scalars) and therefore
make it much easier to approach how a service provider is configured,
as not all containers have a way to fetch scalar values.
> https://groups.google.com/d/msgid/php-fig/CABAasbeaw5MXGBWJ1vLnqXUP9z1uXsKrDeo-3-_-ritgdQsOgQ%40mail.gmail.com.

David Négrier

unread,
Mar 6, 2018, 9:29:33 AM3/6/18
to PHP Framework Interoperability Group

I think it was a mistake that PSR-11 allowed the container to contain
scalar values.

I strongly disagree here. I think the decision we took to allow returning anything was the good one.
Otherwise, containers like Pimple based on factories could never properly implement PSR-11. PSR-11 adapted fairly well to the wide range of containers out there, without dictating a philosophy. If I had to start it again (god help me :), I would do the same!
 

The value in a config PSR is that it would greatly improve type safety
(container contains objects, config contains scalars) and therefore
make it much easier to approach how a service provider is configured,
as not all containers have a way to fetch scalar values.

Isn't this "type safety" an illusion? I mean... a config PSR could still return strings, ints, arrays of scalars, ...
And even restricted to only returning objects, a container can return ANY type of object on a given key.

So spliting one container into 2 containers (one for config and one for objects) is very far from bringing us any real type safety.

If we want to address type safety issues, we'd better look at Symfony's proposal (using an array to map identifiers to types). That brings way more value.
Reply all
Reply to author
Forward
0 new messages