Service Provider PSR: rebooting the proposal

649 views
Skip to first unread message

Rasmus Schultz

unread,
Jan 12, 2024, 12:00:20 PMJan 12
to PHP Framework Interoperability Group
Dear Figs :-)

I've been hard at work these past couple of months, since I became interested in the Service Provider PSR proposal again.

I've spent many nights and have read through every issue and comment on the issue tracker, and many old discussions here on the group, going back as far as 6-7 years.

I eventually ended up writing a proper draft for the PSR itself, as well as a meta-document. And the updated proposal features optional dependency enumeration, which you can read more about in the new documents.

You can find the new documents here:


This is a call for participation! I'd like to invite you to read the drafts, comment on open issues, and contribute to discussions.

Note that the "Issues" tab on the Github page is intended for issues with the documents themselves, while the "Discussion" tab should be used to propose ideas and discuss implications. ("there is a problem with the spec" is an issue, while "how about this cool idea" is a discussion.)

I worked hard on this, and I'm taking the lead on another push to bring this proposal to fruition, so I really hope some of you will take an interest. Let's make it happen! :-)

Thank You.

Regards,
  Rasmus Schultz

Larry Garfield

unread,
Jan 12, 2024, 12:35:46 PMJan 12
to PHP-FIG
First off, thank you for resurrecting this topic. I argued at the time of PSR-11 that the registration side was at least as important as the extraction side, and while I eventually came around on PSR-11 doing just the extraction part, I still believe the registration side is just as important.

That said, I don't think the current approach is viable. I appreciate the simplicity argument for it over some of the alternatives, but it has one rather large flaw: It's incompatible (as far as I can tell) with compiling or optimizing containers.

