Thanks for your reply, and thanks for laying it out in such detail. I understand performance being a feature of the compiled containers - but in the same way, simplicity is a feature of runtime configured containers.
As you said, these requirements are at odds with each other - they mandate something complex, which won't make sense in the context of containers where simplicity is a feature. And to be fair, the current proposals mandate something simple, which won't perform to the standards of compiled containers. Let's break this down and simplify. Your programming language has two broad categories of features: data and behavior - data meaning strings, numbers, attributes, data structures, etc. and behavior meaning classes, functions, closures, interfaces, etc. Compiled containers require data. It doesn't matter how that data is defined - using a PHP DSL, YAML, XML, JSON, whatever. Runtime configured containers require behavior - or maybe some mix of data and behavior, but it always involves behavior, in some form. So when it comes down to it, service providers must either consist of data only, or some mix of data and behavior. If they rely on any sort of behavior (classes, functions, closures) this inherently introduces runtime overhead for the compiled containers. Given the constraints you laid out, where no performance tradeoff is acceptable, that means nothing involving behavior would be acceptable. This leaves data-only approaches as the only option on the table. Now, regardless of how you define or source that data, this has some implications. First, this standard would offer no IDE support or static validation when writing standard providers by hand. At least, nothing beyond validating that a class-name a service-name is a string. An IDE or static analysis tool can only validate code involving behaviors - calling constructors, checking argument types, etc. all involves behavior. Second, this standard would prevent runtime configured containers from acting as service providers - no one could build a service provider factory that relies on behavior in any form, except maybe by decompiling and transforming source cod, but this would turn your runtime configured container into a compiled container, breaking the "simplicity as a feature" requirement. And maybe we say, okay, that's an acceptable limitation - this PSR is about providing a way to write standard service providers, we don't care if containers can act as service providers, we don't care if someone can build a service provider factory. All standard providers will be written by hand or generated by a compiler. Well, then you still have the other limitations: when writing standard providers you get no IDE support, no static validation, and the runtime configured containers are required to parse and resolve this data and call constructors dynamically, at runtime, adding more overhead. So I'm afraid you're right - the requirements you laid out are fundamentally at odds with each other. In my experience, when you encounter conflicting requirements, that's when you have to make a tradeoff. If no tradeoff is acceptable for compiled containers, that means runtime configured containers have to make the tradeoff. But that's just not what "tradeoff" means. A tradeoff is a compromise - it's an attempt to balance between two desirable but incompatible features. If the position of the core committee is that no compromise would be acceptable, in my opinion, the idea is dead. And in all honesty, maybe that's fine. If the community doesn't want standard service providers, I can't make them want it. :-) Thanks for giving it your time though. :-) Rasmus Schultz On Friday, March 22, 2024 at 4:14:43 PM UTC+1 Larry Garfield wrote: > On Sat, Mar 9, 2024, at 11:42 AM, Rasmus Schultz wrote: > > Hey Larry, > > > > Per chance, have you run into discussions elsewhere about this > > alternative interface? > > > > service-provider/src/ServiceProviderInterface.php at 0.5.0 · > > mindplay-dk/service-provider · GitHub > > < > https://github.com/mindplay-dk/service-provider/blob/0.5.0/src/ServiceProviderInterface.php > > > > > > It's a refactored version of the previously proposed interface, > > designed to work better for compiled containers. > > > > It avoids the use of callables, and the need to registration at run-time. > > > > A compiled container can retrieve the list of services/extensions at > > compile-time, resolve service overrides among providers, etc. avoiding > > this overhead at run-time. > > > > In practice, this would reduce the run-time overhead for compiled > > containers (versus their own internal providers) to a single > > function-call per service/extension. > > > > So it's not zero overhead, but it is reasonably close - perhaps as > > close as you can get, and considerably less overhead compared with the > > previous proposal. > > > > A hand-written or compiled service provider can use the match-based > > approach, as you illustrated above. > > > > In addition, a compiled container/builder can make a generated provider > > available, retaining the full performance of the compiled factory, if > > used to provide services to other (possibly non compiled) containers. > > > > I mean, short of creating a standard that is fully declarative (e.g. an > > XML schema) I believe this is the best we can expect in terms of > > performance, isn't it? If it involves an interface, you can't really > > reduce the overhead to less than a function call. > > > > Whether a PSR in this space ever happens, is essentially up to you, > > Larry. No other core members have shown any interest in this, and > > realistically, it's not going to go any further (and no one is going to > > take this seriously or bother participating) without a formal working > > group and a PSR number. > > > > I would love to see this come to fruition - but unless a core member > > steps up to support this, I should probably just give up and move on > > myself. > > > > What do you think, is there any chance this will ever happen, or am I > > just spinning my wheels here? :-) > > > > Regards, > > Rasmus Schultz > > Well, we'd need more than just a CC member to sponsor it. We'd also need > at least 3 other people for a working group, preferably from projects that > publish popular DICs (Symfony, PHP-DI, etc.) > > Korvin and I talked a little in Discord. To summarize my position, and the > challenge I see: There's a couple of standard features of DI Containers > these days, which if we're going to standardize registration at all need to > be fully considered. However, some of them are at odds with each other. > > 1. Compiled containers. > > These should be as fast as possible. The gold standard right now (not all > do this, but the good ones do) is to go all the way down to something like > this: > > class CompiledContainer { > private array $services = []; > > public function get(string $id): mixed { > return $this->services[$id] ??= return match($id) { > ClassA::class => new ClassA(), > ClassB::class => new ClassB($this->get(ClassA::class), > InterfaceC::class => $this->get(ClassB::class), > // ... > } > } > } > > Yes there's cases where you may also need additional method calls for > setup, but those can be handled. This is just an example, but a design that > allows this to be produced is, IMO, mandatory. > > 2. Runtime configured containers. > > These are also common (even in cases where they shouldn't be, IMO, like > Laravel). For many lower-traffic use cases they're much easier to work > with, and fast enough. The quintessential example here is Pimple, which > shows just how trivially easy this can be if you don't need much > functionality. > > 3. Complex/contextual creation. > > This is usually handled by a factory method, generally another service. > This is also an important use case, even if not the typical one. > > 4. Peer-manipulation. > > (This is a bad name, please come up with a better one.) In some cases, one > dependency wants to manipulate the DI configuration of another dependency. > This could be to add a method call to the setup routine, or to build up an > array of other services to use as a constructor dependency, etc. Symfony > solves this with Compiler Passes, and in practice I don't think there's a > better option, give or take implementation details. > > > Here's the problem: For compiled containers to work properly, you really > have to use an AST of container definitions that can then be compiled down > into the appropriate code, and you cannot have any closures or instantiated > objects in the process. Without that, you're adding at absolute minimum an > extra function call to every lookup, which adds up when there's hundreds of > services. Potentially much more if you're doing any kind of > complex/contextual creation or peer-manipulation. > > The AST approach is basically what Symfony does today, although I find > their interface and data model for it rather clumsy. (Due largely to it > being defined in PHP 5.3, when the language was a lot weaker.) > > However, an AST approach is necessarily more complex, and while it could > be implemented by a runtime container it would likely be slower than just > passing everything a factory closure and moving on. > > An AST approach does not necessarily mean YAML or XML files. The smart way > to do file-based configuration would be to have the config files translate > to the AST, so that it can be unified with any code-provided definitions. A > config file would be just another peer-manipulation. > > (Which is, yes, exactly how Symfony works. I keep coming back to it being > the right architectural design with a lackluster API.) > > I don't know how to resolve this tension between compiled and runtime > containers. > > But here's why it matters: > > If a PSR standard puts forth a runtime-optimized registration design, one > based on factories, then any libraries that use it to expose their > dependencies necessarily are hamstrung in a compiled container situation. > That's not good. OTOH, if we put forward a compile-optimized design, it > makes supporting runtime containers more difficult and slower. And we > should assume that the majority of services eventually get registered via > whatever we come up with, so "it's just for a few services, it won't make a > difference" doesn't really fly. (If it's only used in a few edge cases, > then we have failed with the PSR.) > > Rasmus' latest, if I understand it, is somewhat better, as it essentially > tells each provider to have its own match() block or equivalent. That's not > as bad as having the provider return an array of factories, but it still > adds at least one method call to every lookup, and if extensions are used > (I don't fully understand those), it's several more. > > We don't need to resolve these issues right now. We need to agree that > they need to be solved, and find people interested in solving them. As a > reminder, the purpose of a Working Group is to have these discussions in a > focused way with the experts in a given field, not to throw it out to a > list of however many people, most of whom are idle. :-) All we need for a > WG is a statement of purpose and people willing to work on it. The latter > is the challenge for now. > > Wearing my Core Committee hat, I would vote Yes on a service registration > working group, if the interested parties can be assembled. I would vote no > on a final result that doesn't adequately address the tension I outline > above. What that solution is, well, that's up for the working group to > figure out. > > So the task for now isn't figuring out the solution, it's assembling a > working group. > > --Larry Garfield > -- 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+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/0aa60766-b6fd-43e4-a6fc-227fc97f3fd6n%40googlegroups.com.