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