To be very brief (more for the audience than for Rasmus, who I'm sure knows this already), some containers do all registration at runtime. In that case, having every provider return an array of closures works well enough. Others collect the service and dependency information once and code-generate a file or files that are used at runtime, with all the information pre-computed so the registration process does not need to run.

The quality of compiled container output varies widely, but all of them are substantially faster than a runtime container. The best use generated match() statements, which benchmarks show are faster than even a chain of if-statements and, likely, an array lookup to a closure. In the ideal case, you end up with generated code like:

public function get(string $service): mixed
{
return $this->cache[$service] ??= match($service) {
\App\A::class => new \App\A(),
\App\B::class => new \App\B($this->get(\App\A::class)),
\App\C::class => new \App\C($this->get(\App\A::class), $this->get(\App\C::class), 'some-constant'),
default => throw new Exception(...);
};
}

(You could actually do better than that, but I'm trying to keep it simple.)

The problem is, Closures cannot be serialized. That means the factory approach as presented is incompatible with a compiled container; the only option would be to do some kind of AST parsing of the closure and regenerate its code, which... ew.

So standardizing registration on a mechanism that is incompatible with high-performance containers seems... not great. I realize the alternatives with standard file formats have their own challenges, which is why they haven't been done yet.

I think probably the best option would be to effectively standardize something along the lines of Symfony's compiler passes, with a more modern API design. Pass a ContainerBuilder to a series of objects with some standard interface for defining and modifying service entries. With the data then in-memory in an abstracted form, it could be used at runtime or compiled, as the container implementation prefers.

Part of me wants to build such a compiler-pass system on top of PSR-14, since it's virtually the same model. (And that kind of "registration pass" was an explicit design target of PSR-14.) It could probably be done in a PSR-14-optional way.

--Larry Garfield

Rasmus Schultz

unread,
Jan 12, 2024, 3:04:38 PMJan 12
to PHP Framework Interoperability Group
hey Larry,

the current approach is compatible with Symfony - David Negrier (among others) were very concerned with this, and he did in fact build a Symfony bridge:


I understand this does not enable containers like Symfony or PHP-DI to apply the kind of optimizations they apply to their own configuration - but the
declarative configuration they use is also much more in need of optimization than some of the simpler code-based configuration approaches. These
optimizations exist because these DI containers need them.

Anton Ukhanev (XedinUnknown) previously wrote this:

> I really like the way SP is right now, because I think that currently it addresses all concerns that are related to providing extendable services. I don't think that
> containers themselves, or how they are consumed, is very much the scope of SP. For example, container cannot be compiled themselves, because their
> members are not enumerable. But Service Providers are, because multiple service providers can be merged into one, and then combined definitions
> can be enumerated and cached separately, which will save speed when the definitions have to be resolved by a container.


there are pros and cons to compilation, just as there are pros and cons to any approach taken by any other container types.

note that service providers can be integrated in both directions - if the Symfony container didn't want to consume standard service
providers, it could still make it's own code-generated container produce a service provider, which you could then plug in somewhere else.
other projects in the community might benefit from Symfony's compiled containers that way, if they wanted.

I would also point out that the performance of individual service providers would depend on how they're implemented - if you wanted
to hand-write switch-statements, you can do that, and get equal performance to Symfony. If you can figure out a way that's even faster,
you are free to do that too. :-)

so yes, the individual service provider sets the theoretical speed limit - that would be true for any consuming container, not just Symfony's.

so I don't think it makes sense to paint the situation as though standard service providers in this form would be useless to Symfony?

Rasmus

Rasmus Schultz

unread,
Mar 9, 2024, 1:02:57 PMMar 9
to PHP Framework Interoperability Group
Hey Larry,

Per chance, have you run into discussions elsewhere about this alternative interface?


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


On Friday, January 12, 2024 at 6:35:46 PM UTC+1 Larry Garfield wrote:

Korvin Szanto

unread,
Mar 10, 2024, 6:20:50 PMMar 10
to php...@googlegroups.com
Thanks for putting so much effort into this Rasmus,
I totally agree, it'd be really nice to be able to provide a generic
implementation of some kind that makes accessing my library as
painless as possible.

> That said, I don't think the current approach is viable. I appreciate the simplicity argument for it over some of the alternatives, but it has one rather large flaw: It's incompatible (as far as I can tell) with compiling or optimizing containers.
>
> To be very brief (more for the audience than for Rasmus, who I'm sure knows this already), some containers do all registration at runtime. In that case, having every provider return an array of closures works well enough. Others collect the service and dependency information once and code-generate a file or files that are used at runtime, with all the information pre-computed so the registration process does not need to run.
>
> The quality of compiled container output varies widely, but all of them are substantially faster than a runtime container. The best use generated match() statements, which benchmarks show are faster than even a chain of if-statements and, likely, an array lookup to a closure. In the ideal case, you end up with generated code like:
>
> public function get(string $service): mixed
> {
> return $this->cache[$service] ??= match($service) {
> \App\A::class => new \App\A(),
> \App\B::class => new \App\B($this->get(\App\A::class)),
> \App\C::class => new \App\C($this->get(\App\A::class), $this->get(\App\C::class), 'some-constant'),
> default => throw new Exception(...);
> };
> }
>
> (You could actually do better than that, but I'm trying to keep it simple.)
>
> The problem is, Closures cannot be serialized. That means the factory approach as presented is incompatible with a compiled container; the only option would be to do some kind of AST parsing of the closure and regenerate its code, which... ew.
>
> So standardizing registration on a mechanism that is incompatible with high-performance containers seems... not great. I realize the alternatives with standard file formats have their own challenges, which is why they haven't been done yet.
>
> I think probably the best option would be to effectively standardize something along the lines of Symfony's compiler passes, with a more modern API design. Pass a ContainerBuilder to a series of objects with some standard interface for defining and modifying service entries. With the data then in-memory in an abstracted form, it could be used at runtime or compiled, as the container implementation prefers.
>
> Part of me wants to build such a compiler-pass system on top of PSR-14, since it's virtually the same model. (And that kind of "registration pass" was an explicit design target of PSR-14.) It could probably be done in a PSR-14-optional way.
>
> --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+u...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/ac46ca26-b5e8-4aa1-a1d8-d9fdb78bec08%40app.fastmail.com.


I think this might point to PSR-11 being the wrong target. What if
instead of focusing on providing things to a container we consider how
to make factory classes or products of factories more available.
Could something like this be viable? This is basically pseudo-code and
just a passing thought so don't hold it against me:
https://gist.github.com/KorvinSzanto/ae797b48600a53dc1c415ef114d53088

Larry Garfield

unread,
Mar 22, 2024, 11:14:43 AMMar 22
to PHP-FIG
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>
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

Rasmus Schultz

unread,
Mar 23, 2024, 9:05:43 AMMar 23
to PHP Framework Interoperability Group
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

Rasmus Schultz

unread,
Mar 23, 2024, 9:06:11 AMMar 23
to PHP Framework Interoperability Group
Just to entertain the idea, if we were to go fully declarative, service providers cannot include any behavior (classes, methods, closures or interfaces) and must essentially be data structures, since nothing else works for the compiled containers - which would mean:

* Non-compiled containers would be unable to act as service providers.
* Non-compiled containers would take on extra overhead from parsing the data-structures.
* Service providers would need to be hand-written, with no IDE support or static inspections. (beyond checking that a class-name or service ID is a string etc.)
* I don't think most people would want to write service providers under these circumstances?

But maybe that's okay? Maybe they're only required for a few libraries with complex bootstrapping, and maybe there is still value in making this bootstrapping portable between containers.

I'm not optimistic about it, to be honest, but I am willing to entertain the idea.

Just to get some sense of what we're talking about, I wrote a quick draft:

https://gist.github.com/mindplay-dk/6118034336e32376c62c6ca5f28b9470

It's a far cry from the simplicity of the current proposal - you basically have to replace at least a subset of the programming language with a value-based model, essentially a DSL or AST of a sort.

It's not far from what you have to do with the DSLs of existing compiled containers though, most of which also do not provide IDE support or static type-checking.

One glaring issue with this approach is it implies that callback-based containers would be able to somehow reverse callables back to models, which is near-impossible to implement - either that or just accept the limitation that standard service providers can only extend standard service providers, a pretty harsh tradeoff for runtime configured containers that really takes away from the value of a standard.

I don't know if this is worth pursuing.

I'm not convinced something like this is feasible without both sides of the ecosystem making some tradeoffs - if the compiled side of the ecosystem isn't willing to give an inch on performance, essentially we're talking about a PSR that only works for compiled containers, aren't we?

Rasmus Schultz

Larry Garfield

unread,
Mar 23, 2024, 1:32:53 PMMar 23
to PHP-FIG
I think there's at least two loopholes to exploit here: Nothing prevents a compiled container from using factories, and in practice most do. And one can build behavior off of data structures, as long as the behavior pattern is known.

To spitball, so bear with me if this is a bit buggy or basic, suppose we had a service definition like so:

class ServiceDef {
/** @param array<string, DefRef> $constructorArgs */
public function __construct(
public string $class,
public array $constructorArgs = [],
) {}
}

Compiling that into a match statement is pretty straightforward:

foreach ($def->constructorArgs as $name => $defRef) [
$args[] = sprintf('%s: $this->get(%s)' , $name, $defRef->name;
}
$arm = "new \$def->class(" . implode(', ', $args);

// Append $arm into the match statement.

However, very similar code can happen at runtime in a runtime container, if it has a list of service definitions objects.

public function get(string $name): mixed {
$class = $this->defs[$name]->class;
foreach ($ $this->defs[$name]->constructorArgs as $name => $defRef) [
$args[$name] = $this->get($defRef->name);
}
return new $class(...$args);
}

It's a little more involved than just $this->factories[$name]($this), but not by that much. The performance should be in the same general neighborhood, if maybe slightly less. (Though if you're using a runtime container, you already don't care about performance optimization. Which is perfectly fine in some cases, but if you care about container performance, you're compiling it.)

Conversely, for a compiled container, you can do this:

class FactoryDef {
public function __construct(
public string $factory,
public string $method = '__invoke',
// And some kind of argument handling I'll skip for now to avoid typing, but it's largely the same as above.
) {}
}

For a runtime container, that's pretty easy, and barely any more than it would do with a closure today:

public function get(string $name): mixed {
$factory = $this->defs[$name]->factory;
$factory = $this->defs[$name]->method;
return $factory->$method();
}

(Some kind of real polymorphism can probably make this all a lot smoother, but that's not the point right now.)

For a compiled container, it could compile down to this:

public function get(string $name): mixed {
return $this->cache[$name] ??= match ($name) {
AFactory::class => new SomeFactory(),
AService::class => $this->get(AFactory::class)->theMethod(),
}
}

All existing compiled containers I know of support something like this. At least the good ones. :-)

Naturally the above is me just barfing ideas into an email, so it's not as robust as anything would need for production, but I think it makes the point that supporting both runtime and compiled containers is a solvable problem, just not a really trivial one.

I will also note that no service registration PSR would preclude supporting some other mechanism. So if a given runtime implementation wanted to also support $container->factory(Foo::class, fn(ContainerInterface $c) => new FooBar()), it absolutely could without in any way contradicting the spec.

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

I disagree here. Everything in the examples above is well type-checked. The language itself is providing the validation we need, and any violation would be a type error already. At worst, we could throw on some PHPStan/Psalm-style generic docblocks to make hinting the return of get() better, which is... already an existing issue for PSR-11. :-)

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

To be clear: The Core Committee has taken no position on this matter. Two members of the core committee have expressed their personal views on the matter, and their reservations, but are both positive on the concept. The other 10 have been so far silent.

But, and I hate to beat this dead horse but it's still wriggling, *the Core Committee is not the definition of "the community"*. If all 12 CC members agreed on a spec, but Symfony, PHP-DI, Laminas, and Laravel all completely ignored it, it would be a total failure. If Symfony, PHP-DI, Laminas, and Laravel all agreed on a common spec, and FIG ignored it, that would be a huge win for the ecosystem. FIG is here to help that second version happen, but its "blessing" means nothing unless implementers implement it.

In short, stop talking to me and start talking to the Symfony DI, PHP-DI, Laminas DI, and Laravel DI maintainers. :-) Convincing the Core Committee to charter a working group will be, I can virtually guarantee, 100x easier than convincing enough of the right people to sign on to the working group in the first place. That's the blocker, not figuring out a perfect architecture for me.

Also, other CC members: please weigh in here. Following this thread and weighing in with relevant, thoughtful comments is what you were elected for. Please do so. This should not be just me and Korvin.

--Larry Garfield

Rasmus Schultz

unread,
Mar 23, 2024, 2:05:51 PMMar 23
to php...@googlegroups.com
Yes, this is very similar to the code in my second post - and yes, no problem for basic factories like these to work in both compiled and run-time containers.

For extensions, it gets more challenging - a callback based container doesn't have models and can't generate them from functions.

In a nutshell, compiled containers are "data in, code out" - while run-time containers, in relation to data-based service providers, would be "have code, need data".

Parsing code would be the only way to get from functions to models, but that isn't going to make any sense in the context of e.g. Pimple.


--
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/SWIfYEkw89I/unsubscribe.
To unsubscribe from this group and all its topics, send an email to php-fig+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/01f1381f-42dd-4c9d-ac88-4813b9cf8db8%40app.fastmail.com.

Larry Garfield

unread,
Mar 23, 2024, 2:27:11 PMMar 23
to PHP-FIG
On Sat, Mar 23, 2024, at 12:55 PM, Rasmus Schultz wrote:
> Yes, this is very similar to the code in my second post - and yes, no
> problem for basic factories like these to work in both compiled and
> run-time containers.
>
> For extensions, it gets more challenging - a callback based container
> doesn't have models and can't generate them from functions.
>
> In a nutshell, compiled containers are "data in, code out" - while
> run-time containers, in relation to data-based service providers, would
> be "have code, need data".
>
> Parsing code would be the only way to get from functions to models, but
> that isn't going to make any sense in the context of e.g. Pimple.

(Please stop top posting. :-) )

