Hi internals,
I would like to ask for early feedback on an idea I have been
exploring recently: runtime modules.
I put my initial idea in writing here:
https://news-web.php.net/php.internals/127343
I have recently been playing around with the idea locally.
It seems technically doable, but it touches enough parts of the engine
that I would like to check whether this direction makes sense before
writing a proper RFC.
The problem has been discussed many times over the past few years. I
am now looking at it from real-life package level usage with Composer,
and how to achieve package symbol isolation in a way that would
minimally impact the ecosystem.
The rough idea is to have request-lifetime runtime modules.
A runtime module would be a named internal unit with its own userland
class/function/constant tables.
Code running in a module would define symbols into that module, and
symbol identity for module-owned code would effectively become
(module, symbol_name).
The root context would keep the existing behavior, and conceptually we
can view it as a root/default module.
Root context userland symbols would not automatically be visible to
runtime modules.
A module would see its own symbols, PHP internal/builtin symbols, and
symbols from its direct dependencies. This maps fairly naturally to a
Composer style model where a package depends on PHP and on other
packages, but not implicitly on application/root symbols.
Just to make it clearer, composer related, I see it this way:
- each package would be defined in its own module
- module dependencies would map directly from Composer package dependencies
One possible userland API shape I have been using to experiment with is:
module_add_dependency(string $module): void
module_run(string $module, Closure $closure): mixed
module_add_dependency()declares a module dependency for the current module.module_run()executes the passed closure in the specified module context.- All execution contexts have attached to them the module they were
defined for, and any new symbols defined while an execution runs would
have the same module. The exception ismodule_run(), which overrides
the closure module before running it.
The exact API is not the main point at this stage, but I aim to keep it minimal.
I am more interested in whether the model itself is reasonable.
Technically, the engine would need to track module ownership for
compiled code and symbols, keep per-module symbol tables and direct
dependency lists, and make lookup, type resolution, autoload, and
include_once behavior module aware.
Also, due to the dynamic nature of PHP, objects can be passed to
module code that might not have their class known, but I do not see
this as a blocker.
The design I have been considering also rejects visible shadowing:
unrelated modules may define the same symbol name, but adding a
dependency or declaring a later symbol would fail if it makes two
different symbols with the same name visible from the same context.
I would like feedback on this package oriented runtime module model,
especially whether you see any major technical blockers or design
flaws.
I aim to work on turning this into a complete RFC within the next 6
months, but it might take more. I am far from experienced with
internals details, and I will most probably need guidance and help
with the implementation.
It is not something I want to rush, as this could be an important
addition to the language and we need to get it right.
Thank you,
Alex
The rough idea is to have request-lifetime runtime modules.
A runtime module would be a named internal unit with its own userland
class/function/constant tables.
Code running in a module would define symbols into that module, and
symbol identity for module-owned code would effectively become
(module, symbol_name).
The root context would keep the existing behavior, and conceptually we
can view it as a root/default module.
Root context userland symbols would not automatically be visible to
runtime modules.
Hi Alex,
If I understand right, this is what I have suggested in previous threads
could be called "containers". The reason I prefer that name is that it
frames expectations of who needs to make changes: the person
distributing a piece of code, or the person consuming it.
In most contexts, terms like "module", "package", "library", etc, refer
to ways to distribute a piece of code; structuring code, adding
metadata, etc, so that the code can be combined with others to produce
larger pieces of functionality. A "container", in the sense of Docker,
Podman, Kubernetes, etc, is a way to consume other people's code;
taking a complete configured application and isolating it without
modifying each internal part.
The example I've been using is a WordPress plugin which wants to use a
specific version of Guzzle, without colliding with other plugins. To do
that, it needs to isolate not just Guzzle itself, but a tree of at least
a dozen other packages which Guzzle depends on. If every one of those
packages needs to be altered in some way, as implied by the term
"module", the chance of success is low.
On the other hand, if the WordPress plugin can create a "container"
where all of those packages run unchanged, then the feature would
immediately give access to thousands of existing packages.
One possible userland API shape I have been using to experiment with is:
module_add_dependency(string $module): void module_run(string $module, Closure $closure): mixed
module_add_dependency()declares a module dependency for the current module.module_run()executes the passed closure in the specified module context.
Given the above, I'm not sure what "module_add_dependency" would do;
what is the difference between "depending on" something, and "running"
that thing?
I also don't think using a string as an identifier is useful or
necessary; avoiding reliance on global names is the whole point of the
exercise, after all.
Instead, how about this?
class ExecutionContainer {
public function run(callable $code): mixed;
}
Creating a new container initialises a new symbol table, autoloader
stack, etc, and gives you an object referring to them. Calling that
object's run() method then executes some code in the context of that
container, and returns its result.
Also, due to the dynamic nature of PHP, objects can be passed to
module code that might not have their class known, but I do not see
this as a blocker.
I think this is actually the biggest challenge: what happens when
objects are passed between containers?
To use the previous example: as an initialisation step, the WordPress
plugin might want to set up an API client inside its container; later,
it might want to make use of that API client, plus an object passed to
it by a WordPress hook.
The containers are inside the same thread, so in principle there's no
problem referencing object handles which are "owned by" a different
container. But what is the type of those objects? How do they respond to
get_class(), instanceof, etc?
Perhaps there are things we can learn from other languages like Java's
"isolated ClassLoader" which I mentioned on another thread?
https://www.javathinking.com/blog/what-is-an-isolated-classloader-in-java/
The design I have been considering also rejects visible shadowing:
unrelated modules may define the same symbol name, but adding a
dependency or declaring a later symbol would fail if it makes two
different symbols with the same name visible from the same context.
I think this would mean in practice that every container should start
with an empty symbol table (or rather, one with only built-in symbols).
If a container starts with all currently-loaded symbols, it would no
longer have any control over name collisions, so would be useless.
You could perhaps have a way to "import" and "export" specific symbols,
so that e.g. "Psr\Log\LoggerInterface" refers to the same thing in two
different containers; but I think this would need to explicit, so the
container always ran consistently.
I would like feedback on this package oriented runtime module model,
especially whether you see any major technical blockers or design
flaws.
I think this would be a powerful feature, but one that the vast majority
of PHP applications won't use. So the key to success will be minimising
the impact in performance and engine complexity.
Regards,
--
Rowan Tommins
[IMSoP]
Hi, Alex and Rowan!
I think this is actually the biggest challenge: what happens when objects are passed between containers?
To use the previous example: as an initialisation step, the WordPress plugin might want to set up an API client inside its container; later, it might want to make use of that API client, plus an object passed to it by a WordPress hook.
The containers are inside the same thread, so in principle there's no problem referencing object handles which are "owned by" a different container. But what is the type of those objects? How do they respond to
get_class(), instanceof, etc?Perhaps there are things we can learn from other languages like Java's "isolated ClassLoader" which I mentioned on another thread? https://www.javathinking.com/blog/what-is-an-isolated-classloader-in-java/
I actually really like the idea of some sort of "containers" in general,
because it is quite a common problem in my practice when we have a
more-or-less big monolithic app and eventually come to conflicting
dependencies.
As for passing objects between containers, it does not seem like an
unsolvable problem. Even though our thoughts are very abstract for now,
generally speaking there could be some matching of objects in
inter-container usage. Like, if "my" container is now using
\My\Namespace\SomeObject from my dependencies (let it be, say,
myvendor/mylib: 1.0), when receiving such an object from another
container, it could be known what exact dependency it is from. And if it
is also myvendor/mylib: 1.0 - there should be no problems. If versions
don't match, there could be some explicit inter-dependency mapping defined
by the user, or an explicit runtime error if no mapping was provided.
Such an inter-container mapping mechanism would require careful designing
of course, but technically it does not look unsolvable. Generally
speaking, we could have the mentioned \My\Namespace\SomeObject loaded both
from v1.0 and say v1.1 so we could map one to another on the calling side.
Here comes the quirk that PHP itself is not aware about the composer and
its structure. But PHP can always know where exactly a specific class was
sourced from, so we could use it as a distinguisher. I bet distinguishing
by filename would be a very bad design though. But if PHP allowed to
somehow "tag" loaded classes, it would be handy for containers AND
composer.
Like, when "starting" a new container, the only thing we need to provide is
a class-loader, which would presumably "tag" the loaded classes in a known
manner. In the case of composer, it could tag them by version. In our code,
we could import all needed classes with some advanced syntax with
specifying the "tag". As an example from the top of my head:
use \My\Namespace\SomeObject tagged "..." [as SomeObjectV1], explicitly
telling which version we need. And we could have both versions imported
this way (and map one to another, accordingly). Autoload will receive this
"tag" (if provided) and load a corresponding class. The power of containers
would come with carefully designed defaults for these class versions when
no version was explicitly stated (but container-based autoloader already
knows where it is rooted from).
These are still very rough thoughts though, but with deeper thinking, I believe
it can evolve what Alex and you are suggesting.
Regards,
Alexander Egorov.
As for passing objects between containers, it does not seem like an
unsolvable problem. Even though our thoughts are very abstract for now,
generally speaking there could be some matching of objects in
inter-container usage. Like, if "my" container is now using
\My\Namespace\SomeObject from my dependencies (let it be, say,
myvendor/mylib: 1.0), when receiving such an object from another
container, it could be known what exact dependency it is from. And if it
is also myvendor/mylib: 1.0 - there should be no problems. If versions
don't match, there could be some explicit inter-dependency mapping defined
by the user, or an explicit runtime error if no mapping was provided.
Just knowing the version / source of an individual class is not enough.
At the very least, you need a fingerprint that also includes the
versions of the classes it inherits from, the interfaces it implements,
and the traits it uses.
And then what about other relationships, like the classes they create
and return? For example:
class WidgetFactory {
public function makeWidget(): Widget { ... }
}
$factoryA = new WidgetFactory();
$factoryB = $someContainer->run(fn() => new WidgetFactory);
$widgetA = $factoryA->makeWidget();
$widgetB = $factoryB->makeWidget();
If the container contains the same version of WidgetFactory, but a
different version of Widget, what objects do I end up with?
Like, when "starting" a new container, the only thing we need to provide is
a class-loader, which would presumably "tag" the loaded classes in a known
manner. In the case of composer, it could tag them by version. In our code,
we could import all needed classes with some advanced syntax with
specifying the "tag". As an example from the top of my head:
use \My\Namespace\SomeObject tagged "..." [as SomeObjectV1], explicitly
telling which version we need. And we could have both versions imported
this way (and map one to another, accordingly).
This feels like it's moving much more back to the "module" idea -
changes which have to propagate deep into existing code before you can
use them.
To me, the guiding principle of containers needs to be that the
configuration all exists at the boundary of the container. I'm
thinking of the EXPOSE and VOLUME keywords in a Dockerfile, for example.
--
Rowan Tommins
[IMSoP]
On Sat, Jun 27, 2026 at 9:00 PM Rowan Tommins [IMSoP]
imsop.php@rwec.co.uk wrote:
This feels like it's moving much more back to the "module" idea -
changes which have to propagate deep into existing code before you can
use them.
No, not at all. That's what I was implying by saying about carefully
designed defaults. Existing code would still behave as it knows nothing
about containers, or better say: knows nothing about the container it is
run in. It intersects with what Alex initially wrote about each module
defining its own symbols. But I think what I describe is not exactly the
same with what Alex proposes, even though we have some things in common.
I'll try to describe your example better further, but now I would like to
"translate" Alex's ideas into my terminology and vise-versa. What we have
in common is that all symbols would be identified not just by name, but
also by "module", or as in my terms: a "tag". Like, in your example there
would be not just WidgetFactory, but ("A", WidgetFactory) and ("B",
WidgetFactory) - two separate classes but with the same name. What is
"module" in Alex's terms is a "tag" in my examples. The key moment here is
what we would use "by default" if the version is not explicitly defined
(like it would be in any currently existing code).
Now let's get back to your example. First, I think it is not exactly
correct, because if you instantiate exactly the same WidgetFactory, it
would most probably give out the same Widget as a result. So, I'll
"rephrase" your example. Let's say, we already use the Widget class from
some library my/widgets: 2.0. But in our project we also added another
library (let it be vendor/widgetfactory) which provides the factory, and it
uses the my/widgets: 1.0. And this 1.0 still has the "same" Widget class.
So, we try to run this WidgetFactory inside a container, like this:
// It'll be Widget from 2.0, let's omit for now how do we determine that
use My\Widgets\Widget;
$ourWidget = new Widget();
$container = new Container(...); // we also provide an autoloader
$anotherWidget = $container->run(static fn() => ...
$widgetFactory->makeWidget());
Here WidgetFactory will be run inside the container with its own set of
symbols and would not know anything about our Widget 2.0. The autoloader
supplied to the container would presumably know that it is "rooted" from
vendor/widgetfactory, so it will only load widgets: 1.0. It works
internally as it would normally do, probably not even knowing that PHP
introduced the concept of containers.
Now for the result of the invocation. As I've said, symbols are now
identified not only by name, but also by the "tag". In this case $ourWidget
is not just an instance of some \My\Widgets\Widget, it is an instance of
("2.0", \My\Widgets\Widget), where "2.0" would be the default for our
current "context" (or also a container, if we're inside one, or if the root
context is treated itself as just a root container). In other words (or in
pseudo-code) $ourWidget instanceof Widget === $ourWidget instaceof ("2.0",
Widget) in current context.
But $anotherWidget is not instanceof Widget in the current context. It is
instanceof ("1.0", Widget); How can we work with it then? As I've said, we
could import 1.0 version alongside our "default" version:
use My\Widgets\Widget; // [as Widget]
use My\Widgets\Widget tagged "1.0" as WidgetV1;
// ....
// $anotherWidget instaceof WidgetV1 === true;
function convertWidget(WidgetV1 $oldWidget): Widget {
// .... perform the mapping to "our" widget
}
How do we know that? Well, that's the reason why we're containerizing
conflicting code: we know exactly that it uses another version of the same
class and we're expecting it as a return. If our containerized code does
not return any object (like, it's purely executing code or it returns basic
data-type), we don't need any "mapping" at all: we just execute that code
inside its own "context".
Now as for the mentioned "defaults" which are a crucial part of it. In our
main "context" when loading Widget class, our autoloader must know where it
is rooted from and what version to load by default. Inside the container
with WidgetFactory, the default would be "1.0", and autoloader should know
that. There could be multiple ways to configure an autoloader for that. If
speaking about composer, we could tell it to "root" from
"vendor/widgetfactory", and since it by itself requires "my/widgets: 1.0",
it'll load a corresponding version 1.0 of the Widget. Or in a more
straightforward approach, we could explicitly instruct the loader to load a
class from my/widgets: 1.0 when it needs \Me\Widgets\Widget.
Of course there are lots of nuances to keep in mind, but the letter is
already too big for that. Besides, it feels like I've already started to
draft my own RFC for that :) But I hope I could explain what I see under
these "containers" or "modules" as Alex initially suggested. Except that I
don't see the need to somehow provide "dependencies" on PHP-level itself.
Now let's get back to your example. First, I think it is not exactly
correct, because if you instantiate exactly the same WidgetFactory, it
would most probably give out the same Widget as a result.
That "most probably" is doing a lot of work. Let me give a real-world
example:
google/apiclient depends on monolog/monolog, which in turn depends on
psr/log
The following code will give you an object which is an instance
Monolog\Logger, and an instance of Psr\Log\LoggerInterface:
$client = new \Google\Client;
$logger = $client->getLogger();
But:
- the current version of google/apiclient can use either version 2.x or
3.x of monolog/monolog - the 3.x series of Monolog is compatible with both version 2.0 and 3.0
of psr/log - the 2.x series is compatible with 1.0.1, 2.0, and 3.0 of psr/log
So even with the exact same version of the \Google\Client class, there
are 6 different combinations which might be installed.
Now what happens if I instead run this:
$client2 = $container->run(fn() => new \Google\Client);
$logger2 = $client2->getLogger();
Even if I detect somehow that $client2 is the same version of
Google\Client I'm using, $logger2 can be any of the 6 different
combinations.
Now for the result of the invocation. As I've said, symbols are now
identified not only by name, but also by the "tag". In this case $ourWidget
is not just an instance of some \My\Widgets\Widget, it is an instance of
("2.0", \My\Widgets\Widget), where "2.0" would be the default for our
current "context" (or also a container, if we're inside one, or if the root
context is treated itself as just a root container). In other words (or in
pseudo-code) $ourWidget instanceof Widget === $ourWidget instaceof ("2.0",
Widget) in current context.
This sounds reasonable, if you replace the "2.0" tag with some kind of
identifier for a whole container. In fact, this is apparently how Java
works: every class definition is silently tagged with the "ClassLoader"
which defined it, and two classes can have the same name but different
ClassLoader tags.
But $anotherWidget is not instanceof Widget in the current context. It is
instanceof ("1.0", Widget); How can we work with it then? As I've said, we
could import 1.0 version alongside our "default" version:use My\Widgets\Widget; // [as Widget]
use My\Widgets\Widget tagged "1.0" as WidgetV1;// ....
// $anotherWidget instaceof WidgetV1 === true;
If the idea is that two containers somehow know to give a class entry
the same tag, I don't think it's possible; in the example above, the
"tag" for Google\Client needs to describe not just its own version, but
which of the six possible combinations its getLogger() method will return.
Again, if the "tag" somehow identifies a container, this could work;
but as I mentioned in my first reply, identifying containers by name
seems like it would just re-create the name collision problem we're
trying to solve.
Now as for the mentioned "defaults" which are a crucial part of it. In our
main "context" when loading Widget class, our autoloader must know where it
is rooted from and what version to load by default. Inside the container
with WidgetFactory, the default would be "1.0", and autoloader should know
that. There could be multiple ways to configure an autoloader for that. If
speaking about composer, we could tell it to "root" from
"vendor/widgetfactory", and since it by itself requires "my/widgets: 1.0",
it'll load a corresponding version 1.0 of the Widget. Or in a more
straightforward approach, we could explicitly instruct the loader to load a
class from my/widgets: 1.0 when it needs \Me\Widgets\Widget.
This is what I mean about "module-like" - the code inside the container
has to be modified to be aware of which classes belong to which
packages, and how to annotate them with the correct versions. And what
if some code inside the container has been edited, so doesn't correspond
to any official version?
A DockerFile defines exposed ports, and file system mount points; it
doesn't define a list of packages and their versions which another
container can reach in and interact with.
--
Rowan Tommins
[IMSoP]
On Sun, Jun 28, 2026 at 1:23 AM Rowan Tommins [IMSoP]
imsop.php@rwec.co.uk wrote:
That "most probably" is doing a lot of work. Let me give a real-world
example:google/apiclient depends on monolog/monolog, which in turn depends on
psr/logThe following code will give you an object which is an instance
Monolog\Logger, and an instance of Psr\Log\LoggerInterface:$client = new \Google\Client;
$logger = $client->getLogger();But:
- the current version of google/apiclient can use either version 2.x or
3.x of monolog/monolog- the 3.x series of Monolog is compatible with both version 2.0 and 3.0
of psr/log- the 2.x series is compatible with 1.0.1, 2.0, and 3.0 of psr/log
So even with the exact same version of the \Google\Client class, there
are 6 different combinations which might be installed.Now what happens if I instead run this:
$client2 = $container->run(fn() => new \Google\Client);
$logger2 = $client2->getLogger();Even if I detect somehow that $client2 is the same version of
Google\Client I'm using, $logger2 can be any of the 6 different
combinations.
Thanks for the example, it is a very interesting edge-case. But I think
even this situation is solvable. First, why do I consider it an edge-case.
In my vision, the primary use of containers would be through supplying
proper autoloaders for them. In this case, all code inside the container
and which relies by default on autoloading (which is the majority of cases)
will load all classes in a way we expect with corresponding tags. And so we
would be able to easily match all classes.
Your case differs from that because, as I understand it, it loads classes
manually depending on the conditions. In this case those classes would be
loaded with some meaningless tag for us as a creator of the container, in
other words $logger2 would be an instance of (<some_tag>, Logger). But we
could theoretically still match this class against known versions to us.
For this PHP could introduce alternatives to strict "instanceof". Let's
consider this example: we have some class defined in src/MyClass.php, and
it has been loaded twice with different tags: tag1 and tag2.
In this case $a = ("tag1", MyClass) is not an instance of ("tag2", MyClass)
in a strict (or formal) way. But since it is still the same class
internally (and PHP would be aware of that), we could have a less formal
"instanceof" which would give us true when comparing them. So, coming back
to your example, if we need to strictly map returned logger to our own
symbols, we could try to match it against all known/expected versions of
Logger, like:
use Monolog\Logger\Logger; // our "default" logger
use Monolog\Logger\Logger tagged "1.0" as LoggerV1;
use Monolog\Logger\Logger tagged "2.0" as Logger V2;
$client2 = $container->run(fn() => new \Google\Client);
$logger2 = $client2->getLogger();
match (true) {
$logger2 instanceof Logger => ...., // here I mean less formal instanceof
$logger2 instanceof LoggerV1 => ...,
$logger2 instanceof LoggerV2 => ...,
}
Even though $logger2 would not strictly match to our imported symbols
because it has a meaningless tag to us, we could match it less strictly. In
fact, as it seems to me, such less formal comparison would make sense as a
default behaviour for instanceof.
If the idea is that two containers somehow know to give a class entry
the same tag, I don't think it's possible; in the example above, the
"tag" for Google\Client needs to describe not just its own version, but
which of the six possible combinations its getLogger() method will return.
Well, as I've said, in my vision autoloaders play an important role here
because it is them who decide which tag to attach to loaded classes. And in
this case all classes loaded inside the container could have meaningful
tags for us as a calling side.
Again, if the "tag" somehow identifies a container, this could work;
but as I mentioned in my first reply, identifying containers by name
seems like it would just re-create the name collision problem we're
trying to solve.
In my vision, "tag" does not really identify the container. It is whatever
we wish semantically. For example, let's say we're using Monolog\Logger 2.0
as our "default" logger. But we also use another library which is designed
for 1.0. And for us that Logger from version 1.0 would be identified as
("1.0", Logger) according to the semantics we've chosen and which is
reflected in our autoloader.
While running inside the container, this library will create its own logger
as it would do without any container (assuming it relies on autoloading).
For it it will still just be a Monolog\Logger\Logger class in exactly the
same way as it works now. And it will eventually return this logger to us.
But since it was loaded using the autoloader which we gave to the
container, for us it will already be not just some "unknown" Logger, but
exactly ("1.0", Logger) in our "host" semantics. And as I've described
earlier, even if the library does not rely on autoloading, "their" logger
would not be that meaningful to us directly, as it would have whatever tag.
But we could still match that (<whatever>, Logger) to our own ("1.0",
Logger).
This is what I mean about "module-like" - the code inside the container
has to be modified to be aware of which classes belong to which
packages, and how to annotate them with the correct versions. And what
if some code inside the container has been edited, so doesn't correspond
to any official version?A DockerFile defines exposed ports, and file system mount points; it
doesn't define a list of packages and their versions which another
container can reach in and interact with.
I hope I could explain why no existing code has to be modified to be usable
inside containers, at least as I see it. No need to explicitly "expose"
anything from 3rd-party code. All the "management" would be done on the
calling side. But speaking about the example you provided with google
client, it is a good example why such "polymorphic" libraries themselves
would benefit from such class-versioning and containerization and would
themselves want to switch to it to get rid of all that compatibility
shenanigans.
In my vision, the primary use of containers would be through supplying
proper autoloaders for them. In this case, all code inside the container
and which relies by default on autoloading (which is the majority of cases)
will load all classes in a way we expect with corresponding tags. And so we
would be able to easily match all classes.
OK, then your vision is fundamentally different from mine. I don't want the container to know anything about how the code inside it works.
Remember that autoloading is something that any library can implement its own version of. There are autoloaders which fetch from PHAR files, autoloaders which generate code on demand, and so on.
Version tags break the abstraction of the container, and I don't think they solve a real problem.
Your case differs from that because, as I understand it, it loads classes
manually depending on the conditions.
No, just that the container configuration doesn't know anything about "packages" or "versions". You run 'require "$someDir/vendor/autoload.php";' inside the container as normal. Or maybe you don't, and you just list a big bunch of includes for some reason. The container doesn't care.
As a human author, you maybe know what versions of packages are in your container - depending how tightly you've configured them - but you have no idea what someone else might include in their container.
If you include version 2.11 of Monolog and version 2.0 of psr/log, but some other container includes version 2.11 of Monolog and version 3.0 of psr/log, those objects aren't interchangeable. If you treat them as though they are, something is going to blow up in your face.
$client2 = $container->run(fn() => new \Google\Client);
$logger2 = $client2->getLogger();match (true) {
$logger2 instanceof Logger => ...., // here I mean less formal instanceof
$logger2 instanceof LoggerV1 => ...,
$logger2 instanceof LoggerV2 => ...,
}
I think you're still missing the point - if I pass around $client2, and it passes checks for "instanceof Google\Client", I can pass it to functions which have no idea it came from a different container. They will have no idea that it might return an object from getLogger() which doesn't pass a check for "instanceof Logger".
In my vision, "tag" does not really identify the container. It is whatever
we wish semantically. For example, let's say we're using Monolog\Logger 2.0
as our "default" logger. But we also use another library which is designed
for 1.0. And for us that Logger from version 1.0 would be identified as
("1.0", Logger) according to the semantics we've chosen and which is
reflected in our autoloader.
I think the only way for two containers to agree that a class is the same, is if they both start with that class name already defined, so it points to the same class entry in memory.
Otherwise, containers can completely break each other by using the same tags for different definitions.
But speaking about the example you provided with google
client, it is a good example why such "polymorphic" libraries themselves
would benefit from such class-versioning and containerization and would
themselves want to switch to it to get rid of all that compatibility
shenanigans.
On the contrary, the entire point of packages like psr/log is to define objects which can be passed around from one package to another. By advertising that it can be installed with multiple versions of something, a library is giving applications freedom to resolve a set of versions which works for them.
That's exactly why I've been arguing for a distinction between "modules" and "containers", and saying that the vast majority of current code would not benefit from containers at all.
Rowan Tommins
[IMSoP]
So, Alex and Rowan have started kicking around containers/modules/whatevers
again. And I've advocated for the concept and been looking around for a way
to accomplish it, and I found one, then I read something that made me want
to cry.
You see, the PHP True Async RFC can do containers without being changed.
Well, more specifically, it can be done in userland with that RFC included
in the runtime. For review:
https://wiki.php.net/rfc/true_async
A lot of the criticism aimed at that RFC was, what would we ever use this
for? I myself didn't send any posts but that's what I was thinking as I
watched the debate over it a few months ago.
The author has a github for the RFC and its code:
https://github.com/orgs/true-async/discussions
What got me to tear up a little bit, and made me angry, was this post.
https://github.com/orgs/true-async/discussions/32
Particularly this:
Will the TrueAsync RFC be put to a vote within the coming year, or at least
for PHP 8.7?
Answer: No, it will not. There is a 90% chance the RFC will not be
accepted.The main reason: the project has no support, not even at the level of its
underlying philosophy.
Folks, I'm going to be blunt. If you want PHP to be the next COBOL, this is
how you go about it. COBOL went from one of the more important languages
out there in 1980 to a joke by 1995. PHP can fall just as fast. We've all
seen the "PHP is dead" threads on LinkedIn. Knives are out.
Async calls are a part of every other language now. This is a missing
feature, and a major one. Edmond Dantes put a huge amount of work into
this, and for what?
COBOL didn't get Object Oriented features until the late 90's. By then it
was too late. Coders had moved on. No one was learning it out of college,
just people maintaining it.
I've said this before, but PHP brought me back into coding. I was able to
make a career, first out of bb boards, then WordPress and I finally moved
beyond that. I'll be forever grateful to the efforts of the people here.
You have changed my life. I've never claimed to be a master coder with even
1% of the knowledge and expertise of the people who maintain the language
here. That's why I don't post a lot, because I'm too stupid for most of the
conversations. But I do bring the perspective of someone who actually uses
the language. I hope that's worth something.
I do remember typing Dantes' work should be the centerpoint of PHP 9, not
part of a point release. Even if it is fully backwards compatible in its
own right, it is still a sea change moment for the language should it be
adopted.
I can understand the hesitation of adopting something this major. People
remember the PHP 6 unicode disaster well. But there was never a fully
working implementation of PHP 6. Async has a working implementation.
There's rough edges, but there's something here.
Change is scary, but it's the only constant. Async operation is the norm
now. Object Oriented Code had become the norm by 1995. COBOL ignored it,
and died as a result. Will PHP do the same now?
Folks, I'm going to be blunt. If you want PHP to be the next COBOL, this
is how you go about it. COBOL went from one of the more important languages
out there in 1980 to a joke by 1995. PHP can fall just as fast. We've all
seen the "PHP is dead" threads on LinkedIn. Knives are out.
Gonna age myself, but I actually used COBOL, and its "death" has nothing to
do with OO but a plethora of other reasons, many tied to the great
programming languages that followed and it is not dead.
I can understand the hesitation of adopting something this major. People
remember the PHP 6 unicode disaster well. But there was never a fully
working implementation of PHP 6. Async has a working implementation.
There's rough edges, but there's something here.Change is scary, but it's the only constant. Async operation is the norm
now. Object Oriented Code had become the norm by 1995. COBOL ignored it,
and died as a result. Will PHP do the same now
Async is a useful function no doubt, but in the context of a primarily web
programming language like PHP, I would argue the practical utility of the
functionality is relatively limited. That is not to say PHP should not have
Async, I think it should, but not everything needs async and it is best a
solution where doing things in parallel makes sense, ie slow db queries or
http/api requests (although this can already be done with curl).
--
Ilia Alshanetsky
Technologist, CTO, Entrepreneur
E: ilia@ilia.ws
T: @iliaa
B: http://ilia.ws
On Sun, Jun 28, 2026 at 12:51 PM Rowan Tommins [IMSoP]
imsop.php@rwec.co.uk wrote:
OK, then your vision is fundamentally different from mine. I don't want the container to know anything about how the code inside it works.
Remember that autoloading is something that any library can implement its own version of. There are autoloaders which fetch from PHAR files, autoloaders which generate code on demand, and so on.
Version tags break the abstraction of the container, and I don't think they solve a real problem.
...
No, just that the container configuration doesn't know anything about "packages" or "versions". You run 'require "$someDir/vendor/autoload.php";' inside the container as normal. Or maybe you don't, and you just list a big bunch of includes for some reason. The container doesn't care.
Probably :) But that's exactly what I tried to describe in your google
client example. 3rd-party library may still have any loading it wants and
you would know nothing about it. And if you're invoking the code with no
feedback (like one-sided action) - you still don't need to know anything.
No abstraction is broken.
$result = $container->run(static function(string $endpoint, string
$actionData): string {
// Client class be autoloaded by Container's autoloader
// Or this code can load Client class by its own means,
// No packages, no versioning, no composer, just an include
// require_once '/path/to/sophisticated/client.php';
$client = new Client($endpoint);
$client->reportAction($actionData);
return "Action successfully reported";
}, ['https://myservice.com', 'User 1 logged in']);
echo $result; // "Action ..."
// this will trigger error, because we don't know what Client is
$client = new Client();
So, the case when we're running containerized code without any transfers of
objects between them is pretty straightforward, I think.
As a human author, you maybe know what versions of packages are in your container - depending how tightly you've configured them - but you have no idea what someone else might include in their container.
Sure, you have no idea about what your container uses, but if you're
deliberately expecting some object from it as a return, you must be aware
of what you're expecting, because most probably you are supposed to work
further with it. Let's start simple: we don't need any strict typing and
contracts, we just want to use returned object. Imagine that the above
container returned the client itself. We can use the returned object right
away by invoking its methods or using its properties, knowing it's contract
mentally:
$client = $container->run(...);
$client->reportAction('Another action');
We need no specific imports or additional autoloading, we already have a
working object. Moreover, if we're OK with that, we can pass this object or
return it further.
But the main thing starts if we need to strictly reference its type. If we
want to intentionally work with this client in a strict-type manner, we
must ourselves be capable of identifying this class and loading it. In
other words, right here we already must know this specific part of the
internals of the containerized code, you just can't omit this part. We may
still know nothing about other internals of that code, it could include
whatever it needed for its work, but if we're going to work with this
specific class it must be somehow known and "reachable" by us.
Let's again start simple and temporarily forget about tags. We also load
this class, either manually by including that same file, or instruct our
autoloader for that. Now we also have this symbol in our own container:
\Client. Despite that $client is also a \Client, its original symbol (which
is remembered by PHP internally) is unavailable to us (like "shadowed") and
there is no conflict. Moreover, since those are still technically the same
class (PHP can handle that, I believe), $client instaceof \Client === true;
And we can use this symbol throughout our main app.
But if we loaded some other class with that same name (from
/path/to/forked/client.php), which is a somewhat newer version of original
\Client, $client instanceof \Client === false; So, we faced the problem
which class-versioning could solve: throughout our application we want to
use the newer \Client, but somewhere in our code we must still rely on
older \Client. That's where tag-approach could help. Nothing changes for
the old code. But for us as a calling side which is aware of containers and
class-versioning, we can now have multiple versions of \Client being
loaded, because FQN of the classes (or probably better say FQI) now
technically consists of (<tag>, FQN).
For the old code, the \Client it loads would also have some tag internally
for PHP, but it is unaware of that and it does not use it in any way. It
just references \Client as it always did. Now back to our code.
In our code, we load our own version of \Client by simply including the
file. But as I've said, since we deliberately want to work with an old
\Client, we must be capable of loading it also. For this we could utilise
something like this:
require_once "/path/to/sophisticated/client.php" tagged "legacy";
And now we have to versions of \Client available to us, which we could
refer to as:
use \Client; // our default newer version
use \Client tagged "legacy" as LegacyClient;
$client = $container->run(...);
// $client instanceof Client === false;
// $client instanceof LegacyClient === true;
Despite that $client fromally is (<some_tag>, \Client), PHP would know that
it is technically the same as our ("legacy", \Client)
If you include version 2.11 of Monolog and version 2.0 of psr/log, but some other container includes version 2.11 of Monolog and version 3.0 of psr/log, those objects aren't interchangeable. If you treat them as though they are, something is going to blow up in your face.
I think you're still missing the point - if I pass around $client2, and it passes checks for "instanceof Google\Client", I can pass it to functions which have no idea it came from a different container. They will have no idea that it might return an object from getLogger() which doesn't pass a check for "instanceof Logger".
No, I don't think I'm missing something here. First of all, I didn't mean
they are interchangeable. I was talking about a proper mapping of one to
another assuming you DO need to know this specific internals of the
containerized code since you're deliberately trying to obtain these objects
from the container. And here comes strong typing:
// we use our "default" version of the logger; 2.0 in you example
use \Psr\Log\LoggerInterface;
function getLogger(): LoggerInterface {
$logger = $container->run(static function() {
// ...
return $someInternalClass->getLogger();
// Within this container, $logger is instance of
LoggerInterface, but for 3.0 version;
});
// this would throw because of type-mismatch, because
// our return type refers to LoggerInterface from 2.0
return $logger;
}
So, if you're using strong types, the "mysterious" object from the
container will not just go anywhere unintentionally.
But we could still explicitly map it to our preferred version.
use \Psr\Log\LoggerInterface;
use \Psr\Log\LoggerInterface tagged "3.0" as LoggerInterfaceV3;
If we're using an autoloader, and 3.0 version has not yet been loaded
manually by require_once (using syntax mentioned earlier), autoloader also
gets this tag "3.0" as input and decides what exact file to load. E.g. from
vendor/psr/log/v3.0/src/LoggerInterface.php, and correspondingly loads this
class as tagged 3.0.
Now we can easily map the object from the container to our own version.
function getLogger(): LoggerInterface {
$logger = $container->run(static function() {
// ...
return $someInternalClass->getLogger();
// Within this container, $logger is instance of
LoggerInterface, but for 3.0 version;
});
if ($logger instanceof LoggerInterfaceV3) {
// LoggerProxyV3 implements our version LoggerInterface
return new LoggerProxyV3($logger);
}
}
Or you can extend this example to a more complex situation with your
initial example with Monolog and Google client.
I think the only way for two containers to agree that a class is the same, is if they both start with that class name already defined, so it points to the same class entry in memory.
Otherwise, containers can completely break each other by using the same tags for different definitions.
Actually, I just realized that passing the autoloader to the container is
not even necessary. I mean that no matter how classes were loaded in the
container, we anyway don't need their tags because of using the mentioned
version of instanceof which would easily determine that an object
(<some_tag>, FQN) from the container is absolutely the same or not as our
("XXX", FQN).
On the contrary, the entire point of packages like psr/log is to define objects which can be passed around from one package to another. By advertising that it can be installed with multiple versions of something, a library is giving applications freedom to resolve a set of versions which works for them.
That's exactly why I've been arguing for a distinction between "modules" and "containers", and saying that the vast majority of current code would not benefit from containers at all.
As it looks to me you're anyway relying on the libraries themselves that
they can or should support different versions of other packages, but to me
the whole idea of containers is that we can completely not care about that
and run any code inside the container, no matter what or how it loads.
Sure, the packages may be advised to somehow declare such kinds of things,
probably. But it looks like it is not strictly necessary.
Honestly, this has really started to look like a future RFC to me :) Even
though I highly doubt I could implement such a thing myself.