Re: [PHP-DEV] Two-step creation for PHP modules

On 08/06/2026 16:36, Alex Rock wrote:

Recently, we had proposals for "Friend classes" and "Pure PHP files". These suggestions aren't new at all, but they demonstrate some wish in the PHP ecosystem to make certain changes for PHP to be closer to other programming languages.

I don't think "being closer to other programming languages" is, or should be, an aim of the project. Our aim should be to make PHP as good a language as it can be.

That includes borrowing ideas which have proved successful in other languages, large and small, as well as avoiding pitfalls which have been revealed by those other languages. It also includes retaining PHP's identity as a distinct language with its own history, user base, and ecosystem.

As I've said in previous discussions, JavaScript's module system was designed around a very specific set of facilities and constraints. It has some similarity to Python, which uses some of the same concepts (e.g. all definitions are essentially anonymous objects, named by assigning to variables).

PHP has an entirely different set of facilities and constraints, much more similar to Java and C# (e.g. all definitions have a globally unique name, namespace imports are primarily short-hand for those unique names).

Your proposal looks very much like trying to wedge JavaScript's solution into PHP, rather than a realistic path forward for PHP's existing ecosystem.

--
Rowan Tommins
[IMSoP]

On Mon, Jun 8, 2026 at 5:00 PM Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:

On 08/06/2026 16:36, Alex Rock wrote:

Recently, we had proposals for “Friend classes” and “Pure PHP files”.
These suggestions aren’t new at all, but they demonstrate some wish in
the PHP ecosystem to make certain changes for PHP to be closer to
other programming languages.

I don’t think “being closer to other programming languages” is, or
should be, an aim of the project. Our aim should be to make PHP as good
a language as it can be.

That includes borrowing ideas which have proved successful in other
languages, large and small, as well as avoiding pitfalls which have been
revealed by those other languages. It also includes retaining PHP’s
identity as a distinct language with its own history, user base, and
ecosystem.

As I’ve said in previous discussions, JavaScript’s module system was
designed around a very specific set of facilities and constraints. It
has some similarity to Python, which uses some of the same concepts
(e.g. all definitions are essentially anonymous objects, named by
assigning to variables).

PHP has an entirely different set of facilities and constraints, much
more similar to Java and C# (e.g. all definitions have a globally unique
name, namespace imports are primarily short-hand for those unique names).

Your proposal looks very much like trying to wedge JavaScript’s solution
into PHP, rather than a realistic path forward for PHP’s existing ecosystem.


Rowan Tommins
[IMSoP]

I see the bear is out for it’s yearly walk eh? At least I wasn’t the one who woke it this time.

Kidding aside.

Changing the language for change sake is a non-starter Alex. First, what is the problem you want to solve?

The largest problem a module system could solve is resolving namespace conflicts. If you delve into the discussion Rowain and I had on this last year it goes into detail on this to some degree, but the real world problem is this. Suppose I write AkiPublish, a plugin for Wordpress which uses composer to resolve dependencies. Suppose someone else out there writes BobMod, a plugin to help moderate. For whatever reason the two plugins want to use different incompatible versions of a library called LogIt.

The end user will have a crash if they ever install these plugins together. And if they’re like 90% of WordPress users they won’t have a clue why. PHP has no mechanism for code isolation - plugins, extensions, modules or whatever the heck you call them, they all must share a global state. Careful coding mitigates most of this problem and it is the reason Laravel, Symfony, Drupal et al take the form they do - avoiding establishing global variables at all cost and putting this into classes. But that isn’t the architecture of WordPress and indeed most of PHP before 5.3.

And while many are dismissive of Wordpress, it does have one of the largest, perhaps the largest, footprints in the PHP CMS space.

True code isolation would be useful to have, but it needs to take a form that is minimally disruptive. Rowain and I had worked out some concepts, but I dropped the ball on this and got distracted with other projects. To be honest, these days I spend as little time working with computer code outside of work as I can because it just doesn’t interest me anymore. I wrestle with Dorico and try to actually “compose”.

That said, at this time completion of the PHP-ASYNC project might be prerequisite to doing this. The reason is simple - the best way to isolate code packages is to put them on different threads. The moment that happens calls to functions living on those threads will have to be handled asynchronously. That’s one of the reasons I’ve remained silent on this for much of the last year.

Le 09/06/2026 à 02:37, Michael Morris a écrit :

On Mon, Jun 8, 2026 at 5:00 PM Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:

    On 08/06/2026 16:36, Alex Rock wrote:
    > Recently, we had proposals for "Friend classes" and "Pure PHP
    files".
    > These suggestions aren't new at all, but they demonstrate some
    wish in
    > the PHP ecosystem to make certain changes for PHP to be closer to
    > other programming languages.

    I don't think "being closer to other programming languages" is, or
    should be, an aim of the project. Our aim should be to make PHP as
    good
    a language as it can be.

    That includes borrowing ideas which have proved successful in other
    languages, large and small, as well as avoiding pitfalls which
    have been
    revealed by those other languages. It also includes retaining PHP's
    identity as a distinct language with its own history, user base, and
    ecosystem.

    As I've said in previous discussions, JavaScript's module system was
    designed around a very specific set of facilities and constraints. It
    has some similarity to Python, which uses some of the same concepts
    (e.g. all definitions are essentially anonymous objects, named by
    assigning to variables).

    PHP has an entirely different set of facilities and constraints, much
    more similar to Java and C# (e.g. all definitions have a globally
    unique
    name, namespace imports are primarily short-hand for those unique
    names).

    Your proposal looks very much like trying to wedge JavaScript's
    solution
    into PHP, rather than a realistic path forward for PHP's existing
    ecosystem.

    -- Rowan Tommins
    [IMSoP]

I see the bear is out for it's yearly walk eh? At least I wasn't the one who woke it this time.

Kidding aside.

Changing the language for change sake is a non-starter Alex. First, what is the problem you want to solve?

The largest problem a module system could solve is resolving namespace conflicts. If you delve into the discussion Rowain and I had on this last year it goes into detail on this to some degree, but the real world problem is this. Suppose I write AkiPublish, a plugin for Wordpress which uses composer to resolve dependencies. Suppose someone else out there writes BobMod, a plugin to help moderate. For whatever reason the two plugins want to use different incompatible versions of a library called LogIt.

The end user will have a crash if they ever install these plugins together. And if they're like 90% of WordPress users they won't have a clue why. PHP has no mechanism for code isolation - plugins, extensions, modules or whatever the heck you call them, they all must share a global state. Careful coding mitigates most of this problem and it is the reason Laravel, Symfony, Drupal et al take the form they do - avoiding establishing global variables at all cost and putting this into classes. But that isn't the architecture of WordPress and indeed most of PHP before 5.3.

And while many are dismissive of Wordpress, it does have one of the largest, perhaps the largest, footprints in the PHP CMS space.

True code isolation would be useful to have, but it needs to take a form that is minimally disruptive. Rowain and I had worked out some concepts, but I dropped the ball on this and got distracted with other projects. To be honest, these days I spend as little time working with computer code outside of work as I can because it just doesn't interest me anymore. I wrestle with Dorico and try to actually "compose".

That said, at this time completion of the PHP-ASYNC project might be prerequisite to doing this. The reason is simple - the best way to isolate code packages is to put them on different threads. The moment that happens calls to functions living on those threads will have to be handled asynchronously. That's one of the reasons I've remained silent on this for much of the last year.

Thanks Michael and Rowan for your quick insights, especially since I have read some of your past conversations about Modules :slight_smile:

So, let me try to answer your questions.

> First, what is the problem you want to solve?

The main problems that PHP modules solve are the following:

- Libraries can finally isolate code completely, and not only in private class methods, but they can isolate entire structures, and can even isolate a sub-library
- Since all modules internally contain a hashed-prefix version of all their definitions, two versions of the same library can coexist, since a module hash is unique based on its file path and contents (I should have made it explicit that the module prefix is hashed based on file contents & path, to ensure uniqueness)