To make sure we mean the same thing, what do you mean by "Extensions"? I'm imagining something like Symfony Compiler Passes where you can add "call this method after the object is created" commands and such, which, honestly, would be trivial to implement as PSR-14. (That kind of pattern was an explicit design goal.)

You are correct that there's no meaningful way to turn

$container->factory(Foo::class, fn(ContainerInterface $c) => new FooBar($c->get(Baz::class)));

into an AST, so that isn't something a unified mechanism could support. I think where we disagree is whether that's fatal or not. I really don't think it is, not when any reasonably performant container already doesn't support that, because it's compiled anyway. (Something simple like Pimple is still pretty fast at read time, but has the overhead of re-registration on every request that slows it down.)

But if a Provider returned an array like this:

class Provider {
public function getServices() {
return [
Baz::class => new Service(Baz::class),
FooBar::class => new Service(FooBar::class, [new DefRef(Baz::class)],
];
}
}

That would be quite easy for Pimple-like containers to support, and map back into a runtime model:

public function registerProvider(Provider $provider) {
foreach ($provider->getServices() as $name => $def) {
$this->factories[$name] = static fn(ContainerInterface $c) => new ($def->class)($def->args);
// OK, that line would be slightly more complicated for the arg handling, but you get the idea.
}
}

And then its get() method looks exactly like it does today already. No issue.

The *only* thing that is unsupportable in the standard itself would be

class Provider {
public function getServices() {
return [
Foo::class => fn(ContainerInterface $c) => new FooBar($c->get(Baz::class))),
];
}
}

