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,switchormatchstatements 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...whileorforeach), 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/finallyare useless if global scope has no calls, so
they can be safely forbidden.- Statements like
break,continueare not allowed either, because
they implicitly expect a "parent context". newis allowed for built-in classes only, and can only receive
literals (because it cannot refer to variables or user-based constants.)exit/dieare also forbidden in the global scope, and should be
replaced with exceptions.throwis 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 anecho "\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
userkey when callingget_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 anUndefined constantwarning 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 useversion_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:
importandexport, as well as
import ... as ...andexport ... as .... - Module names are useless, since the module is the file itself.
- A module can
exportwhatever is declared in said file: constants,
functions, classes, enums, interfaces..., as long as it's only a
declaration and not an actual call. - The
exportkeyword 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:
is the exact same as the following:<?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; - 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
importcan 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 useimport 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
importandimport ... 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
importkeyword can only refer
to string literals and/or built-in constants. This restriction is
only applied for the newimportkeyword, and not to the rest of
the file. - The
importkeyword will explicitly make all imported structures
accessible in the current file, just like if the module was loaded
withinclude, but onlyexport-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, likeimport 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 Analyseimport-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 ofimport-ed ones. Can be done at
importer-compile-time too.
This way, it would behave similarly to(require|include)_oncebut
at a more granular level: with only the structures that were
imported by the current file. Any subsequent call toimportfor
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 isimported. - 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 thestr_containsfunction, if it wanted. And
it could even reuse the native function by usinguse 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
ReflectionModuleclass, 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 forimportstatements. It
allows Composer-package-compatible syntax likeimport Request from '@symfony/http-foundation/Request.php';being registered with
something likespl_register_module('@symfony/http-foundation', __DIR__.'/vendor/symfony/http-foundation/'); - The
spl_register_modulefunction 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 likeimport 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 Thecomposer.jsonfile 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
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:
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:> 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).
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]
Le 09/06/2026 à 22:34, Rowan Tommins [IMSoP] a écrit :
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.
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:
- Add a keyword, or an attribute, at the top of each internal class, limiting its use to a certain namespace prefix.
- 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 :
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:
- Add a keyword, or an attribute, at the top of each internal class, limiting its use to a certain namespace prefix.
- 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.
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]
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...
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]
Le 11/06/2026 à 08:52, Rowan Tommins [IMSoP] a écrit :
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.
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
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.
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 :
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:
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 :)
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 MyPackagehas been called, any further file doingpackage MyPackagewill 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]
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:
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
On Fri, Jun 12, 2026 at 12:54 PM Larry Garfield larry@garfieldtech.com
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:
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:
https://github.com/Crell/php-rfcs/blob/master/modules/spec-brainstorm.md
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 :)
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 :)
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:> 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:
- 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. - 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. - 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 :)
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 :D
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?
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.
A small correction: this is exactly the same way that namespaces work in Java, with exactly the same implication: the fully qualified class name has to be globally unique. Java's solution to isolating an entire set of names is apparently to define an "Isolated ClassLoader": https://www.javathinking.com/blog/what-is-an-isolated-classloader-in-java/
C# also has this model of namespaces, but allows you to isolate a set of names from a particular "assembly" using an "extern alias" statement: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/extern-alias
I suspect both mechanisms rely on details of their respective runtimes that wouldn't translate to PHP, but in principle what we're looking for is something similar.
Rowan Tommins
[IMSoP]
Le 13/06/2026 à 19:14, Rowan Tommins [IMSoP] a écrit :
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.
A small correction: this is exactly the same way that namespaces work in Java, with exactly the same implication: the fully qualified class name has to be globally unique. Java's solution to isolating an entire set of names is apparently to define an "Isolated ClassLoader": https://www.javathinking.com/blog/what-is-an-isolated-classloader-in-java/C# also has this model of namespaces, but allows you to isolate a set of names from a particular "assembly" using an "extern alias" statement: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/extern-alias
I suspect both mechanisms rely on details of their respective runtimes that wouldn't translate to PHP, but in principle what we're looking for is something similar.
Rowan Tommins
[IMSoP]
Namespaces being just a globally accessible and define-able symbols
system is exactly why, so far, a PHP package cannot define "private
internal" code. The way the language works already allows
autoload-hijacking in order to override code from external libs, or even
hack the visibility (like removing "final" or "private" keywords before
autoloading the file...).
After 18 years of PHP coding, including 15 years as a professional, I've
seen a huge lot of legacy PHP projects that benefit the language's
flexibility. Sure thing many of businesses could succeed thanks to being
able to override almost everything (contrary to stricter languages), but
maintaining these codebases with up-to-date versions of PHP, or
3rd-party libs, is so much of a hassle that, at some point, lots of
maintainers are going more and more into adding new things into PHP that
enforce a bit more strictness, opt-in of course. Some other libs (I'm
thinking about EasyAdmin, in the Symfony ecosystem, for example) have
even made the choice to close pretty much everything and provide
"maintainable" public entrypoints, enforcing the Open/Close principle
with a strict approach (immutable final classes, stricter options and
arguments, etc.). It has its pros and cons, and can be frustrating for
some devs, but the main goal was to make maintenance a bit easier for
the core devs so that they are not forced to maintain BC on too many layers.
Introducing Modules, whether they are based on namespaces or not, would
help maintainers, I think, and could reinvent how we think of PHP
packages, while still keeping PHP what it has always been because it
would be opt-in for maintainers.
On my side, I don't care if modules are tightly coupled to namespaces or
not, and if more, that's even why my first idea was to bind a module to
a single-file at first, and making all modules imported from such file
being non-importable-again, thus creating a single module tree that can
be stored at compile-time, have no side-effect at load-time (which is a
big safeguard to avoid other issues), and allowing multiple modules of
seemingly same name to be loaded in the memory at the same time,
similarly to how Node.js allows having different versions of the same
packages to be installed, while also allowing to have one version used
by many modules too.
The only drawback of having modules like that would be that final
dependency trees in Composer would be non-flat, but again, I think this
non-flat system should be opt-in. Similarly to how flat systems are
opt-in in the Node.js ecosystem (and trust me, I've tried to enforce
"flat" dependencies resolutions in certain Node projects, and it's a
huge hassle). Luckily, the PHP ecosystem is quite fine so far, compared
to Node, and I would definitely keep the "flat" system in Composer for
as long as possible, and make non-flat resolutions only case-specific
(which is rare, and usually framework-dependent, like with Wordpress
plugins for instance).
I still hope we could have a grouped discussion on this.
Larry's Modules proposal (here:
https://github.com/Crell/php-rfcs/blob/master/modules/spec-brainstorm.md )
is IMO a very good starting point, and it only lacks the "multiple
modules with different versions". And module definition with an INI file
looks interesting.
Adding a "container" system when importing a module can maybe allow
this, I don't know...?
@Michael: do you think you could start a new thread for PHP containers?
Do you already have some research that you could bring for a pre-RFC
discussion?
Le 13/06/2026 à 19:14, Rowan Tommins [IMSoP] a écrit :
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.
A small correction: this is exactly the same way that namespaces work in
Java, with exactly the same implication: the fully qualified class name has
to be globally unique. Java's solution to isolating an entire set of names
is apparently to define an "Isolated ClassLoader":
https://www.javathinking.com/blog/what-is-an-isolated-classloader-in-java/C# also has this model of namespaces, but allows you to isolate a set of
names from a particular "assembly" using an "extern alias" statement:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/extern-aliasI suspect both mechanisms rely on details of their respective runtimes
that wouldn't translate to PHP, but in principle what we're looking for is
something similar.Rowan Tommins
[IMSoP]Namespaces being just a globally accessible and define-able symbols
system is exactly why, so far, a PHP package cannot define "private
internal" code. The way the language works already allows
autoload-hijacking in order to override code from external libs, or even
hack the visibility (like removing "final" or "private" keywords before
autoloading the file...).
PHP has never kept people from shooting themselves in the foot. And in the
past it's came with some big guns too (Register Globals in PHP 3 anyone?).
Enforcing namespace visibility will come at a cost in performance, and it
won't be enough to stop people from shooting themselves because PHP code is
quite mutable. I don't see much of a point in trying to guide people's
coding, especially in a language that doesn't have a set paradigm. That
is, in Java, you're using classes. In PHP you can approach things that way,
but you write purely procedurally. You can use the language as it was
originally conceived - a templating language.
After 18 years of PHP coding, including 15 years as a professional, I've
seen a huge lot of legacy PHP projects that benefit the language's
flexibility. Sure thing many of businesses could succeed thanks to being
able to override almost everything (contrary to stricter languages), but
maintaining these codebases with up-to-date versions of PHP, or
3rd-party libs, is so much of a hassle that, at some point, lots of
maintainers are going more and more into adding new things into PHP that
enforce a bit more strictness, opt-in of course. Some other libs (I'm
thinking about EasyAdmin, in the Symfony ecosystem, for example) have
even made the choice to close pretty much everything and provide
"maintainable" public entrypoints, enforcing the Open/Close principle
with a strict approach (immutable final classes, stricter options and
arguments, etc.). It has its pros and cons, and can be frustrating for
some devs, but the main goal was to make maintenance a bit easier for
the core devs so that they are not forced to maintain BC on too many
layers.
Introducing Modules, whether they are based on namespaces or not, would
help maintainers, I think, and could reinvent how we think of PHP
packages, while still keeping PHP what it has always been because it
would be opt-in for maintainers.
When you say modules, what are talking about, because multiple languages
implement "modules" in different ways. JavaScript is the most well known,
but Go also has modules and they are quite different and support versioning
out of the box. In each of these examples there is a requirement that the
programmer write code to import the modules at some place either in the
project (Go) or in every bloody file in the project (JavaScript). PHP
doesn't require this, instead autoloading symbol definitions as they are
encountered.
Any package/module/whatchamacallit system used in PHP needs to build on
this principle, not swim against it, if it is to have any hope of
consideration on the list.
Also, I think the following is important to point out - Composer is NOT a
package manager. It is a dependency manager. There's a subtle difference
between the two.
On my side, I don't care if modules are tightly coupled to namespaces or
not, and if more, that's even why my first idea was to bind a module to
a single-file at first, and making all modules imported from such file
being non-importable-again, thus creating a single module tree that can
be stored at compile-time, have no side-effect at load-time (which is a
big safeguard to avoid other issues), and allowing multiple modules of
seemingly same name to be loaded in the memory at the same time,
similarly to how Node.js allows having different versions of the same
packages to be installed, while also allowing to have one version used
by many modules too.
NPM (not Node, node is the engine not the package/dependency manager) does
this using aliases. PHP doesn't have a way to alias symbols at compile
time. It would be nice if it did - I've suggested it as a parameter to
include not knowing at the time of the suggestion that include isn't a
function, it's a statement, so include "file.php" is the same as
include("file.php"). Setting that aside, PHP projects rarely execute the
include themselves - they let an autoloader handle it, so the autoloader
must know which version to load. This isn't something that even NPM does -
it just aliases and makes both libraries available. The caller still must
specify which library is being pulled in.
Node modules have a single point of entry, defined in the package.json file
but usually index.js. In most Node packages this index.js imports all the
public parts of the package and immediately exports them. Further, an
import in node is an object (as is everything in JavaScript) that
corresponds to that file. Hence
import { foo, bar } from 'MyPackage';
This is using JavaScripts dereference mechanic to pluck members of an
object into the current symbol table. I could go on, but I think it's more
on topic to just say that Node's import/export mechanic, elegant as it
looks, is highly bound to JavaScript's scope rules and how its symbol
table works. These aren't compatible with PHP.
For one, PHP has two symbol tables - one for variables (which is why all
variables start with $ ) and one for everything else. Variables are
affected by function scope, nothing else is. For example, if you define a
function within a function in JavaScript that function will be private to
the function it is defined in - it can't be seen in other scopes. In PHP
the function will be visible on the symbol table.
For another, Node gives each file its own module scope. To move a symbol
from one file to another you should use export and import. This creates a
clear chain of custody for objects and symbols, but by God it also creates
a LOT of boilerplate that the PHP community has no interest in replicating.
Without the chain of custody you rapidly run into trouble. It doesn't
matter if you try to allow the user to put a prefix on the namespace as I
once suggested, or have PHP's engine prefix it automagically somehow - you
still need to correctly disambiguate every blasted reference to that
namespace. This is no easy task - take a look at the code of Strauss -
https://github.com/BrianHenryIE/strauss That project allows you to rewrite
a package with a new namespace, in effect doing it in userland. But as a
precaution it prefixes every single package imported and severs the tie
back to the original packagist. Strauss is used by several WordPress
plugins to make composer library use possible while still remaining
portable between WordPress sites.
The only drawback of having modules like that would be that final
dependency trees in Composer would be non-flat, but again, I think this
non-flat system should be opt-in. Similarly to how flat systems are
opt-in in the Node.js ecosystem (and trust me, I've tried to enforce
"flat" dependencies resolutions in certain Node projects, and it's a
huge hassle). Luckily, the PHP ecosystem is quite fine so far, compared
to Node, and I would definitely keep the "flat" system in Composer for
as long as possible, and make non-flat resolutions only case-specific
(which is rare, and usually framework-dependent, like with Wordpress
plugins for instance).
I don't think it's a reasonable ask of Composer or any other dependency
manager to try to resolve this without having a clear binding map like the
one JavaScript forces you to create by how its module system works.
I still hope we could have a grouped discussion on this.
Larry's Modules proposal (here:
https://github.com/Crell/php-rfcs/blob/master/modules/spec-brainstorm.md )is IMO a very good starting point, and it only lacks the "multiple
modules with different versions". And module definition with an INI file
looks interesting.Adding a "container" system when importing a module can maybe allow
this, I don't know...?@Michael: do you think you could start a new thread for PHP containers?
Do you already have some research that you could bring for a pre-RFC
discussion?
This is the Container thread - I changed the subject line on my previous
reply although I did include (was: ) so that people could track I was
forking.
I don't have much to add beyond my previous post. I haven't heard from
anyone with engine experience about the feasibility or difficulty of what I
propose. To review (for anyone just joining but missing the previous
message)
- The user signals to the engine they want to include a php file AND
process it independent of the current execution environment. No shared user
defined symbols or variables. - That file returns an interface so that the calling thread can talk to
the child. - As the processes reside on different threads these calls will need to be
asynchronous to be optimal, otherwise the caller will be in a blocked state
while the child executes the called method. It can work without the PHP
Async / Await RFC's conclusion, but it won't be in ideal form without it.
Containers provide process isolation. Say I have a WordPress plugin that I
want to use composer libraries with. Say, Guzzle. I can write my own
container interface without touching Guzzle's code exposing the parts of
Guzzle I need for my plugin. Since WordPress has no current core composer
support I would need to carry a loader in my distro to be ran on the
install hook of the plugin, bringing in composer and letting it in turn
pull the libraries that I need to use. If someone later writes a plugin
that uses an incompatible version of Guzzle that doesn't matter - even if
they lazily load it into the main symbol table by not using a container.
My plugin is isolated.
That isolation comes at a price. Each plugin has an independent vendor
directory with no way to resolve complete redundancy. At least not in
WordPress, which has no current composer implementation in core (I'm on the
wp trac in a thread discussing it though).
There are open questions though that must be answered before this container
idea is seriously explored. Most critical, can PHP be made to handle an
object passing from one thread to another? That is, continuing from the
example above, can a client instance from one GuzzleContainer coexist on
the main thread with a client instance from a different container and
referencing different code? Or will methods be unable to cross the barrier?
What are the implications for the Reflection API and using it to examine an
object that originated from another process thread referencing symbols that
don't exist on the current thread?
The idea of containers is simple to state - but the implications are
profound and might be unworkable. I simply don't know enough about the PHP
engine to know.
Le 16/06/2026 à 06:45, Michael Morris a écrit :
Namespaces being just a globally accessible and define-able symbols system is exactly why, so far, a PHP package cannot define "private internal" code. The way the language works already allows autoload-hijacking in order to override code from external libs, or even hack the visibility (like removing "final" or "private" keywords before autoloading the file...).PHP has never kept people from shooting themselves in the foot. And in
the past it's came with some big guns too (Register Globals in PHP 3
anyone?). Enforcing namespace visibility will come at a cost in
performance, and it won't be enough to stop people from shooting
themselves because PHP code is quite mutable. I don't see much of a
point in trying to guide people's coding, especially in a language
that doesn't have a set paradigm. That is, in Java, you're using
classes. In PHP you can approach things that way, but you write purely
procedurally. You can use the language as it was originally conceived
- a templating language.
I think it's morally good to bring new features to the core that
disallow someone from shooting themselves in the foot. Readonly classes,
property hooks, etc., are methods for maintainers to be able to stricten
their code so that end users don't shoot themselves in the foot. Yet we
have the Reflection API that permits a huge lot of things, and combining
with autoload hacking or bounded closures, it means that all these
requirements, strictening tools, etc., are contracts we sign with
maintainers. If someone uses any hacky tool built in PHP to override its
native behavior, it's at their own risk. But the language allows it.
With my suggestion for modules, having all structures being declared
globally but with a prefix still allows someone to hack into it. It just
lowers the pressure on how it's developed in the core, in order for it
to be both simple to use, but also simpler to implement. Doesn't prevent
feet shots, but has a big warning sign nonetheless.
When you say modules, what are talking about, because multiple
languages implement "modules" in different ways. JavaScript is the
most well known, but Go also has modules and they are quite different
and support versioning out of the box. In each of these examples there
is a requirement that the programmer write code to import the modules
at some place either in the project (Go) or in every bloody file in
the project (JavaScript). PHP doesn't require this, instead
autoloading symbol definitions as they are encountered.
There are different terms available for the things we talk about, so as
a reminder to the two/three people reading this message:
- Module = file, similar to JS. This implies that everything defined
in the file is private, unless "publicly exposed", either via a
"public" keyword (as Larry suggests in his modules-brainstorming
document), or an "export" statement (similar to JS/TS). - Module = package, in that case, this means that a module is composed
of several files, but a module/package needs an entrypoint to define
its components. Larry's aforementioned idea is about using an INI
file as entrypoint that prevents double-loading of a list of files,
but this could be implemented with other methods: my proposal for
"module = file" could add a "package" system without a need for an
INI file, and based purely on the list of publicly available
structures from the main imported file, but implies a bidirectional
relationship between the "main package file" (aka "entrypoint") and
the "package-included-only file". It's just a different approach.
None of these approaches removes namespaces, and considering my first
idea and re-thinking it after all the comments in here, I think focusing
on "using namespaces for that" is definitely the best approach. My
proposal still stands, it just needs an update to better use namespaces
for that need.
NPM (not Node, node is the engine not the package/dependency manager)
does this using aliases. PHP doesn't have a way to alias symbols at
compile time. It would be nice if it did - I've suggested it as a
parameter to include not knowing at the time of the suggestion that
include isn't a function, it's a statement, soinclude "file.php"is
the same asinclude("file.php"). Setting that aside, PHP projects
rarely execute the include themselves - they let an autoloader handle
it, so the autoloader must know which version to load. This isn't
something that even NPM does - it just aliases and makes both
libraries available. The caller still must specify which library is
being pulled in.Node modules have a single point of entry, defined in the package.json
file but usually index.js. In most Node packages this index.js imports
all the public parts of the package and immediately exports them.
Further, an import in node is an object (as is everything in
JavaScript) that corresponds to that file. Henceimport { foo, bar } from 'MyPackage';
This is using JavaScripts dereference mechanic to pluck members of an
object into the current symbol table. I could go on, but I think it's
more on topic to just say that Node's import/export mechanic, elegant
as it looks, is highly bound to JavaScript's scope rules and how its
symbol table works. These aren't compatible with PHP.
I'm a bit familiar with Node.js's import model. PHP could be partly
compatible with this in the future, but complete scoped encapsulation of
a single PHP file is too much of a paradigm change for PHP itself. My
proposal is making this "private" by prepending a null character and
hash to all "included modules" whatever their kind, so that it
/feels/ private, but still behaves similarly to how PHP currently works.
Less maintenance for the core team, zero BC break (purely additive and
opt-in), can be transparent for the end-user (and is transparent for
transient dependencies), still overridable and hackable by developers.
Some of the reasons why PHP became famous in the first place.
For one, PHP has two symbol tables - one for variables (which is why
all variables start with $ ) and one for everything else. Variables
are affected by function scope, nothing else is. For example, if you
define a function within a function in JavaScript that function will
be private to the function it is defined in - it can't be seen in
other scopes. In PHP the function will be visible on the symbol table.For another, Node gives each file its own module scope. To move a
symbol from one file to another you should use export and import. This
creates a clear chain of custody for objects and symbols, but by God
it also creates a LOT of boilerplate that the PHP community has no
interest in replicating.Without the chain of custody you rapidly run into trouble. It doesn't
matter if you try to allow the user to put a prefix on the namespace
as I once suggested, or have PHP's engine prefix it automagically
somehow - you still need to correctly disambiguate every blasted
reference to that namespace. This is no easy task - take a look at the
code of Strauss - https://github.com/BrianHenryIE/strauss That project
allows you to rewrite a package with a new namespace, in effect doing
it in userland. But as a precaution it prefixes every single package
imported and severs the tie back to the original packagist. Strauss
is used by several WordPress plugins to make composer library use
possible while still remaining portable between WordPress sites.
Interesting, and auto-prefixing namespaces for module=packages systems
is IMO the easiest way to implement this without breaking the compiler
nor the engine. It keeps everything as-is, and visually provides all
that's needed for a "modules/packages" system to work: encapsulated
private structures, an explicit public API, and the ability to load the
same structures in the same namespaces because they are internally
hidden with a hash, similarly to anonymous classes.
The only drawback of having modules like that would be that final dependency trees in Composer would be non-flat, but again, I think this non-flat system should be opt-in. Similarly to how flat systems are opt-in in the Node.js ecosystem (and trust me, I've tried to enforce "flat" dependencies resolutions in certain Node projects, and it's a huge hassle). Luckily, the PHP ecosystem is quite fine so far, compared to Node, and I would definitely keep the "flat" system in Composer for as long as possible, and make non-flat resolutions only case-specific (which is rare, and usually framework-dependent, like with Wordpress plugins for instance).I don't think it's a reasonable ask of Composer or any other
dependency manager to try to resolve this without having a clear
binding map like the one JavaScript forces you to create by how its
module system works.
Composer doesn't have to change, because modules might be autoloadable.
Whatever the solution found: if it's a bidirectional relationship
(meaning any included file contains information on the "package" it's
in, guiding to the entrypoint) or with a config file (like with an INI
file), it can be built-in, and Composer would be compatible as long as
loaded structures are public and not internal to the module.
This is the Container thread - I changed the subject line on my
previous reply although I did include (was: ) so that people could
track I was forking.I don't have much to add beyond my previous post. I haven't heard from
anyone with engine experience about the feasibility or difficulty of
what I propose. To review (for anyone just joining but missing the
previous message)
- The user signals to the engine they want to include a php file AND
process it independent of the current execution environment. No shared
user defined symbols or variables.- That file returns an interface so that the calling thread can talk
to the child.- As the processes reside on different threads these calls will need
to be asynchronous to be optimal, otherwise the caller will be in a
blocked state while the child executes the called method. It can work
without the PHP Async / Await RFC's conclusion, but it won't be in
ideal form without it.Containers provide process isolation. Say I have a WordPress plugin
that I want to use composer libraries with. Say, Guzzle. I can write
my own container interface without touching Guzzle's code exposing the
parts of Guzzle I need for my plugin. Since WordPress has no current
core composer support I would need to carry a loader in my distro to
be ran on the install hook of the plugin, bringing in composer and
letting it in turn pull the libraries that I need to use. If someone
later writes a plugin that uses an incompatible version of Guzzle that
doesn't matter - even if they lazily load it into the main symbol
table by not using a container. My plugin is isolated.That isolation comes at a price. Each plugin has an independent vendor
directory with no way to resolve complete redundancy. At least not in
WordPress, which has no current composer implementation in core (I'm
on the wp trac in a thread discussing it though).There are open questions though that must be answered before this
container idea is seriously explored. Most critical, can PHP be made
to handle an object passing from one thread to another? That is,
continuing from the example above, can a client instance from one
GuzzleContainer coexist on the main thread with a client instance from
a different container and referencing different code? Or will methods
be unable to cross the barrier? What are the implications for the
Reflection API and using it to examine an object that originated from
another process thread referencing symbols that don't exist on the
current thread?The idea of containers is simple to state - but the implications are
profound and might be unworkable. I simply don't know enough about the
PHP engine to know.
This seems completely out of a "modules" scope to me: threading comes
with a huge cost (like state interoperability, thread healthchecks,
memory usage, and many other things), and this brings a lot of
complexity that is not needed yet IMO. If someone wants to "sandbox" PHP
somehow, it's already possible to execute another PHP script from inside
a PHP script via CLI, and passing arguments to it, but there's no
back-and-forth communication, only a "request-response-like" system.
Le 16/06/2026 à 06:45, Michael Morris a écrit :
Namespaces being just a globally accessible and define-able symbols
system is exactly why, so far, a PHP package cannot define "private
internal" code. The way the language works already allows
autoload-hijacking in order to override code from external libs, or even
hack the visibility (like removing "final" or "private" keywords before
autoloading the file...).PHP has never kept people from shooting themselves in the foot. And in the
past it's came with some big guns too (Register Globals in PHP 3 anyone?).
Enforcing namespace visibility will come at a cost in performance, and it
won't be enough to stop people from shooting themselves because PHP code is
quite mutable. I don't see much of a point in trying to guide people's
coding, especially in a language that doesn't have a set paradigm. That
is, in Java, you're using classes. In PHP you can approach things that way,
but you write purely procedurally. You can use the language as it was
originally conceived - a templating language.I think it's morally good to bring new features to the core that disallow
someone from shooting themselves in the foot. Readonly classes, property
hooks, etc., are methods for maintainers to be able to stricten their code
so that end users don't shoot themselves in the foot. Yet we have the
Reflection API that permits a huge lot of things, and combining with
autoload hacking or bounded closures, it means that all these requirements,
strictening tools, etc., are contracts we sign with maintainers. If
someone uses any hacky tool built in PHP to override its native behavior,
it's at their own risk. But the language allows it.With my suggestion for modules, having all structures being declared
globally but with a prefix still allows someone to hack into it. It just
lowers the pressure on how it's developed in the core, in order for it to
be both simple to use, but also simpler to implement. Doesn't prevent feet
shots, but has a big warning sign nonetheless.When you say modules, what are talking about, because multiple languages
implement "modules" in different ways. JavaScript is the most well known,
but Go also has modules and they are quite different and support versioning
out of the box. In each of these examples there is a requirement that the
programmer write code to import the modules at some place either in the
project (Go) or in every bloody file in the project (JavaScript). PHP
doesn't require this, instead autoloading symbol definitions as they are
encountered.There are different terms available for the things we talk about, so as a
reminder to the two/three people reading this message:
- Module = file, similar to JS. This implies that everything defined
in the file is private, unless "publicly exposed", either via a "public"
keyword (as Larry suggests in his modules-brainstorming document), or an
"export" statement (similar to JS/TS).- Module = package, in that case, this means that a module is composed
of several files, but a module/package needs an entrypoint to define its
components. Larry's aforementioned idea is about using an INI file as
entrypoint that prevents double-loading of a list of files, but this could
be implemented with other methods: my proposal for "module = file" could
add a "package" system without a need for an INI file, and based purely on
the list of publicly available structures from the main imported file, but
implies a bidirectional relationship between the "main package file" (aka
"entrypoint") and the "package-included-only file". It's just a different
approach.None of these approaches removes namespaces, and considering my first idea
and re-thinking it after all the comments in here, I think focusing on
"using namespaces for that" is definitely the best approach. My proposal
still stands, it just needs an update to better use namespaces for that
need.NPM (not Node, node is the engine not the package/dependency manager) does
this using aliases. PHP doesn't have a way to alias symbols at compile
time. It would be nice if it did - I've suggested it as a parameter to
include not knowing at the time of the suggestion that include isn't a
function, it's a statement, soinclude "file.php"is the same as
include("file.php"). Setting that aside, PHP projects rarely execute the
include themselves - they let an autoloader handle it, so the autoloader
must know which version to load. This isn't something that even NPM does -
it just aliases and makes both libraries available. The caller still must
specify which library is being pulled in.Node modules have a single point of entry, defined in the package.json
file but usually index.js. In most Node packages this index.js imports all
the public parts of the package and immediately exports them. Further, an
import in node is an object (as is everything in JavaScript) that
corresponds to that file. Henceimport { foo, bar } from 'MyPackage';
This is using JavaScripts dereference mechanic to pluck members of an
object into the current symbol table. I could go on, but I think it's more
on topic to just say that Node's import/export mechanic, elegant as it
looks, is highly bound to JavaScript's scope rules and how its symbol
table works. These aren't compatible with PHP.I'm a bit familiar with Node.js's import model. PHP could be partly
compatible with this in the future, but complete scoped encapsulation of a
single PHP file is too much of a paradigm change for PHP itself. My
proposal is making this "private" by prepending a null character and hash
to all "included modules" whatever their kind, so that it feels private,
but still behaves similarly to how PHP currently works.
Less maintenance for the core team, zero BC break (purely additive and
opt-in), can be transparent for the end-user (and is transparent for
transient dependencies), still overridable and hackable by developers. Some
of the reasons why PHP became famous in the first place.For one, PHP has two symbol tables - one for variables (which is why all
variables start with $ ) and one for everything else. Variables are
affected by function scope, nothing else is. For example, if you define a
function within a function in JavaScript that function will be private to
the function it is defined in - it can't be seen in other scopes. In PHP
the function will be visible on the symbol table.For another, Node gives each file its own module scope. To move a symbol
from one file to another you should use export and import. This creates a
clear chain of custody for objects and symbols, but by God it also creates
a LOT of boilerplate that the PHP community has no interest in replicating.Without the chain of custody you rapidly run into trouble. It doesn't
matter if you try to allow the user to put a prefix on the namespace as I
once suggested, or have PHP's engine prefix it automagically somehow - you
still need to correctly disambiguate every blasted reference to that
namespace. This is no easy task - take a look at the code of Strauss -
https://github.com/BrianHenryIE/strauss That project allows you to
rewrite a package with a new namespace, in effect doing it in userland. But
as a precaution it prefixes every single package imported and severs the
tie back to the original packagist. Strauss is used by several WordPress
plugins to make composer library use possible while still remaining
portable between WordPress sites.Interesting, and auto-prefixing namespaces for module=packages systems is
IMO the easiest way to implement this without breaking the compiler nor the
engine. It keeps everything as-is, and visually provides all that's needed
for a "modules/packages" system to work: encapsulated private structures,
an explicit public API, and the ability to load the same structures in the
same namespaces because they are internally hidden with a hash, similarly
to anonymous classes.The only drawback of having modules like that would be that final
dependency trees in Composer would be non-flat, but again, I think this
non-flat system should be opt-in. Similarly to how flat systems are
opt-in in the Node.js ecosystem (and trust me, I've tried to enforce
"flat" dependencies resolutions in certain Node projects, and it's a
huge hassle). Luckily, the PHP ecosystem is quite fine so far, compared
to Node, and I would definitely keep the "flat" system in Composer for
as long as possible, and make non-flat resolutions only case-specific
(which is rare, and usually framework-dependent, like with Wordpress
plugins for instance).I don't think it's a reasonable ask of Composer or any other dependency
manager to try to resolve this without having a clear binding map like the
one JavaScript forces you to create by how its module system works.Composer doesn't have to change, because modules might be autoloadable.
Whatever the solution found: if it's a bidirectional relationship (meaning
any included file contains information on the "package" it's in, guiding to
the entrypoint) or with a config file (like with an INI file), it can be
built-in, and Composer would be compatible as long as loaded structures are
public and not internal to the module.This is the Container thread - I changed the subject line on my previous
reply although I did include (was: ) so that people could track I was
forking.I don't have much to add beyond my previous post. I haven't heard from
anyone with engine experience about the feasibility or difficulty of what I
propose. To review (for anyone just joining but missing the previous
message)
- The user signals to the engine they want to include a php file AND
process it independent of the current execution environment. No shared user
defined symbols or variables.- That file returns an interface so that the calling thread can talk to
the child.- As the processes reside on different threads these calls will need to
be asynchronous to be optimal, otherwise the caller will be in a blocked
state while the child executes the called method. It can work without the
PHP Async / Await RFC's conclusion, but it won't be in ideal form without
it.Containers provide process isolation. Say I have a WordPress plugin that I
want to use composer libraries with. Say, Guzzle. I can write my own
container interface without touching Guzzle's code exposing the parts of
Guzzle I need for my plugin. Since WordPress has no current core composer
support I would need to carry a loader in my distro to be ran on the
install hook of the plugin, bringing in composer and letting it in turn
pull the libraries that I need to use. If someone later writes a plugin
that uses an incompatible version of Guzzle that doesn't matter - even if
they lazily load it into the main symbol table by not using a container.
My plugin is isolated.That isolation comes at a price. Each plugin has an independent vendor
directory with no way to resolve complete redundancy. At least not in
WordPress, which has no current composer implementation in core (I'm on the
wp trac in a thread discussing it though).There are open questions though that must be answered before this
container idea is seriously explored. Most critical, can PHP be made to
handle an object passing from one thread to another? That is, continuing
from the example above, can a client instance from one GuzzleContainer
coexist on the main thread with a client instance from a different
container and referencing different code? Or will methods be unable to
cross the barrier? What are the implications for the Reflection API and
using it to examine an object that originated from another process thread
referencing symbols that don't exist on the current thread?The idea of containers is simple to state - but the implications are
profound and might be unworkable. I simply don't know enough about the PHP
engine to know.This seems completely out of a "modules" scope to me: threading comes with
a huge cost (like state interoperability, thread healthchecks, memory
usage, and many other things), and this brings a lot of complexity that is
not needed yet IMO. If someone wants to "sandbox" PHP somehow, it's already
possible to execute another PHP script from inside a PHP script via CLI,
and passing arguments to it, but there's no back-and-forth communication,
only a "request-response-like" system.
However complex threading is, the complexity of aliasing is going to be
higher. I've walked down this thought experiment several times in the last
4 years. Threading isn't easy, but the work is in progress by Edmond HT -
https://wiki.php.net/rfc/true_async
Containers, as I'm proposing them, rest on the foundation of this already
in progress RFC which in turn builds on ReactPHP, AMPHP, Swoole and others.
In my opinion this ...
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).
... doesn't match this:
- 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.
Just because a piece of functionality is internal to the library, doesn't mean it shouldn't be covered by unit tests. Indeed that's almost the definition of a unit test vs an integration test, if the library only exposes a narrow interface; and "refactor into a separate helper" is a common answer to "I want to unit test a private method".
Meanwhile this ...
- Allowing multiple modules with same names to be loaded at the same time, as long as they are different files.
... basically answers this:
Containers as "code block that's executed in another thread" seems like something completely different than implementing modules, or am I missing something?
In order to reuse code written assuming a class name is unique (which is every line of PHP currently in existence) you need to define a boundary where code "inside" runs without affecting the code "outside", except through a specific interface. Whether that would literally be two separate threads, I'm not sure, but it certainly has a lot in common.
And yes, that is completely different from "modules" as units of scope. It needs to affect the public parts of a module, as well as the private parts; and it needs to affect a group of modules which rely on each other, not each in isolation.
Rowan Tommins
[IMSoP]
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 10/06/2026 à 00:24, Larry Garfield a écrit :
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.