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 is 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()) { // ... } } }
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) 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(); // Error: Cannot access namespace-private method $session->checkExpiry(); } }
Rule 1: Exact namespace match required
Sub-namespaces are considered different namespaces:
namespace App\Auth; private(namespace) function helper() {} namespace App\Auth\OAuth; helper(); // Error: Different namespace
Rule 2: Works with static and instance members
private(namespace) function instanceMethod() {} private(namespace) static function staticMethod() {} private(namespace) string $instanceProperty; private(namespace) static string $staticProperty;
Rule 3: Asymmetric visibility for properties
private(namespace) can be used as a set visibility modifier, following PHP’s asymmetric visibility pattern:
// Valid: Get is less restrictive than set public private(namespace)(set) string $prop; protected private(namespace)(set) string $prop; // Invalid: Set is less restrictive than get private(namespace) public(set) string $prop; // Compile error private(namespace) protected(set) string $prop; // Compile error
Visibility hierarchy:
public < protected < private(namespace) < private (least restrictive → most restrictive)
Rule 4: Visibility checked at call-site
The namespace of the calling code is compared to the namespace where the member was declared.
Rule 5: Works from top-level code
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; // Error: Different namespace $s = new \App\Auth\Session(); $s->foo();
The namespace context is tracked at compile-time.
private(namespace) members are inherited (like private members), but visibility is enforced based on the namespace where they were declared:
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() { // Error - Different is in App\Other, but method was declared in App\Auth $this->helper(); } } $diff = new Different(); $diff->callHelper(); // Still works! Parent method in App\Auth can call it
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without breaking the application. How does private(namespace) affect LSP?
Short answer: private(namespace) does NOT violate LSP because it’s not part of the public contract.
From outside the namespace, private(namespace) members are invisible. The public API remains unchanged, so LSP is preserved:
namespace App\Auth; class SessionManager { private(namespace) function validateToken() { /* ... */ } public function login(): bool { return $this->validateToken(); } } class AdvancedSessionManager extends SessionManager { // Public API unchanged - fully substitutable }
namespace App\Controllers; function authenticate(SessionManager $session): bool { // LSP satisfied - AdvancedSessionManager works identically here // Only public methods are accessible, private(namespace) is hidden return $session->login(); } $basic = new SessionManager(); $advanced = new AdvancedSessionManager(); authenticate($basic); // Works authenticate($advanced); // Works identically - LSP preserved
Within the namespace, private(namespace) members ARE part of the contract. A child class in a different namespace is intentionally NOT substitutable for namespace-internal operations:
namespace App\Auth; class SessionManager { private(namespace) function validateToken() { /* ... */ } } class SessionStore { public function check(SessionManager $session): bool { // Works with SessionManager return $session->validateToken(); } }
namespace App\Other; class CustomSession extends \App\Auth\SessionManager { // Inherits validateToken() but in different namespace }
namespace App\Auth; $custom = new \App\Other\CustomSession(); $store = new SessionStore(); // Breaks LSP within namespace - but this is INTENTIONAL $store->check($custom); // Error: Cannot access namespace-private method
Design rationale: This is intentional! The child class is in a different namespace, so it’s outside the namespace boundary. The namespace API contract only applies to classes within the same namespace. This enforces encapsulation at the namespace level.
private(namespace) members should remain in the same namespace to maintain substitutability within that namespace.
Bottom line: private(namespace) preserves LSP for public APIs while allowing controlled LSP violations within namespace boundaries. This is analogous to package-private access in Java or internal visibility in C#/Kotlin.
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 are 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) { // Error - OtherController is in App\Controllers // (different from SessionManager's namespace App\Auth) $session->validateToken(); } }
In short:
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)
None
None
No changes needed. Visibility checks happen at call-site, not in opcache.
Minimal impact. One additional flag check at method/property access time (already checking visibility flags).
This RFC intentionally excludes:
private class Foo {}private(namespace) const X = 1;None at this time.
Accept private(namespace) visibility for methods and properties? Yes/No (2/3 majority required)
Implementation: https://github.com/php/php-src/pull/20421