Which I am completely OK with not supporting, for the flexibility that gets us. Even without mentioning compilation, it wouldn't allow for other providers to modify the arguments; they could only add post-creation method calls. An AST approach would still allow a runtime container to have "alter hooks" if it wanted.

--Larry Garfield

Rasmus Schultz

unread,
Mar 23, 2024, 5:22:21 PMMar 23
to php...@googlegroups.com
> To make sure we mean the same thing, what do you mean by "Extensions"? 

Explained in my second reply, which includes this code:


Extensions modify existing definitions.

In e.g. Pimple, an existing service definition might only exist as a function, defined using Pimple's API - you can't turn that into a service definition model.

So yeah, standard service providers would be able to modify definitions from other standard service providers - but they won't be interoperable with service definitions created with the container's own callable-based API.

I don't know how to explain that any better. Try reading my second reply again.

Rasmus


--
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/SWIfYEkw89I/unsubscribe.
To unsubscribe from this group and all its topics, send an email to php-fig+u...@googlegroups.com.

Steve McDougall

unread,
Mar 23, 2024, 5:24:49 PMMar 23
to php...@googlegroups.com
I have to admit, my knowledge of this topic is lacking. However, from what Larry said - I am in agreement with his points. While I love the idea of this proposal, I can't help with it. But, I am happy to see Larry is on the ball here.