Many existing libraries could migrate parts of their internal structures (the ones not supposed to be supported by their BC policy) to modules with no impact on userland code.

And the future of this might help improving the ecosystem by providing a different way to package applications themselves: if the first loaded file is a module, and all files in the module tree are also modules and all "include/require" are done on compiler-resolvable paths, this means that an entire PHP app could be bundled as the opcodes. It can already be bundled like that, but it needs a few hacks, whereas modules make it directly built-in. We could imagine the FrankenPHP worker mode have one base memory impact based on the modules tree, and all the rest would be I/O-related memory consumption.

Having everything made into modules also allows giving more power to the compiler and engine in order to control stack/heap-related structures: garbage collection points can be more deterministic, objects references can be tracked a bit more easily, and so on.

> I don't think "being closer to other programming languages" is, or should be, an aim of the project. Our aim should be to make PHP as good a language as it can be.

You're right, and that's the reason why, since modules have been requested for some time now, I think the best way to have this feature is to ensure its integration smoothly in the compiler and engine.

> True code isolation would be useful to have, but it needs to take a form that is minimally disruptive. Rowain and I had worked out some concepts, but I dropped the ball on this and got distracted with other projects. To be honest, these days I spend as little time working with computer code outside of work as I can because it just doesn't interest me anymore. I wrestle with Dorico and try to actually "compose".

That's why my thoughts are about doing it in two steps: first make sure that a module is "harmless when loaded", ensuring proper impact-free compilation and structure-definition, so that the compiler can guarantee that such compiled file is "safe", or "pure" (in the means of "no impact on external code", apart compiler-detectable issues, like existing classes, constants, etc.), and that userland can still use this file without BC breaks. And as said, this "simple feature" makes all existing codebases still work the same.

As I already said: the main goal isn't to resemble JS or other languages. It's to solve the aforementioned problems in a way that can be both close to existing languages (for familiarity), and smooth on the ecosystem (which the proposed two steps are for the ecosystem, but the second step is heavy on the engine).

On 09/06/2026 10:39, Alex Rock wrote:

Many existing libraries could migrate parts of their internal structures (the ones not supposed to be supported by their BC policy) to modules with no impact on userland code.

I think this is the crux for me, and why I reacted as I did to your initial e-mail. It doesn't look anything like how PHP libraries lay out their code today, so migrating to it would involve a complete change of coding style.

In JavaScript, it has always been common practice to ship libraries as a single file, so the user only needed to add a single <script> tag. Inside that file, a single function creates a private scope, and everything declared is hidden unless passed out in some way. The modern module format still basically works that way: "import" references a file, and "export" defines objects to pass back.

In PHP, we've always had include/require, and for the last 20+ years, we've had autoloading, so packages commonly have many small files. Libraries have always prefixed names to avoid colliding with other code, and namespaces made that easier. Loading a package usually means configuring an autoloader to look in directory X for namespace prefix Y, and reference the classes you need by name.

Changing every file in a library to use import and export statements would be a huge chore, and break all sorts of assumptions, for very little benefit. The natural boundary in PHP is not an included file, it's a registered namespace prefix.

And the future of this might help improving the ecosystem by providing a different way to package applications themselves: if the first loaded file is a module, and all files in the module tree are also modules and all "include/require" are done on compiler-resolvable paths, this means that an entire PHP app could be bundled as the opcodes.

OpCache already handles the difference between per-file compilation and "linking" between files; the reason not to ship opcodes is more about the stability of the OpCode format itself, which can change in even minor versions.

Module-level optimisation would potentially be a significant benefit, but again the key there is how to define a boundary around *multiple* files, to cache them as a single unit. If the cache key has to include *extra* granularity, for "file X imported from file Y" vs "file X imported from file Z", that just leads to more cache misses.

--
Rowan Tommins
[IMSoP]

On Tue, Jun 9, 2026, at 11:39 AM, Alex Rock wrote:

First, what is the problem you want to solve?

The main problems that PHP modules solve are the following:

- Libraries can finally isolate code completely, and not only in
private class methods, but they can isolate entire structures, and can
even isolate a sub-library
- Since all modules internally contain a hashed-prefix version of all
their definitions, two versions of the same library can coexist, since
a module hash is unique based on its file path and contents (I should
have made it explicit that the module prefix is hashed based on file
contents & path, to ensure uniqueness)

Many existing libraries could migrate parts of their internal
structures (the ones not supposed to be supported by their BC policy)
to modules with no impact on userland code.

See, I don't think this is remotely true.

Consider Serde. (My go-to example.) It contains dozens of class-likes. A typical serialization request is going to use 90% of them. Being able to front-load and link all of them without going through the autoloader every time sounds good! But...

There is no way in hell that I'm moving dozens of classes into a single file. Multi-thousand-line files are frowned upon for a reason. In practice I don't really need most of them to be private. Maybe one, I dunno. So I simply wouldn't bother., meaning I wouldn't benefit from whatever performance optimizations we are able to add later.

So this approach makes modules something usable ONLY by code bases that have lots of defined class-likes or functions that are "private", AND they're all very very small so that the resulting mega-file isn't too large for my IDE to open. That's a very, very small number of cases.

I will reiterate what Rowan said above, and what I said the last time modules were discussed: module == file is absolutely a dead-end for PHP. It simply will not work in practice. Not because of PSR-4 like some people keep claiming, but because the resulting files would be just too damned big and unwieldy.

Add to that, how do I write tests for the "private" classes? I should still be able to test those on their own, without having to go through the few public facing classes. If the tests are in a separate file... how do I do that?

Let's stop trying to make module == file happen. It's not going to happen.

--Larry Garfield

Le 09/06/2026 à 22:34, Rowan Tommins [IMSoP] a écrit :

On 09/06/2026 10:39, Alex Rock wrote:

Many existing libraries could migrate parts of their internal structures (the ones not supposed to be supported by their BC policy) to modules with no impact on userland code.

I think this is the crux for me, and why I reacted as I did to your initial e-mail. It doesn't look anything like how PHP libraries lay out their code today, so migrating to it would involve a complete change of coding style.

That's the goal here: keep BC at all costs, and make modules transparent in userland. Change of coding style is opt-in for libraries/frameworks maintainers that want internal structures whenever they need it.

In JavaScript, it has always been common practice to ship libraries as a single file, so the user only needed to add a single <script> tag. Inside that file, a single function creates a private scope, and everything declared is hidden unless passed out in some way. The modern module format still basically works that way: "import" references a file, and "export" defines objects to pass back.

In PHP, we've always had include/require, and for the last 20+ years, we've had autoloading, so packages commonly have many small files. Libraries have always prefixed names to avoid colliding with other code, and namespaces made that easier. Loading a package usually means configuring an autoloader to look in directory X for namespace prefix Y, and reference the classes you need by name.

My proposals for modules do not imply that everything is shipped in one single file. It implies that a module allows "hiding" some of its internal structures instead of exposing everything to the global space, so that instead of adding "@internal" or "final" to everything to avoid users extending such code, you can just ship that internal code along the rest.

Changing every file in a library to use import and export statements would be a huge chore, and break all sorts of assumptions, for very little benefit. The natural boundary in PHP is not an included file, it's a registered namespace prefix.

As said: it's not mandatory. And namespaces can be overriden at will in userland, while internal module code cannot (unless you succeed in hacking the hashed prefix and sort of extend that, but modules should make this impossible, and if not, as hard as possible).

And the future of this might help improving the ecosystem by providing a different way to package applications themselves: if the first loaded file is a module, and all files in the module tree are also modules and all "include/require" are done on compiler-resolvable paths, this means that an entire PHP app could be bundled as the opcodes.

OpCache already handles the difference between per-file compilation and "linking" between files; the reason not to ship opcodes is more about the stability of the OpCode format itself, which can change in even minor versions.

