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.

Reply via email to