Kind Regards
Steve McDougall

Rasmus Schultz

unread,
Mar 24, 2024, 6:02:22 AMMar 24
to php...@googlegroups.com
There's nothing wrong with anything Larry posted.

But I already knew all of that would work.

The issue is extensions - if you want to change an argument to a service definition, that requires the original service definition, so your extension can change it and return a new one.

If you're in Pimple, and you registered the original service using a callback, there is no service definition.

How do you get from a function literal to a service definition model instance? You can't.

No runtime configured container would be able to provide service definitions as objects, unless the original service definition also came from a standard service provider.

Service definitions as data/models are fundamentally incompatible with containers that use native functions.

I don't think you can fix that.

You could have a service providers PSR without extensions, I suppose. It's a pretty important feature to just omit though. Every container I know of supports extensions.

Larry Garfield

unread,
Mar 24, 2024, 11:31:06 AMMar 24
to PHP-FIG
On Sun, Mar 24, 2024, at 1:47 AM, Rasmus Schultz wrote:
> There's nothing wrong with anything Larry posted.
>
> But I already knew all of that would work.
>
> The issue is extensions - if you want to change an argument to a
> service definition, that requires the original service definition, so
> your extension can change it and return a new one.
>
> If you're in Pimple, and you registered the original service using a
> callback, there is no service definition.
>
> How do you get from a function literal to a service definition model
> instance? You can't.
>
> No runtime configured container would be able to provide service
> definitions as objects, unless the original service definition also
> came from a standard service provider.
>
> Service definitions as data/models are fundamentally incompatible with
> containers that use native functions.
>
> I don't think you can fix that.
>
> You could have a service providers PSR without extensions, I suppose.
> It's a pretty important feature to just omit though. Every container I
> know of supports extensions.