Module-level optimisation would potentially be a significant benefit, but again the key there is how to define a boundary around *multiple* files, to cache them as a single unit. If the cache key has to include *extra* granularity, for "file X imported from file Y" vs "file X imported from file Z", that just leads to more cache misses.

In my mind, the goal is to deport runtime "a list of compiled files are executed" concept into a compile-time "when importing a module, its ast is directly copy/pasted here, with safeguards to avoid redefining the same structures in the symbols table". Since a module file will contain a hashed prefix (based on file path & content), it's (almost) impossible to have two similar hashes that would conflict with each other, and the safeguard is only here to make sure two files can import the same module and reuse the defined structures directly from memory if they were set already.

A module doesn't necessary need to be a single unit: as said, you can still have a "PSR-4-way of implementing modules", where modules are all exporting one single PHP class, and it shouldn't make any difference in the first place. The differences will be in the internal structures that can be necessary for said PHP class. Sure it might break PSR-4 itself, hence why I do *not* suggest implementing true PSR-4 on modules, because that's not really the same thing, but their similarity allow for internal data structures, constants, etc., that allow maintainers a bit more freedom on their BC policy.

  • Creating a “package definition” file that defines the list of modules for said package - Exposing the public API from the main file, instead of "every module can determine whether they are public API or internal to the package. - Adding “declare(module=1);” and “package …” statements to all package files.
···

Le 10/06/2026 à 00:24, Larry Garfield a écrit :

On Tue, Jun 9, 2026, at 11:39 AM, Alex Rock wrote:

First, what is the problem you want to solve?

The main problems that PHP modules solve are the following:

- Libraries can finally isolate code completely, and not only in 
private class methods, but they can isolate entire structures, and can 
even isolate a sub-library
- Since all modules internally contain a hashed-prefix version of all 
their definitions, two versions of the same library can coexist, since 
a module hash is unique based on its file path and contents (I should 
have made it explicit that the module prefix is hashed based on file 
contents & path, to ensure uniqueness)

Many existing libraries could migrate parts of their internal 
structures (the ones not supposed to be supported by their BC policy) 
to modules with no impact on userland code.

See, I don't think this is remotely true.

Consider Serde.  (My go-to example.)  It contains dozens of class-likes.  A typical serialization request is going to use 90% of them.  Being able to front-load and link all of them without going through the autoloader every time sounds good!  But...

There is no way in hell that I'm moving dozens of classes into a single file.  Multi-thousand-line files are frowned upon for a reason.  In practice I don't really need most of them to be private.  Maybe one, I dunno.  So I simply wouldn't bother., meaning I wouldn't benefit from whatever performance optimizations we are able to add later.

It always depends on the libraries: if Serde has structures that must not be accessible from userland, they would be a great fit for being added to your module file as internal structures. If they can be used by users for something else than full serialization/deserialization, then of course they have to be part of the public API. It all depends on how you view your library, how you want to expose your code, and how comfortable you would be with maintaining a bigger public API whereas you could actually maintain internal code with no BC breaks if you keep the public API but change the internals. As of today, there’s zero way to prevent this natively in PHP, and the only workarounds are adding “@internal” phpdoc everywhere, which is only interpreted by static analysis, which we already know isn’t a globally implemented dev workflow.

Again: many existing PHP projects that didn’t embrace the PSR-0/4 autoload norms, don’t use Composer, or other tooling that have huge legacy non-standard codebases like Wordpress or Dolibarr, would be able to benefit from modules for something different than just “internal structures”, being the possibility to have two versions of the same library used by different parts of their codebases (for plugins/extensions, mostly), which is a huge change in how these projects could evolve in the future, because they currently have no way to do this, and not even proper workarounds (apart potential class-prefixing, similar to what Box-project can do for PHAR files, but this is quite hard to implement for these projects).

Add to that, how do I write tests for the "private" classes?  I should still be able to test those on their own, without having to go through the few public facing classes.  If the tests are in a separate file... how do I do that?

Just like you do when you have to test private class methods: you don’t. It’s not a good practice. Testing must be done on the public API anyway, and internal/private code must only be considered as an unreachable black-box.
Though, my proposal doesn’t necessarily imply that testing internal structures is impossible: I still say that the proposed ReflectionModule class can include the hashed prefix, and an access to the internal structures. All of internal structures are just “normal structures” but they are just not accessible from the global scope.
This ReflectionModule class could give you access to the hash-prefixed fully-qualified-names of all internal structures, and if they are functions, you could still use ReflectionFunction on them to call them from tests, and if they are classes, you could use ReflectionClass to create a new instance.
It’s not impossible, it’s just that it’s the same hacks that you have to do when testing private methods, and not a good practice overall.

So this approach makes modules something usable ONLY by code bases that have lots of defined class-likes or functions that are "private", AND they're all very very small so that the resulting mega-file isn't too large for my IDE to open.  That's a very, very small number of cases.

I will reiterate what Rowan said above, and what I said the last time modules were discussed: module == file is absolutely a dead-end for PHP.  It simply will not work in practice.  Not because of PSR-4 like some people keep claiming, but because the resulting files would be just too damned big and unwieldy.

Ok, I thought that two-step PHP modules was already big as a suggestion and didn’t want to dig into too much “new ideas”. Two are already a lot.
But… the problem you think modules create (aka “huge files with one single exported public-API-related class”) can be solved with another concept that PHP doesn’t have, but that has been requested for quite a long time. I have already thought about it when sending my first message to internals, but I was afraid it would backlash a bit, and/or overwhelm the readers.

So, as an avant-première, here’s my third step proposal after modules: packages.

I already thought about how PHP packages can work, and instead of relying mostly on namespaces (like the existing proposals), packages would rely on two things: package “main file”, and “package-included” files.

The packages system, in my mind, would work similarly to how Rust crates are defined.

Conceptually, a PHP package can only contain modules.

A PHP package main file would look like this:

```
<?php declare(module=1);
// serde/main.php

main Crell\Serde; // Unique namespace-like name.

// All these imports are resolved into files
import Serializer from 'serialize.php' as module;
import Deserializer from 'deserialize.php' as module;
import SERDE_VERSION 'some_internal_code.php' as module;

export Serializer; // Module syntax. Exports the public API from this module. No namespace needed.
export Deserializer;

// Example of public export using internal non-exposed constant:
export class Serde {
    public static version(): string { return SERDE_VERSION; }
};
```

How it is used:

```
<?php declare(module=1);
// serde/serialize.php

package Crell\Serde;

export Serializer from './src/Serializer.php';
```

How does it work?

First file content must be main: it declares the main file for a package.

All “import … as module” statements in a “main” file are considered similarly to any imported module, but it adds a new feature: packaged modules.

A file imported as a module must contain the “package” declaration, it means that once the compiler encounters it, it checks the module tree to ensure that such module is loaded ONLY by a file that is inside the main package’s module tree. (in the above case: Crell/Serde, for instance, which must be repeated in all modules of this package).

This means that, from this stage, the compiler will prevent ANY other PHP file from including, requiring or importing this packaged module.

With such safeguard, it would be extremely annoying for end-users to do something like this: import SERDE_VERSION from 'vendor/crell/serde/some_internal_code.php', because they would need to create a custom file that replaces vendor/crell/serde/main.php, copy/paste its content and change whatever they want, then they might need to override the spl module path registration if they wanted to override any other file from this vendor dir, and so on. Though it would be possible, the task would be extremely tedious, similarly to how nowadays we use to override library classes with dirty hacks, reflection, aliases, or autoload-based overrides that change the PHP code before loading it.

And something more is implied to all declared structures (as said, inspired by Rust): package-level structure visibility.

In Rust, when you create a file, all structures you declare are, by default, internal. Instead of using “export” like in JS, you can allow other files to use it if you use the “pub” keyword. But “pub” makes it part of the entire public API. That’s when the “pub(crate)” keyword comes: it allows public visibility only for the package you’re creating (from its “main” file), making internal code accessible from anywhere in the library, but inaccessible from the global scope.

