Two-step creation for PHP modules

php.internals

Alex Pierstoval Rock

85 days ago
Again, another "modules" proposal, but in more steps. (sorry, this is a very long post). === TL;DR: the final goal (in a long time, possibly) is to be able to do something like this: <?php declare(module=1); // some_file.php export class SomeClass { public function someMessage() { myInternalFunction(); } } function myInternalFunction() { echo "Hello world!"; } <?php // index.php import SomeClass from 'some_file.php'; $o = new SomeClass(); $o->someMessage(); // Works. myInternalFunction(); // Fatal error: undefined function "myInternalFunction". === Trivia: 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. On my side, I've reviewed some of the discussions regarding "modules", and though it's quite messy because there are lots of different views and opinions, I think, maybe too optimistically, that there might be a way to pave the way to "PHP modules" in another way than a huge change in the entire engine. I think we can implement actual "modules" in two steps: - Implement *definition files* first, so they can be handled in the compilation step and have no runtime effect (see later). - Implement "*modules*" with an "import" keyword, and make "import"-ed modules in a way that a "module" is only a definition file that can "export" some of its defined structures and/or "import" structures from other modules, with a completely enclosed and standalone scope/context. === First step: *definition files*. One of the proposals for "modules" implied files with a different PHP extension, to make them easily distinguishible from other files, and the recent "pure files" suggestion follows the same idea: removing the "<?php" tag so that PHP doesn't need it anymore. However, these discussions had certain conclusions that I agree with: - Making the extensions different will profoundly change how PHP includes files. Extensions like ".inc", ".php5" or alike were discouraged for specific reasons, and nowadays, most PHP handlers (apache, nginx, caddy, and possibly others) are defaulted to ".php" extension files, so adding a new extension means changing both the engine and the whole ecosystem. The conclusion is overall that changing the extension will not give any benefit, and only brings disadvantages. - "pure" files, as in "no open/close tags" brings no real value, because having "<?php" is similar to having a shebang line in many other file types, and even PHP files themselves can contain a shebang line, so it's already a nice indicator, and ALL tooling around PHP code needs them to distinguish PHP code from "non-PHP" whatever-they-are characters. On my side, when I first read the "pure" proposal, I was thinking mostly about "pure" as in "has no side-effect". Which brings another idea: what if include-ing a PHP file actually had zero side-effect, apart its compilation process? That's where I'm coming with this idea: *definition files:*
> Notes: > - I will often refer to "built-in" in here, and "built-in" means
"built in PHP or one of the enabled extensions", which implies "accessible at compile-time"
> - When I say "global scope", I also imply "global namespace scope"
for every namespace defined in the file. * A PHP "definition" file is a file that has *ZERO global state, calls, or mutable statements*. * It is declared with a `declare(def=1);` statement at the top of it. * It can only contain *declarations*: `(include|require)(_once)?` `const`, `function`, `namespace`, `class`, `return`, `interface`, `trait`, `use`, `enum`, etc. * It can *include/require* a file, as long as this statement is a *string literal* (or a built-in constant) * Since the file must not contain statements, the global scope of the file must not refer to any variable, and must not define variables either. Even superglobals. * `if`/`else`/`elseif`, `switch` or `match` statements can also be allowed, only if they respect the previous points. This way, you can still define functions/constants/classes depending on PHP versions (see next points about constants). I'm not sure about iterators (`for`, `while`, `do...while` or `foreach`), because I see no proper use-case, but they can still be allowed if they imply no global call statements, though it seems very unlikely anyway. * `try/catch/finally` are useless if global scope has no calls, so they can be safely forbidden. * Statements like `break`, `continue` are not allowed either, because they implicitly expect a "parent context". * `new` is allowed for built-in classes only, and can only receive literals (because it cannot refer to variables or user-based constants.) * `exit/die` are also forbidden in the global scope, and should be replaced with exceptions. * `throw` is allowed, but can only throw exceptions from built-in eceptions. User-created exceptions are not allowed. * Global scope can never contain the closing tag `?>`, as a safety against potential "echo" calls. It /might/ use it as only last characters of said file, but IMO it's much easier to handle the "no closing tag" case than "possible as last statement of the file". The problem is that having a closing tag at the end can mess with IDEs that ensure a line feed at the end of every file (file that will therefore have an `echo "\n";` statement in it...) * The file's global scope can refer to constants defined internally by PHP or its extensions. This means every constant that isn't in the `user` key when calling `get_defined_constants(true)` in PHP, as well as magic constants like `__DIR__`. Only constants that are always available at compile-time will be checked. This way, it can't accidentaly trigger an `Undefined constant` warning for userland, but it can trigger one if a native extension isn't enabled or doesn't have said constant, which can also be detected at compile-time. This concept brings more advantages than previous proposals: * Your file is still normal PHP, compatible as usual. * Can still be interpreted by all IDEs that support PHP code * Doesn't need a different file extension * The fact that it's a definition file is explicitly visible at the beginning of the file when you open it * It can still be included/required by any other file without the file itself knowing that it actually includes a "definition file", and is therefore fully eligible to be compatible with all current autoload setups, including Composer * Still allows things that frameworks do for conditional function/class declaration (if it relies on the PHP version constant, for example). Will not be able to use `version_compare()` though, but there are workarounds. * Potential compile-time built-in constant optimization (if not already done by the compiler, I didn't search for this yet) * Everything that is not global/namespace-scope (functions, classes, etc.) can still contain whatever code they need, and theoretically it can even contain the PHP closing tag, since it's compiled as an "ECHO" statement. * All potential errors when including/requiring such file will be compile-time errors, therefore if the file is "correct", compiling it definitely means that it has its place in the opcache for a very long time as no runtime can alter its global context. * Having no actual call statements in the global/namespaced scope ensures no "echo", but overall has absolutely zero runtime impact other than compile-time errors, since there cannot be notice/warning errors that might also pollute the current buffer. (I might have forgotten what else can throw a notice/warning, but feel free to correct me if I do). There are only a tiny amount of drawbacks to this (from what I've thought about so far): * All definition files will have to begin with `<?php declare(def=1);` (fair enough IMO, since some static analysers are already capable of adding "declare(strict_types=1)` automatically...) * Potential tiny compile-time performance drop and/or memory consumption, because all global statements would have to be checked and analysed. And maybe a bit more if constants are also validated. For end-users, a "definition" file has only one single advantage: it has no runtime impact when being loaded, and only when its defined structures are used. This is a guarantee of trust that can benefit all frameworks and libraries. But this advantage paves the way to "modules" in a very interesting manner: all modules *must* be definition files in the first place. === Second step: *PHP modules*.
> TL;DR: loading a "module" is similar to an "include/require"_once,
but at the compiler-level instead of the engine/runtime-level. The concept of "module" in my mind in PHP is the following: A *PHP Module* is a normal PHP file that is, at first, a *definition file*, but instead starts with `declare(module=1);`. This declaration automatically implies `declare(def=1);`, and the wrong combo `declare(module=1,def=0);` can throw a compile-time error. On the Module side: * It has access to new keywords: `import` and `export`, as well as `import ... as ...` and `export ... as ...`. * Module names are useless, since the module is the file itself. * A module can `export` whatever is declared in said file: constants, functions, classes, enums, interfaces..., as long as it's only a declaration and not an actual call. * The `export` keyword must implicitly be in the global namespace, even if it is written inside another namespace. This implies that these variants have to be considered strictly equivalent: ``` <?php declare(module=1); namespace My\Namespace; export class MyClass {}; <?php declare(module=1); namespace My\Namespace {     export class MyClass {}; } <?php declare(module=1); namespace My\Namespace {     class MyClass {}; } export MyClass; ``` is the exact same as the following: * Composer can add a new package type named "php-module" that accepts only one single PHP file as input, that file must be a module. * A module can import other modules. * Conditional, or encapsulated `import` can also be resolved at compile-time, and since they can only contain compiler-accessible statements (string literals or built-in constants), the behavior will be similar to an "include" statement at runtime on a definition file anyway. * Unused imports can be detected at compile-time and throw a notice message * (my opinion, so definitely optional) Two exports must never have the same name. By this, I mean that you could use `import someFunction, SomeClass, SOME_CONSTANT from 'file.php';` freely without having to specify *what* you are trying to import. This is of personal taste, to enforce users not to use the same names to avoid confusion in general. I see no proper use-case to allow constants and classes to have the same name, for example, but some people might dislike this. On the engine-side it will still properly define the expected structures from the module, and on the userland, errors will be thrown if said structure is used improperly anyway. To me, considering how big this feature is, this is just a way to "opinionate for better naming" :) (and it avoids ugly things like `import const SOME_CONST as SOME_CONST_ALIAS, class SomeClass as SomeClassAlias, function someFunction as someFunctionAlias from 'file.php';`, right?) And on the userland-side: * The `import` and `import ... as ...` keywords also becomes accessible to ANY other PHP file, whether it is a definition, a module, or a regular PHP file. * Just like in definition files, the `import` keyword can only refer to string literals and/or built-in constants. This restriction is only applied for the new `import` keyword, and not to the rest of the file. * The `import` keyword will explicitly make all imported structures accessible in the current file, just like if the module was loaded with `include`, but only `export`-ed structures are accessible. * Imports can be placed anywhere in the file, and will be resolved at compile-time, since their only drawback is "adding more structures in memory". * An import can define an alias that will only be accessible to the importer-file, like `import SomeClass as MyAlias from 'file.php';`. Internally, there are other interesting things that happen: * All definitions from inside a module will have be prefixed in the symbols table with a hash corresponding to the current module file's hash. It can be similar to how anonymous classes are registered internally. The goal is to make them inaccessible (as much as possible) from the global scope. * All calls to module file's internal definitions will use this hash prefix to refer to said structures. * When using "import", it will do 3 things: o Analyse `import`-ed statements, to retrieve only the structures that are asked by the end-user o Load the file (from file, or from opcache, if not already in memory) o Create the modules definitions (if not already in memory) with internal hashes as previously described, and tree-shake unused structures as of the list of `import`-ed ones. Can be done at importer-compile-time too. This way, it would behave similarly to `(require|include)_once` but at a more granular level: with only the structures that were imported by the current file. Any subsequent call to `import` for the same file will do the same thing, and since these files have no runtime impact and only contain definitions, it should have close to no loading impact. Subsequent imports with the same structures will load only the ones that are already in memory (because they can be referenced with the hash-prefix), and if a "new structure" is found that has not been loaded already in the global space, it will create it in the global scope at runtime. This makes sure that module files with 100 exports will not load /all/ structures in memory when the file is `import`ed. * Modules have no impact on autoload, since they don't function the same way. * Since internal functions/constants/etc. are hash-prefixed, they will never conflict with other internal structures. This means that a module could define the `str_contains` function, if it wanted. And it could even reuse the native function by using `use function str_contains as base_str_contains`. Would also work for object-oriented structures (class, interface, etc.) as well as constants too. * We can create a *`ReflectionModule`* class, which constructor accepts a file name, and throws an exception if the file is not a module. This class would expose the list of exported structures from said module. Maybe it can also contain the processed hash/prefix and the internal structures too, but having these available kinda defeats the purpose of having internal structures in the first place... But the exported structures would be ReflectionClass, ReflectionConstant, etc, with the "module" flag explained in next point: * Other Reflection classes will contain an internal "defined in module" flag, as well as a nullable string corresponding to the path to the module file if the structure is effectively defined in a module. * FQCNs will always resolve to the "global public name", and never to the internal hash-prefixed name. * A new global function can be used: `spl_register_module($prefix, $filePath);`. This way, we could definitely imagine a flexible prefix to resolve to a module path for `import` statements. It allows Composer-package-compatible syntax like `import Request from '@symfony/http-foundation/Request.php';` being registered with something like `spl_register_module('@symfony/http-foundation', __DIR__.'/vendor/symfony/http-foundation/');` * The `spl_register_module` function can refer to structures directly, if needed: `spl_register_module('@symfony/http-foundation/Request', __DIR__.'/vendor/symfony/http-foundation/Request.php');`. This allows for no-extension imports like `import Request from '@symfony/http-foundation/Request';` (but this is just for the fancy looks) The function itself will register the input as a prefix or as a module path based on whether the specified path is a directory or a file, checked at runtime. * Multiple paths can be used for the same prefix, as long as they are all directories. * If a module is registered as a file instead of a directory, there can be only one. * (yeah, I know, this concept looks like a reinvention of include_paths, but hey, it's modules now!) * Autoload-like features can be used for projects using Composer: o The `composer.json` file can contain new field: "modules" and "modules_dev". o This field would contain a key=>value list of prefix=>file_path items, that Composer will register through the aforementioned new spl_register_module() function. o Composer will (sorry folks) need a way to make sure two PHP packages don't contain the same module prefixes resolving to two paths, regardless of them being directories or files. Maybe Packagist (sorry again) will need this too. This is important to avoid vendor name squatting in modules. * These module-autoload rules would not change anything at existing autoload, but they would mostly be here to map a PHP package with its exposed API. * This also makes sure that any PHP package can say "All API exposed as a module is covered by BC policy, all the rest is not". Easier for maintainers to keep their internal stuff, and a bit easier to make the Open/Closed principle available at a package-level instead of a class-level. === I already worked on the first step to "PHP definition files" and made a PR of it: https://github.com/php/php-src/compare/master...Pierstoval:php-src:defs With my tiny knowledge of PHP internals, I required the help of Cursor for that, and I added a lot of `.phpt` test files to ensure the basics are covered, built the project & ran all the tests on my LMDE 7 (Debian) machine multiple times with different configs (embed, fpm, debug, etc.), everything works so far. Apart the tests, the PR seems quite light, but it obviously needs thorough review (or rewrite...) before even being converted into an RFC. It just had the advantage of being fully ready and thoroughly tested (hopefully I didn't forget anything) in less than a day... /> Note: I did NOT use any llm to write this message, it was only used for some bits of code in the above PR, nothing more./ If you have read everything down to this line, thank you very much! It's the fruit of quite some work! Now to you folks, it's yours to take and talk :)
-- Alex "Pierstoval" Rock Polydisciplinary professional web development and training

Rowan Tommins [IMSoP]

84 days ago
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]

Michael Morris

84 days ago
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.

Alex Pierstoval Rock

84 days ago
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 :) 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).

Rowan Tommins [IMSoP]

83 days ago
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]

Alex Pierstoval Rock

83 days ago
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.

Rowan Tommins [IMSoP]

83 days ago
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]

Alex Pierstoval Rock

83 days ago
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.

Rowan Tommins [IMSoP]

83 days ago
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]

Weedpacket

82 days ago
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...

Larry Garfield

83 days ago
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

Alex Pierstoval Rock

83 days ago
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.