So... I'm not sure what your point is here? You are correct, if a service is registered with a closure, it limits what an extension could do to it. But that's not something a PSR *can* solve. That's just how the language works.

The answer, whether we're talking compiled or runtime container, is for the PSR to define services as AST definitions (the structs like we were talking about before), and make the extensions manipulate those AST definitions. If a particular container *also* wants to support closure-based registration, the PSR wouldn't prevent that, but those would then be incompatible with extensions and the container just wouldn't pass those to extensions. That's it's choice to support additional functionality beyond what the PSR specifies, and if the language itself limits what that functionality can do, um, OK? Not to sound flippant, but that's not our problem. Is there something I'm missing here?

Another option, of course, is to break up the interface. Have a ServiceProvider interface with one method, an ExtensionProvider interface with one method, hell we could even consider a ClosureServiceProvider interface if we really really wanted. Let that be opt-in, but with the understanding that it won't be used with extensions. Individual implementations could opt-in to whichever functionality they wanted, and a provider that offers only services or only extensions wouldn't need a dummy function for the other part. Even without talking about closures, that would probably be a good idea.

--Larry Garfield

Chuck Burgess

unread,
Mar 24, 2024, 11:48:44 AMMar 24
to php...@googlegroups.com
--
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 view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/01e63eae-fef3-490a-961a-c95be1ff0fa0%40app.fastmail.com.


This proposal to build a PSR around this does seem reasonable and widely usable to me, so it would get a favorable entrance vote from me.  Like Larry said, if a working group can be convened, the continued technical aspects of the discussion can continue there.
CRB

Rasmus Schultz

unread,
Mar 24, 2024, 1:09:29 PMMar 24
to PHP Framework Interoperability Group
But a PSR *can* solve this problem, and the previous PSR did.

It didn't satisfy the performance constraints of compiled containers, but it did provide full interop with both compiled and runtime containers.

So we are talking about:

- a substantially more complex design
- partially recreating a model of the language within the language
- considerable limitations since every allowed language feature has to be modeled
- no proper IDE support or static analysis
- lacking interop for extensions in runtime configured containers

All of this so the compiled containers can avoid calling a factory function.

Since this won't fully interop with runtime configured containers, I would say maybe it's time to reconsider this idea:


So, an annotated factory class format, designed to enable compiled containers to inline the factory function body.

The idea has come up numerous times, most recently here:


It might look something like this:


This gets proper IDE support and static analysis for things like constructor calls - it avoids recreating a model of the language.