With Packages, thanks to the package keyword used in the package’s modules, any “export”-ed code from modules is only accessible from the modules declared in the main file. The main file’s goal is to expose the package’s public API.

This would allow existing libraries to have everything as internal classes, and instead of having one file with all structures, packages would allow to scatter all files in a PSR-4 way, whether they are internal or part of the public API.

Sure, this has drawbacks:

IMO, if you want “true” packages that behave as black-boxes with a public API and inaccessible internal code while still being able to develop a PHP library with classes scattered in a PSR-4 structure, there isn’t a better way to do so. The package-internal-only structures can only become package-internal if we have either a standardized directory structure (which is IMO a bad idea for PHP, because we don’t enforce file names neither extension, so I don’t see a benefit of enforcing directory structure), or if we have a package-definition file.

That’s where my research stopped, because this idea has some limitations that I have not fixed yet, like “feature flags for conditional modules” (again, inspired by Rust, but quite complex), or testing (aka “make these modules accessible in testing, but not in prod”, which would imply a PHP-level flag, like a “testing = true” INI directive or something similar).
My inspiration from Rust doesn’t help, because Rust has a built-in test framework, whereas PHP relies on 3rd-party frameworks (like PHPUnit), and these frameworks might need full access to internal structures from a library. But the ReflectionModule can be extended into a ReflectionPackage, so that internal structures can also be accessible, and PHPUnit could include a public API to instantiate objects from internal classes. But I preferred to stop my research here, since the creation of “definition files” and “php modules” already triggers too much :slight_smile:

For the rest, I am still convinced that such vision of PHP Modules is completely harmless to existing frameworks, can bring tons of benefits to non-standard PHP projects, and would allow legacy projects to use different versions of the same PHP library (thanks to the prefix system).

PHP packages is just an extension of this in order to solve one more problem: internal code scattered in multiple files, instead of being in one big single file.

IMO, there’s no way to introduce PHP packages if PHP itself doesn’t narrow down how PHP files behave in the first place, hence why “multiple harmless steps” seems like the best compromise to me.

On 10 June 2026 08:36:44 BST, Alex Rock <pierstoval@gmail.com> wrote:

Le 09/06/2026 à 22:34, Rowan Tommins [IMSoP] a écrit :

I think this is the crux for me, and why I reacted as I did to your initial e-mail. It doesn't look anything like how PHP libraries lay out their code today, so migrating to it would involve a complete change of coding style.

That's the goal here: keep BC at all costs, and make modules transparent in userland. Change of coding style is opt-in for libraries/frameworks maintainers that want internal structures whenever they need it.

I'm not talking about BC, or forced migration; I'm talking about the effort required when you *do* want the new facilities.

It's not at all clear to me where an existing library would add "export" and "import" keywords in order to mark some classes as internal. It would seem to require completely rearranging the project structure, and rewriting references to all the moved parts.

Compare to the work required to use a "namespace visibility" feature:

1. Add a keyword, or an attribute, at the top of each internal class, limiting its use to a certain namespace prefix.
2. There is no step 2. Nothing needs to be moved, or renamed, or accessed differently.

That's what I mean by starting with how PHP is used today, and finding ways to enhance it.

Rowan Tommins
[IMSoP]

Le 10/06/2026 à 12:23, Rowan Tommins [IMSoP] a écrit :

On 10 June 2026 08:36:44 BST, Alex Rock <pierstoval@gmail.com> wrote:

Le 09/06/2026 à 22:34, Rowan Tommins [IMSoP] a écrit :

I think this is the crux for me, and why I reacted as I did to your initial e-mail. It doesn't look anything like how PHP libraries lay out their code today, so migrating to it would involve a complete change of coding style.

That's the goal here: keep BC at all costs, and make modules transparent in userland. Change of coding style is opt-in for libraries/frameworks maintainers that want internal structures whenever they need it.

I'm not talking about BC, or forced migration; I'm talking about the effort required when you *do* want the new facilities.

It's not at all clear to me where an existing library would add "export" and "import" keywords in order to mark some classes as internal. It would seem to require completely rearranging the project structure, and rewriting references to all the moved parts.

Compare to the work required to use a "namespace visibility" feature:

1. Add a keyword, or an attribute, at the top of each internal class, limiting its use to a certain namespace prefix.
2. There is no step 2. Nothing needs to be moved, or renamed, or accessed differently.

That's what I mean by starting with how PHP is used today, and finding ways to enhance it.

