-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Exploring a verifiable provider format #53
Comments
I absolutely acknowledge the problem. I remember that before this last edition with just The problem still presents, however. That's why I have been using With regard to having something like this in SP, I'm not sure this will be allowed through if we make it part of this standard. I'm thinking that the best approach could be to keep the service definition to a |
To add to what @XedinUnknown said: The approach implemented by dhii/services is 100% optional and compliant with the current spec, since it merely provides invocable classes that can replace closures, and can expose their dependencies via a They look something like this: class UserServiceProvider implements ServiceProviderInterface
{
public function getFactories()
{
return [
"user-service.connection" => new Factory(
['user-service.pdo'],
function (PDO $pdo) {
return new Connection($pdo);
}
),
// Alternative to `Factory`, calls the ctor and passes the deps
UserRepository::class => new Constructor(UserRepository::class, [
'user-service.connection'
]),
];
}
public function getExtensions()
{
return [
UserRepository::class => new Extension(
[LoggerInterface::class], // UserRepository::class is implied
function (UserRepository $repo, LoggerInterface $logger) {
$repo->addLogger($logger);
return $repo;
},
),
];
}
} The end result isn't all that different from the format that @mindplay-dk proposed - just slightly more verbose - but provides the following benefits:
As @XedinUnknown said, because SP currently provides $deps = ($service instanceof Service)
? $service->getDependencies()
: []; It's not ideal, but IMO it's relatively minor. It's what I've had to resort to in order to detect non-existent services, circular dependencies, and even be able to reuse multiple instances of the same provider (yes, really). So it would be great if an equivalent to |
Yes, this looks very similar to what I'm suggesting. The main difference is this doesn't leverage reflection to resolve singletons. So, where new Factory(
[Foo::class, Bar::class, "my.cache"],
function (Foo $foo, Bar $bar, ICache $cache) {
// ...
}
); I'd prefer something more along the lines of this: new Factory(
[ICache::class => "my.cache"],
function (Foo $foo, Bar $bar, ICache $cache) {
// ...
}
); That is, avoid restating your assumptions - if the type is equivalent to the name, as it typically is for singleton registrations, there's no need to restate that. And it's not the verbosity I'm opposed to - just the fact that stating the same information twice creates another source of errors. Someone looking at code with an error isn't necessarily going to know which of a conflicting pair of type-hints and names is right or wrong. I like what I'm seeing here though - it's very simple and elegant. 🙂 |
I don't think that re-stating type assummptions is a problem here. The type of the service is completely unrelated to the name of the service. Although this is a convention in some frameworks, I for one do not name my services after their type, but instead the name relfects the significance of the service in the application, I've got quite a few projects working this way, and I don't think that this standard should work against any particular naming convention. In this light, the fact that you mention the same type twice in one definition is incidental. Cases where this will happen often are numerous, and here we can even talk about auto-wiring, but this is a case for the implementation, rather than the standard IMHO. |
It can be, but it doesn't have to be. For multitons, you need to invent a name - for example, if two different classes depends on an I would do this with any container, as a convention - even something simple like Pimple, regardless of whether it does any sort of auto-wiring based on the names.
What I'm suggesting does not work against your convention - it just works using this convention also. I don't use auto-wiring myself, but I do rely on this naming convention. I'm pretty sure a lot of people do. |
Yes, of course, like I wrote - there are many cases where the class name is used for a service name. And yet, they are not related: it's just the framework or personal choice to do this. There can be tooling that allows you to leverage that convention, if it is used, such as autowiring, or you can simply make a helper function that encodes that convention. But I really don't think we should relate the service name and the class name in the SP standard. The same flexibility that allows us to have a multitude of useful implementations, like The point of |
I don't know, I go back and forth on this right now. 🤔 On the one hand, I can see your point, that registering services under class/interface names, as a convention, is a personal choice - and I get that it isn't strictly about interoperability, per se. On the other hand, while the current provider proposal does not preclude verifiability, it only permits it as an implementation detail. That is, it's up to the individual authoring a provider, or publishing one from a container, whether or how they validate dependencies. If containers define and validate verifiable dependencies in different ways, that verification becomes non-interoperable. The implementations used by individual providers will report errors differently, throw different exceptions, and so on. One provider can't register dependencies defined by another provider - and a container can't verify them. What I'm proposing is one way of making verifiable requirements interoperable. There might be other ways. For example, taking inspiration from interface ServiceInterface
{
/**
* Retrieves the keys of known, dependent services.
*
* @return string[] A list of strings each representing the key of a service.
*/
public function getDependencies(): array;
} The return-types in Using a class to implement this interface would be optional. When implemented, the list of keys does not have to be complete, and does not have to correspond with the argument order of a callable - it's provides a means of specifying verifiable dependencies only. The The factory type in The container prototype I mentioned in my original post could be refactored, abstracting away the reflection and inference features, while exposing them via this interface - the reflection/inference becomes an implementation detail. Maybe that's a direction worth considering? |
Not sure about this one, and not sure I understood it correctly. Multiple providers can in fact declare services with same names as from other providers. My modularity and loading standard defines a strict resolution mechanic based on load order for this, where the latter provider overrides same name services from earlier providers. With regard to adding a
If With regard to reflections, I'm not sure I understand. As far as I know, |
The But another implementation of And at the end of the day, the implementation of the service doesn't matter to anyone except to the author of the SP. So long as we have the interface as part of the spec, of course. |
Maybe it's time to make some implementations:
Pretty sure @mecha made some progress in the direction of reursion detection some time ago in Dhii/containers#13, and we should probably finalize it. @mindplay-dk, would you like to make a proof of concept that uses reflections, based on what we have already discussed? |
Sure! I had even done some work on statically analyzing a service graph and report errors. I'll dig up the work I had already completed and polish it up. |
This doesn't make sense to me. It's first of all not simple - it's a hidden requirements that can't be expressed in a type-declaration. But it's also irrelevant to the standard. The closure that accepts these dependency-names, in that order, is an implementation detail - so now we're back to imposing requirements based on ideas and implementation details that aren't a part of this abstraction. Which you just talked me out of. 😏
Yes, that's what I was trying to accomplish with my original post. I think that will still be possible at the level of the individual library/container implementation - but it doesn't necessarily have anything to do with interoperability, as far as validation is concerned.
No, this was in relation to my own prototype. 🙂
I might play around with this, yes - so we can have something more concrete to discuss. |
as proposed in container-interop#53 (comment)
I've added the interface to a branch here, so we can play around and compare notes. You can install it via
|
Alright, I have a working prototype that implements the https://bitbucket.org/mindplaydk/mindplay-funbox/src/provider-interop/
The Note that, while this
|
Oh that's much simpler than I imagined. You're basically just "translating" the dependencies. Cool!
Hah! Why do you think we gave it such as generic name? 🤣 |
Totally fair points. I concede.
That's why
Looks very useful if all of the dependencies are services that correspond to the param types by name. And brings you a step closer to autowiring too, if that's needed. If you would like to contribute it to |
Ah, what's what's missing! Yeah, I couldn't make sense of it, since the type union
I left this out because it's unrelated to consuming a factory - it's about building the factory, which isn't essential for interoperability. It belongs to the implementation details of each library, I think.
If you missed it, you can (and must) specify a name in any case where the param-types do not correlate with keys.
Hard pass. 😉 All of my applications have at least two contextual layers of IOC containers - one for long-lived services that survive multiple requests (when running under Swoole, etc.) and another for request-specific dependencies, which gets disposed after each request. Auto-wiring is extremely risky in applications with two layers, as you have no idea about the life-cycle of an auto-wired service. (Unbox specifically lists it as a non-feature, since
I already maintain an IOC container (unbox) - the experimental container (funbox) that I've been tinkering with here is just an experiment to play with PHP 8 patterns and try out some ideas. 🙂
PHP 8 - attributes 🙂 |
Add `__invoke`, per discussion here: container-interop#53 (comment)
So I added We probably should rename it to The terms "service" and "entry" seems to be used interchangeably in the spec? I generally favor "entry", because it doesn't imply that it's a service - it could be just a value. On the other hand, value entries usually don't have dependencies - there is no practical reason to use this interface for values, where On the other other hand, I have seen examples where a registered value is an I guess I'm leaning towards All those in favor say aye? |
I've opened a draft PR, so we can discuss more details in context. I've added some documentation to the README as well - referring to the interface as |
Hmmmm. It is a factory, indeed. But as far as I've read, the |
Quote the "Factories" section in the README:
etc. Based on that, the documentation change seemed to fit. (?) |
Regarding It probably shouldn't, and I say this while crying, be a part of this standard. However, I feel it should be part of some standard. This method is very important for applications that need to be composed of multiple providers - especially when a provider can be reused. I use this method to prefix services in a provider. So I would like it to be something that I could depend on, and not be proprietary.
I'd tend towards definition, so as to not confuse the terminology with what is used in But where does that leave extensions? They require a second parameter in |
I think this is a problem of terminology definitions. The way I understand it is like this: $definition = function(ContainerInterface $c) {};
$service = $definition($c);
[
// a definition in a list is an "entry"
'key' => $definition,
]
class SP implements ServiceProviderInterface
{
public function getFactories() {
// Entries returned from here are "factory" definitions
}
public function getExtensions() {
// Entries returned from here are "extension" definitions
}
} |
I think that's a matter of opinion.I considered this feature for my own container (unbox) in the past, and always landed on "no". One of my motivations for writing a container was to strike a balance between useful and necessary. Where something like Pimple lands firmly on the side of "necessary", and where something like PHP-DI, having "all the bells and whistles", lands pretty heavily on the side of "useful", I tried to land somewhere in the middle - in a place where a given feature has to be very useful to make the cut. A lot of branches and pull requests were opened, tried, tested, discussed, and ultimately rejected if they served only a few use-cases. I worked in an very modular environment where unbox was used for some 20-30 providers, many inter-dependencies between them, many optional add-ons, etc. We found very few use-cases (one or two) where someone needed to actually override a single dependency of a service definition. Typically, if a dependency of a given service definition needs to be replaced, that same dependency needs to be replaced for all dependents in all providers - I've found it's much more common to override the dependency itself, than to replace a single dependency. I also suspect (not from experience) that it's less transparent when there's a problem you need to debug. Which provider is responsible for this service definition? When it's definition is pieced together by several providers, things can get pretty complex. With all that in mind, is it a big deal to have to replace the entire definition of a service? In my opinion, this type of override typically happens in an application-specific provider, not in a library provider - so I think it's actually safer and simpler to replace whole definitions. So I think this comes down to opinions, really? And I think community standards should try to stay away from things that come down to mostly opinion.
Yes, extensions are topic we haven't explored yet - I omitted them from my prototype for now. They will most likely need a separate interface, since their |
The "Factories" section uses phrases like, "Factories accept one parameter". The method-name in the existing interface is True, "definition" and "entry" are used in other sections - but this needs to fit into that section. I'd rather not challenge the existing documentation or it's terminology. 🤔 |
Well, from what I gathered, both factories and extensions are definitions. So probably |
I completely agree. I think you misunderstood that I want such replacement to be supported by a container - I do not. This is about handing control back over to the application, allowing it to decide how to use service providers before constructing its container.
Me neither. But like you said
And for that we'd need an interface from which both factories and extensions derive from, to allow a consumer to expect either one or the other. And since an Or we could keep what we have now: |
@XedinUnknown Yeah so 2 interfaces, like what we currently have in interface WhateverWeDecideToCallIt {
public function getDependencies(): array;
public function __invoke(ContainerInterface $c): mixed;
}
interface ExtensionInterface extends WhateverWeDecideToCallIt {
public function __invoke(ContainerInterface $c, mixed $prev = null): mixed;
} Looking at it from a different perspective, calling the primary interface But if this will be a proposal to integrate into the |
Aha, the terminology makes sense now. 🙂 I'm not sure I understand the examples you both posted - the factory/extension interfaces override I think the parent interface needs to be a proper facet - it's what both interfaces have in common, which is That is, So, short version in semi-pseudo-code, we end up with something like: interface ServiceDefinitionInterface
{
public function getDependencies(): string[];
}
interface ServiceFactoryInterface extends ServiceDefinitionInterface
{
public function __invoke(ContainerInterface $container): mixed;
}
interface ServiceExtensionInterface extends ServiceDefinitionInterface
{
public function __invoke(ContainerInterface $container, mixed $previous): mixed;
} Now, if you type-hint against |
According to the docs, |
I'm going to add comments relating to e.g. |
I'm not sure if there would be a case where So I'm inclined to agree with @mindplay-dk on the interfaces. |
That's a good idea. This thread is already quite long. |
Hmmmmmyeah, but then it goes on to explain:
I think maybe the intention with the default I mean, it wouldn't make any sense for this to be optional - an extension can't get invoked unless there's a factory definition for whatever it's trying to modify. Even if that factory produces a To put it another way, if passing the previous value was optional, the interface wouldn't make any sense at all - you can't extend something if passing that something to your function is optional. I think we need to hear from the original authors on this. 🤔 |
Hm, well, here's some sort of explanation, I guess, maybe: service-provider/src/ServiceProviderInterface.php Lines 29 to 36 in 4969b9e
"If the entry to be extended does not exist" - this seems to imply containers should ignore missing factories and just invoke the extensions with Either way, that probably explains why it's nullable - but still doesn't explain why it's optional. I mean, the end result is the same - whether an extension gets called with So I don't believe there's any reason for this be nullable, or any real reason why this inline documentation needs to stamp out two different callable signatures here? Either of those would get called with I would think this should be corrected, but will wait for confirmation here before touching it. |
Regarding nullables, this has been clarified in the ongoing first draft of the PSR. To the original subject of this thread: there is an ongoing PR #54 which would make dependencies enumerable. I don't think there's anything further to discuss on that subject, so I'm closing this issue - if more discussion is required, please start a thread in Discussions. |
Following the discussion here and a preliminary exploration of a concept here, I'd like to open a discussion about the possibility of a verifiable provider format.
What I mean by verifiable, is: a provider with external dependencies (dependencies that aren't defined by the provider itself) would be verifiable by a container, ahead of time - before actually attempting to look up a component with a missing dependency.
Consider the following stubs for simple use-case:
For this example,
UserRepository
will be registered as a singleton, whereasConnection
and it'sPDO
dependency will be registered under provider-specific names with auser-service
prefix, e.g. allowing for more than oneConnection
andPDO
instance for different dependents. The PDO instance needs to be registered by someone outside the provider. TheUserRepository::addLogger
method demonstrates setter-injection viagetExtensions
.Consider a status quo service provider for these dependencies:
When you bootstrap this provider, you immediately have two problems:
UserRepository
will fail for the missinguser-service.pdo
.LoggerInterface
dependency.You have to discover and fix these, one at a time, as you uncover the failures.
After that, you could have two more problems: the types of
user-service.pdo
andLoggerInterface
aren't checked, so those might fail when passed to their dependents - and that's assuming their dependents have type-hints, otherwise you could have more obscure problems already.Finally, here's the verifiable provider format I'd like to discuss:
Each factory definition consists of tuple with two elements: a factory
function
, and an (optional) array of dependency names, each corresponding to a factory-function argument.The list of dependency names in this example are positional:
["user-service.pdo"]
- but they could also be type-name maps:[PDO::class => "user-service.pdo"]
, or they could be argument-name maps:["pdo" => "user-service.pdo"]
, or possibly a mix of any of those. There are pros and cons for each.There are three benefits to this approach:
Note that this format does not preclude dynamic lookups. Factory functions that dynamically (for example, conditionally) determines component names, can still look up components by requesting
ContainerInterface
via the closure, same as the current proposal. Likewise, containers that provide service-providers, and don't have the dependency list available, can look up everything dynamically, same as before.In both those cases, of course, there's no validation a container can perform. But the option to build or write verifiable containers could be made backwards compatible with the current proposal.
This example is actually less verbose than the original, though I'm not sure if that's generally the case, or just this particular scenario. But factory-functions could be even more abbreviated with PHP 7.4 function expressions, e.g.:
I'm not crazy about using tuples, by the way - it might be better to introduce a class or a function/interface to construct them, e.g.:
Based on the concept in the experimental container I mentioned above, one might also consider PHP 8 attributes, which could remove the need for either tuples or a class/function/interface, e.g.:
Anyways, there's some ideas to explore.
Let me know what you make of this? 🙂
The text was updated successfully, but these errors were encountered: