This RFC proposes adding namespace-scoped visibility to methods and properties in PHP, using the syntax private(namespace). This allows classes within the same namespace to access each other’s internal APIs without exposing them publicly to the entire codebase.
PHP currently has three visibility levels:
public: Accessible everywhereprotected: Accessible within class hierarchy (inheritance-based)private: Accessible only within the same classThere’s no way to share code between classes in the namespace without making it public to the entire application.
namespace App\Auth; class SessionManager { public function validateSession(): bool { return $this->checkExpiry() && $this->verifySignature(); } // PROBLEM: Must be public to be called from SessionStore, // but we don't want these exposed to the entire codebase public function checkExpiry(): bool { /* ... */ } public function verifySignature(): bool { /* ... */ } } class SessionStore { public function refresh(SessionManager $session): void { // Needs to call internal SessionManager methods if (!$session->checkExpiry()) { // ... } } }
In the example above, SessionManager exposes two public methods: checkExpiry() and verifySignature(). To someone reading the class, they look like safe, standalone API calls.
But they’re not. They’re partial checks of a single security invariant. Calling only one is incorrect:
// Looks harmless... but it isn't if ($session->checkExpiry()) { // Session may still be forged }
Teams have to rely on documentation and social convention to avoid this mistake. Code review can easily miss it. New developers won’t know the coupling. Static analysis can’t fully enforce it.
This turns internal implementation details into false public API surface, which can:
private(namespace) solves this cleanly: SessionStore can call internal APIs, other namespaces can’t. The correct usage becomes enforced, not implied.
The workarounds are all problematic:
Add private(namespace) visibility for methods and properties. Members with this visibility are accessible to all code within the same namespace but not to code in other namespaces.
namespace App\Auth; class SessionManager { private(namespace) static string $internalState; // Asymmetric visibility: publicly readable, namespace-writable public private(namespace)(set) int $sessionCount = 0; private(namespace) function checkExpiry(): bool { return time() < $this->internalState; } private(namespace) static function validateToken(string $token): bool { return hash_equals($token, self::$internalState); } } class SessionStore { public function refresh(SessionManager $session): void { // OK - same namespace $session->checkExpiry(); // OK - same namespace, can write to asymmetric property $session->sessionCount++; } }
namespace App\Controllers; use App\Auth\SessionManager; class LoginController { public function login(): void { $session = new SessionManager(); $session->checkExpiry(); // Fatal error: Uncaught Error: Call to private(namespace) method // App\Auth\SessionManager::checkExpiry() from scope App\Controllers } }
private(namespace) restricts access to code whose lexical namespace exactly matches the member’s declaring namespace. Applies to methods and properties (including static). Enforced for direct calls, callables (at creation or invocation as specified), property reads/writes, and property hooks as ordinary method calls. Traits use the receiver class’s namespace.
Inheritance behavior is similar to protected: child classes inherit private(namespace) members and must maintain signature compatibility when redeclaring them. However, visibility enforcement is based on the declaring namespace rather than the class hierarchy. You can’t transition between protected and private(namespace) as they operate on different axes (inheritance vs namespace).
Sub-namespaces are considered different namespaces:
namespace App\Auth; class Foo { private(namespace) function helper() { /* */ } } namespace App\Auth\OAuth; new Foo()->helper(); // Fatal error: Uncaught Error: Call to private(namespace) method // App\Auth\helper() from scope App\Auth\OAuth
private(namespace) function instanceMethod() {} private(namespace) static function staticMethod() {} private(namespace) string $instanceProperty; private(namespace) static string $staticProperty;
The write visibility mustn’t allow a broader set of callers than the read visibility. If it would, it’s a compile-time error.
Important: protected and private(namespace) operate on different axes (inheritance vs namespace) and can’t be combined in asymmetric visibility. These combinations are compile-time errors:
protected private(namespace)(set) - Error: protected operates on inheritance axis while private(namespace) on a namespace axisprivate(namespace) protected(set) - Error: same reasonValid asymmetric combinations include:
public private(namespace)(set) - Publicly readable, namespace-writableprivate(namespace) private(set) - Namespace-readable, class-writableprotected protected(set) - Inheritance-readable, inheritance-writable (already allowed)public protected(set) - Publicly readable, inheritance-writable (already allowed)Here’s an example of the error you’ll get when mixing axes:
namespace App\Auth; class SessionManager { // Error: Cannot mix inheritance and namespace axes protected private(namespace)(set) int $sessionCount = 0; // Fatal error: Property App\Auth\SessionManager::$sessionCount has incompatible visibility modifiers: // protected and private(namespace) operate on different axes (inheritance vs namespace) // and cannot be combined in asymmetric visibility }
The namespace of the calling code is compared to the namespace where the member was declared.
Namespace-private members can be accessed from top-level code within the same namespace:
namespace App\Auth; class Session { private(namespace) function foo() {} } // OK: Top-level code in the same namespace $s = new Session(); $s->foo();
namespace App\Other; $s = new \App\Auth\Session(); $s->foo(); // Fatal error: Uncaught Error: Call to private(namespace) method // App\Auth\Session::foo() from scope App\Other
The namespace context is tracked at compile-time.
private(namespace) members behave like protected members in terms of inheritance: They’re inherited by child classes, and child classes can redeclare them only if they follow compatibility rules. However, visibility is enforced based on the namespace where they were declared, not on the inheritance hierarchy:
namespace App\Auth; class Base { private(namespace) function helper() { echo "Base::helper\n"; } public function callHelper() { // Always works - method declared in App\Auth, called from App\Auth $this->helper(); } } class Child extends Base { public function test() { // OK - Child is in same namespace as declaration (App\Auth) $this->helper(); } } $child = new Child(); $child->callHelper(); // Works - parent method calls via $this
namespace App\Other; class Different extends \App\Auth\Base { public function test() { $this->helper(); // Fatal error: Uncaught Error: Call to private(namespace) method // App\Auth\Base::helper() from scope App\Other\Different } } $diff = new Different(); $diff->callHelper(); // Still works! Parent method in App\Auth can call it
Just like with protected members, redefining an inherited method with incompatible visibility is an error.
protected and private(namespace) operate on different axes and are fundamentally incompatible. You can’t transition between them in either direction:
private(namespace) to protectedprotected to private(namespace)
This prevents soundness violations where code in the declaring namespace could call a private(namespace) method, but if a child class changes it to protected, that call would fail at runtime.
You also can’t reduce visibility from public to private(namespace):
namespace App\Auth; class Base { public function helper() {} } class Child extends Base { private(namespace) function helper() {} // Fatal error: Access level to Child::helper() must be public (as in class Base) }
Here’s why transitioning between protected and private(namespace) would break type safety:
namespace App\Auth; class P { private(namespace) function x(): void { echo "P::x()\n"; } } class C extends P { // ERROR: Cannot change from private(namespace) to protected // If this were allowed, the following would break: protected function x(): void { echo "C::x()\n"; } // Fatal error: Access level to C::x() must be private(namespace) (as in class P) or weaker } function f(P $p): void { // This is legal because f() is in the same namespace as P // and P::x() is private(namespace) $p->x(); } // If C::x() were allowed to be protected, this would fail at runtime: // f(new C()); // Would error: Call to protected method C::x() from global scope
The reverse is also forbidden:
namespace App\Auth; class Base { protected function helper() {} } class Child extends Base { // ERROR: Cannot change from protected to private(namespace) private(namespace) function helper() {} // Fatal error: Access level to Child::helper() must be protected (as in class Base) or weaker }
This would break child classes in other namespaces that expect to access the method via protected inheritance.
Child classes in different namespaces can’t redeclare private(namespace) methods unless the signature is compatible (follows the same inheritance rules as protected methods):
namespace App\Auth; class Base { private(namespace) function helper(string $arg): void {} }
namespace App\Other; class Child extends \App\Auth\Base { // This is an error: incompatible signature private(namespace) function helper(int $arg): void {} // Fatal error: Declaration of App\Other\Child::helper(int $arg): void // must be compatible with App\Auth\Base::helper(string $arg): void }
The Liskov Substitution Principle applies to the public contract of a class. A subclass must be usable anywhere its parent is expected, using only the parent’s public API.
private(namespace) doesn't alter the public API at all. Substitutionally, for external callers, it is unchanged.
Important inheritance note: While private(namespace) members are inherited like protected members, a key difference is that visibility is checked against the declaring namespace rather than the calling class’s namespace. This means:
private(namespace) members.private(namespace) members.protected methods.
For callers in a different namespace, private(namespace) members aren’t accessible. Both parent and child present the same public surface:
namespace App\Auth; class SessionManager { private(namespace) function validateToken() { /* ... */ } public function login(): bool { return $this->validateToken(); } } class AdvancedSessionManager extends SessionManager { // public API unchanged } // in a different file: namespace App\Controllers; function authenticate(SessionManager $session): bool { // same public API for SessionManager and AdvancedSessionManager return $session->login(); }
Inside the declaring namespace, private(namespace) members are accessible and therefore part of an internal API. A subclass defined in a different namespace does not expose that same internal API.
This isn’t a violation of LSP, because LSP only requires substitutability via the public contract. The internal namespace-level API is intentionally not part of the public contract.
When a trait defines a private(namespace) member, the visibility is checked against the receiver class’s namespace, not the trait’s namespace. This is consistent with how traits work in PHP—they’re essentially copied into the class that uses them.
namespace App\Traits; trait SessionHelper { private(namespace) function validateToken() { return true; } }
namespace App\Auth; use App\Traits\SessionHelper; class SessionManager { use SessionHelper; public function login() { // OK - uses SessionManager's namespace (App\Auth) // NOT the trait's namespace (App\Traits) $this->validateToken(); } }
namespace App\Auth; class LoginController { public function test(SessionManager $session) { // OK - LoginController is in App\Auth (same as SessionManager) $session->validateToken(); } }
namespace App\Controllers; class OtherController { public function test(\App\Auth\SessionManager $session) { $session->validateToken(); // Fatal error: Uncaught Error: Call to private(namespace) method // App\Auth\SessionManager::validateToken() from scope App\Controllers\OtherController } }
In short:
Code executed via eval() can access private(namespace) members if it declares an explicit namespace that matches the target member’s namespace:
namespace App\Auth; class SessionManager { private(namespace) int $sessionCount = 0; private(namespace) function validateToken(): bool { return true; } } $session = new SessionManager(); // OK - eval code with explicit namespace declaration eval(' namespace App\Auth; $count = $session->sessionCount; echo "Count: $count\n"; $session->validateToken(); ');
When eval() is called without an explicit namespace declaration, the code runs in the global namespace and can’t access private(namespace) members:
namespace App\Auth; $session = new SessionManager(); // Error - eval code without namespace runs in global scope eval(' $count = $session->sessionCount; '); // Fatal error: Cannot access private(namespace) property // App\Auth\SessionManager::$sessionCount from scope {main}
Functions defined within eval() inherit the namespace context from the evaluated code:
namespace App\Auth; eval(' namespace App\Auth; function helperFunction(SessionManager $session): int { // OK - function defined in App\Auth namespace return $session->sessionCount * 2; } '); $result = \App\Auth\helperFunction($session);
private(namespace) methods can be used with all callable forms in PHP. Namespace visibility is checked using the lexical namespace where the callable is created or invoked.
call_user_func(), call_user_func_array(), and array_map() check namespace visibility using the caller’s lexical namespace:
namespace App\Auth; class SessionManager { private(namespace) function validateToken(): bool { return true; } }
namespace App\Auth; $session = new SessionManager(); // OK - call_user_func called from App\Auth namespace call_user_func([$session, 'validateToken']);
namespace App\Controllers; $session = new \App\Auth\SessionManager(); call_user_func([$session, 'validateToken']); // Fatal error: Uncaught TypeError: call_user_func(): Argument #1 ($callback) // must be a valid callback, cannot access private(namespace) method // App\Auth\SessionManager::validateToken()
First-class callable syntax ($fn = $obj->method(...)) checks namespace visibility at the creation site:
namespace App\Auth; $session = new SessionManager(); // OK - Callable created in App\Auth namespace $fn = $session->validateToken(...); $fn(); // Works
namespace App\Controllers; $session = new \App\Auth\SessionManager(); $fn = $session->validateToken(...); // Fatal error: Uncaught Error: Call to private(namespace) method // App\Auth\SessionManager::validateToken() from scope App\Controllers
Closure::fromCallable() follows the same rules as first-class callables - namespace visibility is checked at the conversion site:
namespace App\Auth; $session = new SessionManager(); // OK - Closure created in App\Auth namespace $closure = Closure::fromCallable([$session, 'validateToken']); $closure(); // Works
namespace App\Controllers; $session = new \App\Auth\SessionManager(); $closure = Closure::fromCallable([$session, 'validateToken']); // Fatal error: Uncaught Error: class@anonymous::__invoke(): Argument #1 ($callback) // must be a valid callback, can't access private(namespace) method // App\Auth\SessionManager::validateToken()
Direct invocation of callable variables (e.g., $callable()) checks namespace visibility at the invocation site, just like with other visibility modifiers:
namespace App\Auth; $session = new SessionManager(); $callable = [$session, 'validateToken']; // OK - Invoked from App\Auth namespace $callable();
namespace App\Controllers; $session = new \App\Auth\SessionManager(); $callable = [$session, 'validateToken']; $callable(); // Fatal error: Uncaught Error: Call to private(namespace) method // App\Auth\SessionManager::validateToken() from global scope
Reflection bypasses namespace visibility, consistent with how it handles private:
$method = new ReflectionMethod(SessionManager::class, 'checkExpiry'); $method->invoke($session); // Works regardless of caller namespace
New Reflection methods:
$method->isNamespacePrivate(): bool $property->isNamespacePrivate(): bool
Scoped visibility beyond class boundaries is standard in modern languages:
| Language | Syntax | Scope |
|---|---|---|
| C# | internal | Assembly |
| Kotlin | internal | Module |
| Swift | internal (default!) | Module |
| Rust | pub(crate) | Crate |
| Java | (no modifier) | Package |
PHP is an outlier in not having this feature.
None. This is a purely additive feature:
Next minor version (PHP 8.6 or 9.0)
This feature provides a new encapsulation mechanism that’ll benefit library and framework developers:
This is a purely additive feature—existing code continues to work unchanged:
public, protected, private) continue to work exactly as beforeprivate(namespace) in new code or when refactoringStatic analysis tools will need to understand the new visibility modifier:
private(namespace) visibility checksprivate(namespace) visibility checksprivate(namespace)
The implementation provides Reflection methods (ReflectionMethod::isNamespacePrivate(), ReflectionProperty::isNamespacePrivate()) that tools can use to inspect namespace visibility.
Symfony:
Laravel:
Doctrine:
Code that uses Reflection to introspect classes will see new private(namespace) members:
// This will now include private(namespace) methods $methods = (new ReflectionClass(SessionManager::class))->getMethods();
However, this is consistent with how Reflection already works—it shows all members regardless of visibility.
None
Extensions that interact with op_arrays directly will need to be aware of the new namespace_name field. Most extensions shouldn’t be affected.
Opcache’s persistence layer must handle the new namespace_name field in zend_op_array:
namespace_name string must be interned into shared memory, similar to function_name
The compiler needed modifications to prevent memory leaks when tokenizing code to track the current namespace to support private(namespace) and private(namespace)(set)
Runtime performance: Minimal impact. One additional flag check at method/property access time (already checking visibility flags).
Memory cost: 8 bytes per zend_op_array (64-bit systems) for the namespace_name field:
Opcache performance: Minor overhead for namespace_name string interning and refcount management during cache persistence. This occurs once per script during compilation, not on every request.
This RFC intentionally excludes:
class@anonymous... vs Foo\Bar\ClassName)private class Foo {}private(namespace) const X = 1;private(namespace) doesn't follow asymmetric visibility (AViz) semantics and is somewhat verbose. However, it clearly communicates the intent and follows the pattern established by other visibility modifiers.
Accept private(namespace) visibility for methods and properties? Yes/No (2/3 majority required)
Implementation: https://github.com/php/php-src/pull/20421