It's more work for compiled containers to inline the factory functions, sure - but they're doing the work at compile-time anyway. And it becomes optional - if they don't want to inline factory functions, they don't have to.

Since compiled containers need an AST-like model anyway, let them use an existing, feature-complete language model, e.g. nikic/php-parser, instead of inventing another one.

Let users author their providers in the language instead of manipulating a model - this should be a lot easier for everyone to understand, even if they don't understand how compiled containers make this work.

I think this could be designed to fully interop with both types of containers, including extensions in runtime configured containers.

It's just functions with some metadata, which you can parse at runtime or statically, making as many optimizations as you like.

Ken Guest

unread,
Mar 24, 2024, 5:46:47 PMMar 24
to php...@googlegroups.com
This all makes sense in at least general terms, and having a PSR to cover this would be great.

It would be wonderful to see a working group formed to help advance this, once/if this comes to an entrance vote stage I think I'll be voting in favour of it. 

Kind regards, 

Ken

--
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 view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/79061441-ff89-4052-a8d9-0eab9329fdedn%40googlegroups.com.

Alexander Makarov

unread,
Apr 2, 2024, 5:48:35 PMApr 2
to PHP Framework Interoperability Group
Reading https://github.com/container-interop/service-provider/issues/71, and it makes perfect sense to me to reboot the WG. Something great might come out of it.

Cees-Jan Kiewiet

unread,
Apr 4, 2024, 4:48:46 PMApr 4
to php...@googlegroups.com
On the surface this PSR looks very valuable. If I can provide an easy way for my packages to be wired up, I'm all in. But this is also a very abstract PSR, so I personally would love to see easy to grasp and get up to speed examples with say a PSR-18 HTTP Client. Coming from a non-blocking PHP environment with ReactPHP, compiled code is always preferred, even with run times of days/weeks/months. Since more classic deployment way such as FPM will also benefit from that I'm curious if that should be the default for this PSR.



--

Rasmus Schultz

unread,
Apr 5, 2024, 12:55:30 AMApr 5
to php...@googlegroups.com
> But this is also a very abstract PSR, so I personally would love to see easy to grasp and get up to speed examples with say a PSR-18 HTTP Client.

It is just an idea at this stage - yes, an example would definitely be required, likely something a bit more complex than a PSR-18 client, as it needs to demonstrate both factories and extensions, how to do an alias, etc.

> Coming from a non-blocking PHP environment with ReactPHP, compiled code is always preferred, even with run times of days/weeks/months.

Compiled code? in PHP? please explain.

> Since more classic deployment way such as FPM will also benefit from that I'm curious if that should be the default for this PSR.

what should be the default?

sorry, you lost me. 😅


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/SWIfYEkw89I/unsubscribe.
To unsubscribe from this group and all its topics, send an email to php-fig+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/CAPY1sU9BkPLZR4u%2B_98jRt1y0HMi00BxTsUU41zHGiOi2ivybA%40mail.gmail.com.

Jaap van Otterdijk

unread,
Apr 5, 2024, 3:36:05 AMApr 5
to PHP Framework Interoperability Group
Hi, 

Thank for your hard work, it's really amazing to see the amount of work you did. Thank you very much for this!

I subscribed to the GitHub repo and will try to understand everything what's in there. But given the level of complexity I think it will take some time to get through this.

Jaapio

Op vr 12 jan. 2024 18:00 schreef Rasmus Schultz <ras...@mindplay.dk>:
--
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.

Rasmus Schultz

unread,
Apr 5, 2024, 6:16:04 AMApr 5
to php...@googlegroups.com
Probably, don't bother with anything in the repository, as we're basically talking about starting over with an entirely different proposal. 😅


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/SWIfYEkw89I/unsubscribe.
To unsubscribe from this group and all its topics, send an email to php-fig+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/CABS-R89wSr0WfJqvu12ma%2BShfLWrNzGVRm5GcX67hiNMxmacvQ%40mail.gmail.com.

