On Sun, Jun 28, 2026 at 12:51 PM Rowan Tommins [IMSoP]
<[email protected]> 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.

Reply via email to