Creating a namespace doesn't ensure your structures will be internal. In PHP, there's nothing today that allow it. Creating a "Package" system that allows internal private structures is not yet on the menu (I have other ideas for that, in another thread), but starting with the concept of PHP modules as files starts the path to make sure a PHP package can be bundled as a list of PHP modules operating with each other, but the package configuration is the only one that can `export` the public API, and all the rest of the defined structures are private and only visible from inside the package.
The goal of modules, with the current problems that it solves, makes it really straightforward to follow with packages, and therefore, "private code". (in userland, because it's another thing for the engine and compiler...).

Such things would allow two similar versions to coexist, and we could even imagine two PHP packages having the same name: since they have to be modules, it's not like defining two classes in the same namespaces, because every module will have an internal hashed prefix, and their own personal module tree.

Modules solve the very first part of packages, and solve a few issues with allowing the creation of private code, even though the compromise is, for now, that such private code is only possible from inside a module.

On 10 June 2026 14:03:49 BST, "Alex "Pierstoval" Rock" <pierstoval@gmail.com> wrote:

Le 10/06/2026 à 12:23, Rowan Tommins [IMSoP] a écrit :

That's what I mean by starting with how PHP is used today, and finding ways to enhance it.

Creating a namespace doesn't ensure your structures will be internal. In PHP, there's nothing today that allow it.

You've missed the point again. I'm not saying that namespaces allow this today. I'm saying that any attempt to solve problems around packages in PHP should start with the functionality we already have, and the way the language is actually used.

Take Guzzle, for example; it has 43 source files within a specific namespace root. A handful of those are marked "@internal", and a way for PHP to error if users reference them directly would be useful. Some users end up wanting conflicting versions of Guzzle simultaneously, e.g. in different WordPress plugins; so some way of isolating or rewriting class names (and all their references) would be useful.

There are not 43 separate "modules", and the maintainers of Guzzle aren't going to combine all of them into one file. Defining a single package with multiple files is not a stretch goal, it's the only plausible starting point.

(And before anyone starts whining about Composer and PSR-4, I could find you a PEAR package from 25 years ago and make the same point. Single file packages have *never* been the norm in PHP.)

Rowan Tommins
[IMSoP]

On 2026-06-11 03:24, Rowan Tommins [IMSoP] wrote:

Take Guzzle, for example; it has 43 source files within a specific namespace root. A handful of those are marked "@internal", and a way for PHP to error if users reference them directly would be useful. Some users end up wanting conflicting versions of Guzzle simultaneously, e.g. in different WordPress plugins; so some way of isolating or rewriting class names (and all their references) would be useful.

There are not 43 separate "modules", and the maintainers of Guzzle aren't going to combine all of them into one file. Defining a single package with multiple files is not a stretch goal, it's the only plausible starting point.

Just throwing this out, but couldn't the "module file" - the one that users bring into their project and the one that exports what it's declared to export - delegate the definitions of the functions etc. both public and private to other files?

I mean, PHP already _has_ an "include" statement...

On 11 June 2026 01:23:29 BST, Morgan <Weedpacket@varteg.nz> wrote:

On 2026-06-11 03:24, Rowan Tommins [IMSoP] wrote:

Take Guzzle, for example; it has 43 source files within a specific namespace root. A handful of those are marked "@internal", and a way for PHP to error if users reference them directly would be useful. Some users end up wanting conflicting versions of Guzzle simultaneously, e.g. in different WordPress plugins; so some way of isolating or rewriting class names (and all their references) would be useful.

There are not 43 separate "modules", and the maintainers of Guzzle aren't going to combine all of them into one file. Defining a single package with multiple files is not a stretch goal, it's the only plausible starting point.

Just throwing this out, but couldn't the "module file" - the one that users bring into their project and the one that exports what it's declared to export - delegate the definitions of the functions etc. both public and private to other files?

I mean, PHP already _has_ an "include" statement...

It could, but that just moves the problem: right now, I can use any class in Guzzle from anywhere in my application as long as I configure the right autoloader. I can pass objects from Guzzle into and out of other libraries, and they all agree on what each class name means.

Having to write an "import" statement means I have to be conscious of where I "start" using Guzzle. If every import of the library creates a magically prefixed copy of the entire library, I'm not even clear how I'd refer to different Guzzle classes in different files of my application; let alone how libraries could pass those objects in and out and agree that they were the same class.

Namespace visibility is really really easy; as if file visibility, if you want that. Usable namespace rewriting is much more difficult, but doing it per file makes it harder, not easier.

As Larry said, just stop trying to make file==module work. It's not the right approach for PHP.

Rowan Tommins
[IMSoP]

If you want to make namespace==module instead, this could maybe be a nice approach for maintainers in making “some private code” in their packages, but it’s not enough: - It still allows creating a namespace from anywhere else in order to “hack” into it and publicly expose an API that wasn’t supposed to be exposed at all. - It doesn’t solve the issue of having the same package in different versions, since namespaces will be the same

···

Le 11/06/2026 à 08:52, Rowan Tommins [IMSoP] a écrit :

On 11 June 2026 01:23:29 BST, Morgan [<Weedpacket@varteg.nz>](mailto:Weedpacket@varteg.nz) wrote:

On 2026-06-11 03:24, Rowan Tommins [IMSoP] wrote:

Take Guzzle, for example; it has 43 source files within a specific namespace root. A handful of those are marked "@internal", and a way for PHP to error if users reference them directly would be useful. Some users end up wanting conflicting versions of Guzzle simultaneously, e.g. in different WordPress plugins; so some way of isolating or rewriting class names (and all their references) would be useful.

There are not 43 separate "modules", and the maintainers of Guzzle aren't going to combine all of them into one file. Defining a single package with multiple files is not a stretch goal, it's the only plausible starting point.

Just throwing this out, but couldn't the "module file" - the one that users bring into their project and the one that exports what it's declared to export - delegate the definitions of the functions etc. both public and private to other files?

I mean, PHP already _has_ an "include" statement...

It could, but that just moves the problem: right now, I can use any class in Guzzle from anywhere in my application as long as I configure the right autoloader. I can pass objects from Guzzle into and out of other libraries, and they all agree on what each class name means.

Having to write an "import" statement means I have to be conscious of where I "start" using Guzzle. If every import of the library creates a magically prefixed copy of the entire library, I'm not even clear how I'd refer to different Guzzle classes in different files of my application; let alone how libraries could pass those objects in and out and agree that they were the same class.

Namespace visibility is really really easy; as if file visibility, if you want that. Usable namespace rewriting is much more difficult, but doing it per file makes it harder, not easier.

Namespace visibility could be implemented, but if you want to have some public code that uses “include” to get access to some other private code in the same namespace, or sub-namespace, then you would somehow hav to specify to whom this namespace becomes visible. Extending the module concept into “packages” makes sure that a package could have an entrypoint that is publicly accessible, and all files in said package would have to specify which package they are part of. But by design, namespaces aren’t dynamic, neither conditional (unless you define one class, then register lots of aliases of this class in different namespaces), so namespace visibility would still be easily “hackable” without having to touch PHP’s internals (like from using Reflection). It’s similar to namespace spoofing where you redefine a function in a certain namespace just for the sake of overriding native behavior. That’s why some CS fixers enforce adding “use function” or add the "" root namespace to function calls, to prevent from overriding native functions. So far, I don’t see how namespace/file visibility would help if you don’t have a bi-directional definition system. That’s usually what Packages are for: they define their public entrypoints, and all the rest is internal to the package and supposedly inaccessible from the global scope.

Another potential workaround would be “Friend classes” (and there are lots of discussions lately about it), but this system only works for classes and cannot be applied to other structure types. It’s not a bad idea, but this enforces “hiding” all your internal logic in classes. Constants would have to be internal static class constants, functions would be internal static class functions, and so on. As said, it’s not a bad idea, but IMO this is a “workaround with PHP’s existing tools”, and doesn’t improve the language, it just improves the OOP part. And wouldn’t fix the “multiple versions of same package” issue either.

As Larry said, just stop trying to make file==module work. It's not the right approach for PHP.

The file==module approach is imperfect, indeed. In Rust, you can create tons of modules in one single file, each with their own private and public code, but most of the times, developers create one file per module, and the module’s name is the filename itself. A Rust Crate (aka “package”) goes even further in allowing a module to expose its API publicly only for the package. I think this is the best approach, and it’s safer than how JS modules are implemented. However, considering the dynamic (instead of compile-only) nature of PHP, and considering the ecosystem’s occasional needs to have multiple versions for one package, the addition of an internal hashed prefix for each module makes it possible. It reuses everything that PHP already has, and it can be almost transparent for the end-user: they won’t write use but rather import instead, and maybe use a local name instead of a FQCN (though modules can define namespaces, therefore you can import a FCQN). And I still think that using namespaces for that, and namespace visibility, won’t help, since they all register things in the global scope with the same names, and doesn’t fix the other issues.

On 11 June 2026 09:12:52 BST, Alex Rock <pierstoval@gmail.com> wrote:

Namespace visibility could be implemented, but if you want to have some public code that uses "include" to get access to some other private code in the same namespace, or sub-namespace, then you would somehow hav to specify to whom this namespace becomes visible.

Why would you need that?

namespace visibility would still be easily "hackable" without having to touch PHP's internals (like from using Reflection).

Visibility isn't a security tool, it's a helper to stop people making mistakes.

That's why some CS fixers enforce adding "use function" or add the "\" root namespace to function calls, to prevent from overriding native functions.

I believe that's actually about performance: if you don't specify, the engine has to try the lookup on every call, in case a namespaced function was registered.

So far, I don't see how namespace/file visibility would help if you don't have a bi-directional definition system.

It's really, really simple: you mark the internal parts "internal". Then you use them from all the same places they're intended, inside the package's namespace; and people using them outside those intended places get a helpful error message that they're using the library wrong.

It's possible we also want to add a way to "seal" the namespace against new definitions anyway, to help with optimisation. Namespaces, not files, would still be the right starting point for that.

- It still allows creating a namespace from anywhere else in order to "hack" into it and publicly expose an API that wasn't supposed to be exposed at all.

Non-issue: as above, visibility is not a security tool.

- It doesn't solve the issue of having the same package in different versions, since namespaces will be the same

A solution to that which requires hundreds of thousands of libraries to rewrite all their code to use relative references rather than namespaced global names isn't going to get very far.

I'll say it one more time, then stop: any solution needs to start with what we have, and what we have is namespaces.

Rowan Tommins
[IMSoP]

Le 11/06/2026 à 11:54, Rowan Tommins [IMSoP] a écrit :

On 11 June 2026 09:12:52 BST, Alex Rock<pierstoval@gmail.com> wrote:

Namespace visibility could be implemented, but if you want to have some public code that uses "include" to get access to some other private code in the same namespace, or sub-namespace, then you would somehow hav to specify to whom this namespace becomes visible.

Why would you need that?

namespace visibility would still be easily "hackable" without having to touch PHP's internals (like from using Reflection).

Visibility isn't a security tool, it's a helper to stop people making mistakes.

That's why some CS fixers enforce adding "use function" or add the "\" root namespace to function calls, to prevent from overriding native functions.

I believe that's actually about performance: if you don't specify, the engine has to try the lookup on every call, in case a namespaced function was registered.

So far, I don't see how namespace/file visibility would help if you don't have a bi-directional definition system.

It's really, really simple: you mark the internal parts "internal". Then you use them from all the same places they're intended, inside the package's namespace; and people using them outside those intended places get a helpful error message that they're using the library wrong.

It's possible we also want to add a way to "seal" the namespace against new definitions anyway, to help with optimisation. Namespaces, not files, would still be the right starting point for that.

What if we have this:

<?php

// vendor/my-library/src/MyClass.php

namespace MyNamespace;

internal class MyClass {};

Fine code would look like this:

<?php

// index.php

function main() {
  $o = new \MyNamespace\MyClass(); // Fatal Error: cannot create instance of internal class MyNamespace\MyClass
  outside of the MyNamespace namespace
}

And "not fine code" would look like this:

<?php

// index.php

namespace MyNamespace {
  function createObject() {
    return new \MyNamespace\MyClass();
  }
}

namespace {
  function main() {
    $o = \MyNamespace\MyHack::createObject(); // Works.
  }
}

Your "internal" system would be quite useless, because anyone would be able to do it. It can even be done at the autoloader level if one wanted. Doesn't use Reflection or such.

Then, it leads to your first question:

On 11 June 2026 09:12:52 BST, Alex Rock<pierstoval@gmail.com> wrote:

Namespace visibility could be implemented, but if you want to have some public code that uses "include" to get access to some other private code in the same namespace, or sub-namespace, then you would somehow hav to specify to whom this namespace becomes visible.

Why would you need that?

Well, to solve the previously shown issue.

The "internal class" previously set (or moved to "internal namespace": same issue) is useless, and it would kinda do the same thing as /** @internal */ today: cannot be enforced at runtime, so not safe at all.

There's no need to add "internal class" if this system doesn't internalize anything and is easily "hackable" from outside.

If you want to have *actual* internal code, there are only two ways to achieve this:

- have code that's internal on said file, with the proposed "module" system. Sure, it's only for a file, but it's better than nothing
- have a "package" system so that a package has an entrypoint that defines private and public files, and said files expose which "package"

I repeat: since PHP is hugely flexible, especially via the whole Reflection API, I don't mean to "make hacking impossible", because, well, with PHP you can override everything. I mean to "make it as hard and ugly as possible". Sure you can execute a private method in a class/object by using either Reflection (or a bounded closure), but these are explicit hacks.
With "just namespaces", you can just use the widely used autoload system and create a class somewhere with a static method, or a global function (like above) , it doesn't even look like a hack.

- It still allows creating a namespace from anywhere else in order to "hack" into it and publicly expose an API that wasn't supposed to be exposed at all.

Non-issue: as above, visibility is not a security tool.

If bad practices are easily accessible from a new feature, you will expect lots of bad practices, because that's what lots of coders end up doing. We all know that, and if you don't know it, I hope you'll realize it some day. That's why "enforcing good practices" is a better approach, because it doesn't disallow bad code, it just makes it harder to produce, costing more energy, time, money, and technical debt.

- It doesn't solve the issue of having the same package in different versions, since namespaces will be the same

A solution to that which requires hundreds of thousands of libraries to rewrite all their code to use relative references rather than namespaced global names isn't going to get very far.

I'll say it one more time, then stop: any solution needs to start with what we have, and what we have is namespaces.

In that case, let's move directly to "packages" without talking about "modules" at all, and make namespaces package-aware somehow. Or the contrary: make a package implicitly create "protected" namespaces.

And use it like this for example:

<?php // vendor/some-package/package.php

define package MyPackage;

package_include MyPackage\MyClass from __DIR__.'/src/MyClass.php';

// ... and possibly all other includes
// (note: this is an idea, there might be another way.
// It could even be an spl_register_internal() function or anything similar
// that makes sure it's not possible to define package-bounded structures
// from outside the package definition file

<?php // vendor/some-package/src/MyClass.php

package MyPackage; // implies "namespace MyPackage;" too, to globally reference non-internal structures

internal class MyClass {};

<?php // invalid include

include 'vendor/some-package/src/MyClass.php'; // Fatal error: package "MyPackage" was not defined.

<?php // invalid instantiation

include 'vendor/some-package/src/package.php';
include 'vendor/some-package/src/MyClass.php'; // Just an appendage to the MyPackage, could even be autoloaded

new MyPackage\MyClass(); // Fatal error: cannot create instance of internal class MyClass

This way, once a `define package MyPackage` has been called, any further file doing `package MyPackage` will throw an error

The "Package" thing is a bit more recommended, but IMO it needs a bidirectional system.

Though, the concept of "modules" as I explained it, whether it uses namespaces or not, makes "package-alike" systems possible, even if it's in one single file.

Another "workaround" to define structures each in their file and merging them together in a module would be a "merging script" system, but that's ugly, and it is similar to the "compilation/transpilation" process in Node.js, where your publicly available package is a "built" version of your source code (what happens in NPM packages), rather than the source code itself (like in PHP or Rust).

I think I won't convice you of anything, and so far there are not many people participating in this discussion, so feel free to drop out if it's annoying or boring for you, I won't take it personally :slight_smile:

On 12 June 2026 13:37:18 BST, Alex Rock <pierstoval@gmail.com> wrote:

Your "internal" system would be quite useless, because anyone would be able to do it. It can even be done at the autoloader level if one wanted. Doesn't use Reflection or such.

I honestly don't think that's a problem. If someone writes code that deliberately works around an "internal" marker, they should not be surprised when that code breaks on library updates.

Right now, there is a package which dynamically removes the "final" keyword from every library installed. Does that mean "final" is useless? I don't think so.

Such "escape hatches" often have legitimate uses, e.g. when writing unit tests. But see below about "sealing"...

it would kinda do the same thing as /**

@internal */ today: cannot be enforced at runtime, so not safe at all.

The difference is that an "@internal" marker can be *accidentally* ignored: a user can see the class mentioned, and assume they can use it anywhere in their own code.

have code that's internal on said file, with the proposed "module"

system. Sure, it's only for a file, but it's better than nothing

You don't need a new type of file for this, you can just add a "file-private" keyword. The only difference from adding an "export" keyword is that definitions would be public by default, in the same way methods in a class are public by default.

This way, once a `define package MyPackage` has been called, any further file doing `package MyPackage` will throw an error

Yes, this is the general idea behind my remark about "sealing" a namespace: some way to say "all symbols in this namespace are loaded, do not allow more to be added". As well as stronger enforcement of boundaries, that would allow OpCache to do "link-time" optimisations and multifile caching.

It doesn't solve the "containerisation" problem (running multiple copies of the same library in different contexts), but I think that needs to be at a wider scale still: if you run two versions of Guzzle, you need to two versions of PSR interfaces to go with them, and so on. I think it needs some way for the *consumer* of the code (e.g. a WordPress plugin) to define what's "inside" and "outside" the container.

I don't think it's actually necessary (or useful) for a "container" system to depend on a "package" system at all - the ideal is to say "all files included in this directory are inside the container", without any cooperation from the author of those files.

So I think both "packages" (a way for the author of Guzzle to define a visibility and optimisation boundary) and "containers" (a way for a WordPress plugin to load a whole vendor directory without causing name conflicts) are useful ideas to discuss, but trying to talk about both at once leads to a lot of muddle.

Rowan Tommins
[IMSoP]

On Fri, Jun 12, 2026, at 7:37 AM, Alex Rock wrote:

What if we have this:

<?php

// vendor/my-library/src/MyClass.php

namespace MyNamespace;

internal class MyClass {};

Fine code would look like this:

<?php

// index.php

function main() {
  $o = new \MyNamespace\MyClass(); // Fatal Error: cannot create
instance of internal class MyNamespace\MyClass
outside of the MyNamespace namespace
}

And "not fine code" would look like this:

<?php

// index.php

namespace MyNamespace {
  function createObject() {
    return new \MyNamespace\MyClass();
  }
}

namespace {
  function main() {
    $o = \MyNamespace\MyHack::createObject(); // Works.
  }
}

Your "internal" system would be quite useless, because anyone would be
able to do it. It can even be done at the autoloader level if one
wanted. Doesn't use Reflection or such.

But it would allow a workaround to access a private class for testing. (And no, "it's private so you can't test it" is not the answer. Classes may be black boxes, but for testing purposes, modules are not and should not be. And in PHP, tests cannot be in the same file as the thing being tested.)

Then, it leads to your first question:

On 11 June 2026 09:12:52 BST, Alex Rock <pierstoval@gmail.com> wrote:

Namespace visibility could be implemented, but if you want to have some public code that uses "include" to get access to some other private code in the same namespace, or sub-namespace, then you would somehow hav to specify to whom this namespace becomes visible.

Why would you need that?

Well, to solve the previously shown issue.

The "internal class" previously set (or moved to "internal namespace":
same issue) is useless, and it would kinda do the same thing as /**
@internal */ today: cannot be enforced at runtime, so not safe at all.

There's no need to add "internal class" if this system doesn't
internalize anything and is easily "hackable" from outside.

If you want to have *actual* internal code, there are only two ways to
achieve this:

The biggest problem with discussions about modules in PHP is that there is no consensus on what problem we're even trying to solve. Visibility? Double-loading? Non-PSR-4 file organization? Performance? Enforcing PSR-1 "just declare symbols" rules?

Ask 10 people on this list and you'll get 12 different combinations of desired features. Trying to solve the How before we agree on the What is why we keep going in circles every year or so.

(This would be an excellent use case for a chartered Working Group, as Ben has proposed.)

- have code that's internal on said file, with the proposed "module"
system. Sure, it's only for a file, but it's better than nothing
- have a "package" system so that a package has an entrypoint that
defines private and public files, and said files expose which "package"

I would strongly urge everyone to keep to the terminology proposed here:

"Package" already means "thing you download via composer." It's a unit of distribution. Trying to redefine it to mean "a unit of scope" is just going to create confusion.

I'll say it one more time, then stop: any solution needs to start with what we have, and what we have is namespaces.

In that case, let's move directly to "packages" without talking about
"modules" at all, and make namespaces package-aware somehow. Or the
contrary: make a package implicitly create "protected" namespaces.

Please see the link above, in detail. Arnaud and I went very far down this rabbit hole. Make sure you've seen the prior art before you try to go down it again.

One other thing I'll note: In today's autoload-based PHP, *you do not know the path to a given class*. That is by design. There is no rule that composer won't reorganize the contents of vendor at some point. Hard coding a path into a file to get from that file to some other file in the file tree is a PHP4-ism we should leave to that era. So module inclusion statements that require knowing the path from one file to another is an immediate dead end.

--Larry Garfield

On Fri, Jun 12, 2026 at 12:54 PM Larry Garfield <larry@garfieldtech.com> wrote:

On Fri, Jun 12, 2026, at 7:37 AM, Alex Rock wrote:

What if we have this:

<?php // vendor/my-library/src/MyClass.php namespace MyNamespace; internal class MyClass {}; Fine code would look like this: <?php // index.php function main() { $o = new \MyNamespace\MyClass(); // Fatal Error: cannot create instance of internal class MyNamespace\MyClass outside of the MyNamespace namespace } And "not fine code" would look like this: <?php // index.php namespace MyNamespace { function createObject() { return new \MyNamespace\MyClass(); } } namespace { function main() { $o = \MyNamespace\MyHack::createObject(); // Works. } } Your "internal" system would be quite useless, because anyone would be able to do it. It can even be done at the autoloader level if one wanted. Doesn't use Reflection or such.

But it would allow a workaround to access a private class for testing. (And no, “it’s private so you can’t test it” is not the answer. Classes may be black boxes, but for testing purposes, modules are not and should not be. And in PHP, tests cannot be in the same file as the thing being tested.)

Then, it leads to your first question:

On 11 June 2026 09:12:52 BST, Alex Rock <pierstoval@gmail.com> wrote:

Namespace visibility could be implemented, but if you want to have some public code that uses “include” to get access to some other private code in the same namespace, or sub-namespace, then you would somehow hav to specify to whom this namespace becomes visible.
Why would you need that?
Well, to solve the previously shown issue.

The “internal class” previously set (or moved to “internal namespace”:
same issue) is useless, and it would kinda do the same thing as /**
@internal */ today: cannot be enforced at runtime, so not safe at all.

There’s no need to add “internal class” if this system doesn’t
internalize anything and is easily “hackable” from outside.

If you want to have actual internal code, there are only two ways to
achieve this:

The biggest problem with discussions about modules in PHP is that there is no consensus on what problem we’re even trying to solve. Visibility? Double-loading? Non-PSR-4 file organization? Performance? Enforcing PSR-1 “just declare symbols” rules?

Ask 10 people on this list and you’ll get 12 different combinations of desired features. Trying to solve the How before we agree on the What is why we keep going in circles every year or so.

(This would be an excellent use case for a chartered Working Group, as Ben has proposed.)

  • have code that’s internal on said file, with the proposed “module”
    system. Sure, it’s only for a file, but it’s better than nothing
  • have a “package” system so that a package has an entrypoint that
    defines private and public files, and said files expose which “package”

I would strongly urge everyone to keep to the terminology proposed here:

https://github.com/Crell/php-rfcs/blob/master/modules/spec-brainstorm.md

“Package” already means “thing you download via composer.” It’s a unit of distribution. Trying to redefine it to mean “a unit of scope” is just going to create confusion.

I’ll say it one more time, then stop: any solution needs to start with what we have, and what we have is namespaces.
In that case, let’s move directly to “packages” without talking about
“modules” at all, and make namespaces package-aware somehow. Or the
contrary: make a package implicitly create “protected” namespaces.

Please see the link above, in detail. Arnaud and I went very far down this rabbit hole. Make sure you’ve seen the prior art before you try to go down it again.

One other thing I’ll note: In today’s autoload-based PHP, you do not know the path to a given class. That is by design. There is no rule that composer won’t reorganize the contents of vendor at some point. Hard coding a path into a file to get from that file to some other file in the file tree is a PHP4-ism we should leave to that era. So module inclusion statements that require knowing the path from one file to another is an immediate dead end.

–Larry Garfield

Points taken. Rounding back to what I remember of last year’s conversation with Rowain, what about containers?

Requiring a container file gets you an interface to the container living in that file. The code execution occurs on a different thread entirely, so calls to and from it would ideally be asynchronous.

Function or process based containers should be doable from userland using shell_exec function (set aside a moment the security implications) with the caveat that the whole process has to run on any call. Illustrative of concept, but hardly production ready.

A more ideal case is for the container to return an interface that hosts multiple methods and persists until its parent process terminates.

With some work containers might also be able to step over initialization and config processes if a container is allowed to persist between page requests.

Key idea here is this - put this stuff on its own process thread. That doesn’t stop people from requiring files within the container directly. Containerizing existing libraries involves writing the wrapper.

On Fri, Jun 12, 2026 at 12:54 PM Larry Garfield <larry@garfieldtech.com> wrote:
> I would strongly urge everyone to keep to the terminology proposed here:
>
> php-rfcs/modules/spec-brainstorm.md at master · Crell/php-rfcs · GitHub

Done, that's a VERY interesting resource, thanks! I see that some of my ideas are close to what you brainstormed, and I have another point of view of how it can be implemented :slight_smile:
Using an INI file definitely makes sense if it needs these three keys to be defined somehow.
I'll try to reuse your wording for clarity and consistency :slight_smile:

Le 12/06/2026 à 19:25, Michael Morris a écrit :

On Fri, Jun 12, 2026 at 12:54 PM Larry Garfield <larry@garfieldtech.com> wrote:

    On Fri, Jun 12, 2026, at 7:37 AM, Alex Rock wrote:

    > Your "internal" system would be quite useless, because anyone
    would be
    > able to do it. It can even be done at the autoloader level if one
    > wanted. Doesn't use Reflection or such.

    But it would allow a workaround to access a private class for
    testing. (And no, "it's private so you can't test it" is not the
    answer. Classes may be black boxes, but for testing purposes,
    modules are not and should not be. And in PHP, tests cannot be in
    the same file as the thing being tested.)

One of the goals of modules is to encapsulate more elements of a module or package than just class methods, therefore private/internal elements of a module should not be accessible from outside, that's exactly the point of it. So yeah, "it's private so you can't test it" still has to apply, as said, just like you shouldn't test your private class methods (even though PHP allows it with hacks).

    > Then, it leads to your first question:
    >
    >> On 11 June 2026 09:12:52 BST, Alex Rock <pierstoval@gmail.com>
    wrote:
    >>> Namespace visibility could be implemented, but if you want to
    have some public code that uses "include" to get access to some
    other private code in the same namespace, or sub-namespace, then
    you would somehow hav to specify to whom this namespace becomes
    visible.
    >> Why would you need that?
    > Well, to solve the previously shown issue.
    >
    > The "internal class" previously set (or moved to "internal
    namespace":
    > same issue) is useless, and it would kinda do the same thing as /**
    > @internal */ today: cannot be enforced at runtime, so not safe
    at all.
    >
    > There's no need to add "internal class" if this system doesn't
    > internalize anything and is easily "hackable" from outside.
    >
    > If you want to have *actual* internal code, there are only two
    ways to
    > achieve this:

    The biggest problem with discussions about modules in PHP is that
    there is no consensus on what problem we're even trying to solve.
    Visibility? Double-loading? Non-PSR-4 file organization?
    Performance? Enforcing PSR-1 "just declare symbols" rules?

On my side:

1. Allowing internal code units, not exposed to the outside of a package or module, but accessible from within it.
This is for maintainers to have a much easier way to define non-BC-covered code.
2. PSR-1 "just declare symbols" is, indeed, something I personally would like to happen. But technically, this is the "first step" of my initial thread, the one with "definition files", and it's really easy to do (I have a branch on php-src for that). This is to ensure loading a PHP file has zero side-effect after being compiled.
3. Allowing multiple modules with same names to be loaded at the same time, as long as they are different files.

If these are problems that a sufficiently enough portion of the community would like to solve, then I think we should go for it somehow :slight_smile:

Your implementation proposal makes everything internal by default, AND allows using a specific keyword to expose parts of a structure, or the whole structure, to the outside world. This is partly similar to what I already have in mind for what I called "packages", because I pictured "module" as being a single file and its module-inclusion tree (similar to Rust crates, and Node.js local-file-imported modules)

What's missing, for now, is a method to allow two modules of same names to coexist.

    One other thing I'll note: In today's autoload-based PHP, *you do
    not know the path to a given class*. That is by design. There is
    no rule that composer won't reorganize the contents of vendor at
    some point. Hard coding a path into a file to get from that file
    to some other file in the file tree is a PHP4-ism we should leave
    to that era. So module inclusion statements that require knowing
    the path from one file to another is an immediate dead end.

Seems like a dead-end based on all counter-arguments I've been having since I started the discussion, so I'll have to follow this movement I guess :smiley:

Points taken. Rounding back to what I remember of last year’s conversation with Rowain, what about containers?

Requiring a container file gets you an interface to the container living in that file. The code execution occurs on a different thread entirely, so calls to and from it would ideally be asynchronous.

Function or process based containers should be doable from userland using shell_exec function (set aside a moment the security implications) with the caveat that the whole process has to run on any call. Illustrative of concept, but hardly production ready.

A more ideal case is for the container to return an interface that hosts multiple methods and persists until its parent process terminates.

With some work containers might also be able to step over initialization and config processes if a container is allowed to persist between page requests.

Key idea here is this - put this stuff on its own process thread. That doesn’t stop people from requiring files within the container directly. Containerizing existing libraries involves writing the wrapper.

Containers as "code block that's executed in another thread" seems like something completely different than implementing modules, or am I missing something?

On Sat, Jun 13, 2026 at 6:32 AM Alex “Pierstoval” Rock <pierstoval@gmail.com> wrote:

Containers as “code block that’s executed in another thread” seems like something completely different than implementing modules, or am I missing something?

It gives you what you’re looking for without having to overhaul the language in any massive way. I made the prior post on my phone where it’s hard to make a message, but I’m on my computer this morning so I can elaborate now. And while elaborating, I realized this probably needs it’s own discussion thread entirely because it can solve not just the original plugin problem, but also the process workers problem if handled correctly.

Let’s review.

PHP’s ecosystem has packages already. It doesn’t need modules as seen in JavaScript. What it needs in certain corner cases is better package isolation: The ability for a package to do its thing without disturbing or being disturbed by outside code. Drupal, Laravel, Symfony, et al do a great job of isolating their libraries, but they still have to register their class names and constants on the global name table. Under the hood Namespaces in PHP are a bit of a hack - they prepend the stated namespace to all labels declared in the file, and the use statement allows the fully qualified name of a class, function or constant to be stated only once, usually at the head of the file. It also allows aliasing, say if you have a “Parser” class out of two different packages - you can alias them as “FooParser” and “BarParser”. It looks like Java or JavaScript importing, but it isn’t.

The global namespace of PHP, both for variables and constants, is limited to the current thread. PHP isn’t natively multithreaded - there’s a whole other RFC on async/await in progress. What I’m about to go over would benefit from that work, but it isn’t a prerequisite to implementing containers.

A container is an interface to a PHP process on another thread. It takes the form of an object that is returned by the included file. Where returns are optional for include/require, the new statement require_container mandates that an object be returned. I’d go even further and mandate that the object must implement a ContainerInterface that is added to PHP core.

By virtue of running on its own thread a container has its own symbol table both for constants/classes/functions but also for variables. It is further restricted by having no superglobals - no $_REQUIRE, $_POST, $_GET, etc. If it needs to work with these then they’ll have to come in through the interface from the outside code, or perhaps a PHP internal function can supply them with their own copy of these variables, but crucially any modification they make will not affect other containers.

When PHP starts a container, it will cache the returned code and hand over a copy. This differs slightly from normal OP_Cache which caches the bytecode of the file itself so that new parsing isn’t needed. It was at this moment I realized that two problems, not one, get solved by this. Let’s start with the original though, package isolation. We’ll use Guzzle for an example. The calling code

<?php $guzzle = require_container('path/to/guzzle/container/class') ?>

Now in PHP we’ve tried to get away from having to reference specific paths like this. Coming up with ways to resolve that path can be done in userland just as composer does this in userland. One example might look like this:

<?php $guzzle = require_container(Container::getPath('Guzzle')) ?>

The required file might look like this, at least in part

<?php use GuzzleHttp\Client; require 'path/to/my/package/composer/autoloader'; return new GuzzleContainer implements Container { public static getClient($options) { return new Client($options); } } ?>

And I trust the folk who know how the engine works can clearly see if this has a hope in Hell of working or not. I’m going to stop here for their input. If it does though, here’s the other problem that might be solved. Here, we look at Drupal’s existing entry index.php

<?php
use Drupal\Core\DrupalKernel;

require_once 'autoload_runtime.php';

return static function () {
  return new DrupalKernel('prod', require 'autoload.php');
};

?>

Without digging deeply, suffice to say that “new DrupalKernel” call kicks off a lot of config processing that has to be done before the request can even start to be processed, and this is redundant to every request. Quite a bit of caching is in play to reduce this burden as much as possible, but if we have a container system, we can fetch a pre-built worker and avoid all of it.

<?php
(require_container 'drupal_worker.php')->parse($_GET, $_POST, $FILES, $_SERVER, $_SESSION);

?>

Remembering what I mentioned before, that the PHP Engine caches the worker’s state at the moment it is returned to the calling file. So it can get its own autoloader setup and configuration parsing completely done and be in a ready state to handle the request. It would likely require some serious refactoring to fully take advantage of this, but the performance gains should be very real.

Thoughts?