Cees-Jan Kiewiet

unread,
Apr 7, 2024, 4:59:29 PMApr 7
to php...@googlegroups.com
>> But this is also a very abstract PSR, so I personally would love to see easy to grasp and get up to speed examples with say a PSR-18 HTTP Client.

> It is just an idea at this stage - yes, an example would definitely be required, likely something a bit more complex than a PSR-18 client, as it needs to demonstrate both factories and extensions, how to do an alias, etc.

Awesome 👍.

>> Coming from a non-blocking PHP environment with ReactPHP, compiled code is always preferred, even with run times of days/weeks/months.

> Compiled code? in PHP? please explain.

Compiled as in we don't have to scan everything and figure out what should be in the container. Doing that in broadcast where https://github.com/WyriHaximus/php-broadcast/blob/master/etc/AbstractListenerProvider.php is used as a template and on composer install it scanes for listeners and a templated outcome of it is put into https://github.com/WyriHaximus/php-broadcast/tree/master/src/Generated and will contain https://gist.github.com/WyriHaximus/21622fc090f15d18a227f3547f8022c5 for that package itself. It's about compiling information into a file and not about compiling source code into an executable.

>> Since more classic deployment way such as FPM will also benefit from that I'm curious if that should be the default for this PSR.

> what should be the default?

Compiled containers.

> sorry, you lost me. 😅

It's what I do 😜

Rasmus Schultz

unread,
May 2, 2024, 5:42:18 AMMay 2
to PHP Framework Interoperability Group
To move the discussion along, I decided to do write a preliminary proposal and create a working prototype:


In this repository, you will find:

- Documentation explaining the role and usage of each attribute.
- An implementation of the proposed attributes.
- A use-case example with some mock dependencies and a factory class using the attributes.
- A sample implementation of a reflection-based FactoryModel reflecting declared services and extensions.
- A minimal example run-time configured PSR-11 container consuming the example factory.
- Basic tests to demonstrate a working proof-of-concept.

I realize your primary concern is with compiled containers and whether this approach would work - but this is laying the ground work, and it's much easier to demonstrate than building support for a compiled container, and we need to start somewhere.

If someone wants to fork this project and attempt an example with a compiled container (either a minimal example of a compiled container from scratch, or an integration with an existing compiled container) please feel free - I'm not deeply familiar with any compiled container myself.

For personal reasons, I probably won't be able to do much more in the near future, but I'm hoping this moves the discussion ahead and maybe inspires someone else to dig in. :-)

Cheers,
  Rasmus

Jaap van Otterdijk

unread,
May 3, 2024, 2:06:58 AMMay 3
to PHP Framework Interoperability Group
It seems fairly easy to me to implement this proposal in for example symfony DI container. If this would be interesting I can give it a shot to implement a draft for that. 

Maybe that will also reveal some issues. But as I see it right now I do not see any blocking issues for that.

Op do 2 mei 2024 11:42 schreef Rasmus Schultz <ras...@mindplay.dk>:

Emmanuel Adesina

unread,
May 3, 2024, 5:28:29 AMMay 3
to php...@googlegroups.com

Hello, Jaap van Otterdijk and everyone

I have been following the entire conversation and it has been a great learning experience for me, and I hope that in the nearest future, I will be able to contribute also.

I'd like to ask if you could allow me join in a on a call with you when you are working on the draft, I hope to learn what it means to implement an example of the draft.

Currently, I am a staff developer building with Laravel, and I hope to build a framework of my own from soon, I hope this would help me understand some concepts that has been talked about in this thread and help me improve.

Thank you.

To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/CABS-R8_LaZi5Me4Ly-FuE-WEcu4BjPnZdEFeRrxaX-TRBge8qg%40mail.gmail.com.
--

Emmanuel Adesina

Teners

Think green! Please do not print thism mail unless absolutely necessary!

Reply all
Reply to author
Forward
0 new messages