Remove the ServiceProvider interface and replace it with an array of callables #23
Conversation
Ok, I've been spending the last 2 days thinking hard about this. First of all, let me say that the "genius" part of the service-providers standard is the signature of the factory (i.e. My first reaction was to say to myself: "Hey, but we need this interface!". But this was also my first reaction when we spoke about the delegate lookup feature in container-interop, and it turned out I was wrong. So I've learned to be careful of my first impressions. Yet, I see some points that trouble me. Here are my thoughts. If we say that a service provider is simply an array of factories, there are really 2 alternatives. Either the array of factories is provided "as-is" to the container, or the container knows somehow how to retrieve this array of factories. Alternative 1: the container is provided the array of factoriesIn this scenario, each container has a method that looks like this: $container->setProvider([
'entry' => function() { ... }
]);
// or
$container = new Container([
'entry' => function() { ... }
]); We really don't care about the name of the method. The problem with this approach is that the container MUST be fed with the array (i.e. the array must be resolved for each query). On the contrary, using the So basically, if we go with a Of course, we could solve this by saying that a service provider is a callable that returns an array of factories. $container->setProvider(function() { return [
'entry' => function() { ... }
]}); I'm not sure how I feel about this. It's pretty close to what we do with the interface. The interface clearly states what we expect (it carries meaning and allows us to do type hinting). For lazy guys, we can always create an anonymous class implementing it (it seems we wont be able to use functional interfaces though). Alternative 2: the container delegates fetching of the array to some other mechanismWe don't want the array of factories to be constructed on each request. So the container must know how to fetch that array (to be able to build it lazily). I agree this does not need to be a Since there are many containers out there, we should definitely define a way for the container to fetch that array. We could very well define that the array of factories are stored in files instead of classes. For instance: return [
'entry' => function() { ... }
]; I understand the appeal of this solution (it mimics services.yml files, it can be discovered using Puli easily...) What is important though is that I think we need to be specific. If we don't tell upfront in this standard how a container can fetch the array of factories (by calling a method or including a file...), we are facing interoperability problems between containers that would not know how to fetch the array of factories. |
Right agreed. Here is a list of options:
Any reason to prefer one or another? |
I'll need to think a bit more about it. In the meantime, here is a 4th possible option:
|
Ok, let me give a try at pros and cons of each option we detected so far. Callable returning an array$container->setProvider(function() { return [
'entry' => function() { ... }
]}); Pros
Cons
File returning an arraymy.provider.php <?php
return [
'entry' => function() { ... }
]; $container->register('my.provider.php'); Pros
Cons
Class method returning an arrayPros
$container->register(new class implements ServiceProvider() {
public function getServices() {
return [
'entry' => function() { ... }
];
}
});
Cons
Class implementing Traversable and ArrayAccessThe idea here is to have an object that behaves as an array of factories. It can be fed to a container just like a regular array of factories (but can be lazily evaluated). Pros
$container->register([
'entry' => function() { ... }
]);
// or
$container->register(new MyServiceProviderThatBehavesLikeAnArray()); Cons
My choiceIf I have to make a choice, I'd clearly eliminate the "Callable returning an array" solution, because I don't see how discovery can work with that. "Class implementing Traversable and ArrayAccess" seems a bit an overkill and is hard to implement. I'll eliminate it too. This leaves us with files VS classes. I can live with both solutions, although I have a preference for the class because:
I'd be curious to hear what others have to say on this topic. |
Personally I would love to see the "Class method returning an array" approach. Mainly because no one needs to deal with any kind of file (auto)loading. It will simly work due to PSR-0 or PSR-4. Compared to the file method there is not really much more "boilerplate" code we need to add. |
To sum up:
Simply saying "an array of callables" makes interoperability hard because we have no "standard" way to discover the arrays, so the whole system is harder to use. We are left with the current solution (method returning an array) and "file returning an array". I agree that if we want to encourage using static methods then an array of callables is not helping (since we would have to create the array of callables + the class containing the factory methods) => closures would be favored by implementors. So right now I tend also to the same conclusion: the class with |
Instead of:
We now write:
Now that we allow callables as factories I don't see the point to having an interface. Discovery of service providers (i.e. PHP arrays) can be handled in many ways without affecting containers (Puli, file returning the array, class/function/method returning the array, etc.).
Also fixes #19