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.
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.
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).
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.
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).
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.
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?
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.
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:
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.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:
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.
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?) |
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:
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
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.
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.
{
return [
'debug' => new CanBeNull(new(MustBeBool()),
'twig_extensions' =>
new CanBeNull(new MustBeArrayOf(TwigExtentionInterface::class)),
];
}
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.
Dave
--
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.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/5329ADE7-B35F-45B6-A336-C2D4BCCC529F%40gmail.com.
For more options, visit https://groups.google.com/d/optout.
Using the container for configuration is not (imho) the purpose of containers and makes it harder to standardize on using FQCN for container identifiers.
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.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/CAGOJM6%2B7z2R21UUGM675Bv%2B_JnX%3DSbu2wFc6PSECvK8_FsYGJA%40mail.gmail.com.
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.