On Sat, Jun 27, 2026 at 6:36 PM Rowan Tommins [IMSoP] <[email protected]>
wrote:

> On 26/06/2026 14:20, Alex Pătrănescu wrote:
>
> 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.
>
>
> 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.
>

Hi Rowan,
Thank you for the message.

I am replying first to this message, since the thread with you and
Alexander grew a bit broader than I intended for this first reply. I'll
find some time to get back to that separately in the next few days.

The name is still open, but I am not sure about using "container" as the
final name.
To me, it does not fully match the design I have in mind: a graph of
symbol-owning packages/modules where code running in one module can only
see its own symbols and the symbols from its declared dependencies.

Other options could be "package", but that conflicts with Composer,
"realm", "domain", "context", or something else that fits better.
For now I went with "runtime module", and we can see if a better name
becomes clear once the details are outlined more fully.


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

As I mentioned, these runtime modules could map naturally over Composer
packages and their required dependencies.
`module_add_dependency()` is used to build the dependency graph, while
`module_run()` is used to run code in a module.
Running code in a module can then initialize that module: include files,
define classes, define autoloaders, and so on.

For example, let's say we have an app that depends on Guzzle, and Guzzle
depends further on PSR-7 and PSR-18. If the app also uses PSR-7 directly,
it declares that dependency too.
The bootstrapping process for this model would look roughly like this:

```php
// Build runtime module dependency graph.
module_add_dependency('guzzle');
module_add_dependency('psr-7');

module_run('guzzle', function () {
    module_add_dependency('psr-7');
    module_add_dependency('psr-18');
});

// Initialize autoloaders.
// Register autoloader for Org\App namespace.
module_run('guzzle', function () {
    // Register autoloader for GuzzleHttp namespace.
});

module_run('psr-7', function () {
    // Register autoloader for Psr\Http\Message namespace.
});

module_run('psr-18', function () {
    // Register autoloader for Psr\Http\Client namespace.
});
```

The reason for having separate operations is that dependencies are often
shared with other modules, and separating "build the graph" from "run code
in a module" makes that model more consistent.

Some initial designs included a `module_create()` function, possibly
without a name parameter, returning an opaque `RuntimeModule` object that
could be used for "run" and "add dependency" operations.
We can revisit that later if needed. The main point is that I need separate
operations for "run code in this module" and "add this module as a
dependency".


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

Yes, Java's ClassLoader was one of the points of reference I used in the
research.
A class in Java is always associated with its class loader, and uniqueness
is determined by the tuple `(fully qualified name, ClassLoader instance)`.

Similarly, in PHP, a class, function, or other global symbol would be
uniquely identified by `(symbol_name, module)`.

Using the earlier example, if Guzzle passed back an exception object
implementing `NetworkExceptionInterface` to the app, and the app did not
know about that interface, the app would still be able to call public
methods such as `getRequest()` on the object.

But if the app tried to use that class/interface name as a type, it would
not magically resolve through the Guzzle module, and it would generate a
class not found error.
And, another example, if used in a `catch`, it would simply not catch it,
which is the behavior PHP has today when the declared class in a `catch`
does not exist.

However, the app could still use reflection on the object and even call
`newInstanceWithoutConstructor()` if it wanted to.
That gets into quite a few details, and I have not finished ironing all of
them out yet.

Going back to the dependency graph, if you see a "class not found" error,
what should you do? In this model, you declare that the app also depends on
PSR-18, and that solves the issue.
With Composer context, this is already highlighted in PhpStorm with
warnings like "using symbol from a package that is not declared as a
dependency", and it is fixed in the same way.



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

Yes, each module starts with only PHP internal symbols visible. It will
futher see only its own symbols and its direct dependency symbols. When a
new symbol is added, collisions are checked; similarly, collisions are
checked when a new dependency is added.


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

The goal is to design and implement it with minimal performance impact, and
to make it directly usable by Composer, everywhere.
I have not reached out to Composer yet, since this is still at an early
stage, but I definitely want to do that before publishing an RFC, to make
sure the model fits well.
If it fits Composer well, I believe it can also fit WordPress use cases.

Regards,
Alex

>

Reply via email to