PHP RFC: Private classes and functions
- Date: 2024-12-11
- Author: Ilija Tovilo, tovilo.ilija@gmail.com
- Status: Draft
- Target Version: PHP 8.x
- Implementation: wip
Proposal
PHP has offered public
, protected
and private
visibility for methods, properties and constants for many years. Private and protected visibility allow abstracting away the inner workings of classes from the user. Minimizing the API surface clarifies how a library is supposed to be used, and dramatically decreases the risk of BC breaks when making changes to code the user should never interact with directly.
However, not only methods, properties and constants are routinely created for internal use only, but so are functions and entire classes. Currently, it is not possible to natively express this intention. All named classes and functions are accessible globally. This RFC proposes adding support for private classes and functions, which limits their usage to the current file or namespace block.
private function test() { echo __FUNCTION__, "\n"; } private class Test { public function __construct() { echo __METHOD__, "\n"; } } test(); // test@/home/ilutov/Developer/php-src/test.php:1$0 new Test(); // Test@/home/ilutov/Developer/php-src/test.php:5$1::__construct
Rationale
As foreshadowed, one of the primary advantages of marking classes and functions of private
is that they are removed from the public API, and prevent users from interacting with them in ways that are not intended by the library author. Today, such structures are commonly marked with a /** @internal */
comment. Native support for private
would provide stronger guarantees that they are not used outside of their intended scope.
The other primary motivator is to avoid clashes for classes used within single files. A good use-case for these are temporary classes in tests.
// ATest.php namespace Tests; private class DbMock { /* ... */ } class ATest { /* ... */ } // BTest.php namespace Tests; private class DbMock { /* ... */ } class BTest { /* ... */ }
We don't have to worry about disambiguation of the DbMock
class name, as it is only used in the current file and thus cannot clash with other private (or even public, non-use
d) classes.
Somewhat less importantly: In PHP, classes are usually placed in separate files to play nice with autoloaders. Classes that are exclusively used within a single other file could be moved to said file without issues. However, this puts the class in a weird state where it may still be used within other files, but can't be autoloaded on its own. private
classes will necessarily be placed in the file from which they are used; and given that they are not accessible in from files, autoloading becomes irrelevant.
Semantics
private
for classes and functions works similar to anonymous classes. Namely, the name of the structure is mangled by appending the file name, line number and incrementing id to the original name. Any usage of this name inside the current file is replaced with the mangled name at compile time, through the same mechanism used by use [function] Foo as Bar;
. This avoids trivial usage of the structure outside of its intended scope, and avoids accidental naming conflicts for internal structures.
// ATest.php private class DbMock { // ... } // loosely: class DbMock@/my/app/tests/ATest.php:1$0 { // ... } use DbMock@/my/app/tests/ATest.php:1$0 as DbMock;
This is entirely handled at compile time, and thus there is no runtime overhead to private classes or functions. Because this is a compile time concept, it is also circumventable. The same is already true for anonymous classes.
private class Test {} class_alias(Test::class, 'Test'); // In some other file: new Test();
Technically, because use
statements are restricted to the current namespace block, different namespace blocks may re-use the same private class name without clashing. Given that using different namespace blocks with the same namespace in the same file is pretty obscure, this is not particularly important for real code.
namespace Foo { private class Bar {} var_dump(new Bar()); // object(Foo\Bar@/home/ilutov/Developer/php-src/test.php:2$0)#1 (0) { } } namespace Foo { private class Bar {} var_dump(new Bar()); // object(Foo\Bar@/home/ilutov/Developer/php-src/test.php:7$1)#1 (0) { } }
Future scope
Likely even more common than file-private classes and functions are structures that should be restricted to some namespace. Contrary to private
, they should not do any name mangling and as such would require a runtime check, along with a way to specify what namespace they may be used in. As this is a more complex task, it should be handled in its own RFC.
It was suggested that nested classes could be an alternative to an explicit class visibility. However, it appears that nested classes could be useful even without being restricted to the surrounding class.
namespace Ast; class Node { enum Kind { case Identifier; case Symbol; } public function __construct( public Kind $kind, public string $lexeme, ) {} }
In this example, Ast\Node\Kind
is closely tied to Ast\Node
, so it makes sense to declare it in the same file. However, Ast\Node\Kind
should still be usable outside of the Ast\Node
context, namely to actually instantiate and identify AST nodes. Combining nested classes with private
visibility might be reasonable.
Vote
Voting starts xxxx-xx-xx and ends xxxx-xx-xx.
As this is a language change, a 2/3 majority is required.