PHP RFC: Namespace-Scoped Visibility for Methods and Properties
- Version: 1.0
- Date: 2025-11-07
- Author: Rob Landers rob@bottled.codes
- Status: Draft
- First Published at: http://wiki.php.net/rfc/namespace_visibility
Introduction
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.
Motivation
The Problem
PHP currently has three visibility levels:
public: Accessible everywhereprotected: Accessible within class hierarchy (inheritance-based)private: Accessible only within the same class
There is no way to share code between classes in the same logical module (namespace) without making it public to the entire application.
Real-World Example
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:
- Make everything public - Pollutes API, no encapsulation
- Use protected + inheritance - Forces artificial inheritance hierarchies
- Put everything in one class - Creates god objects
- Use @internal docblock - Not enforced, easily violated
Proposal
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.
Syntax
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(); } }
Scoping Rules
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.
Inheritance Behavior
private(namespace) members are not inherited and do not participate in inheritance visibility rules:
namespace App\Auth; class Base { private(namespace) function helper() {} } class Child extends Base { public function test() { // OK - same namespace as declaration $this->helper(); } }
namespace App\Other; class Different extends \App\Auth\Base { public function test() { // Error - different namespace than declaration $this->helper(); } }
Redefining in Child Classes
Since private(namespace) methods are not inherited, child classes may define their own methods with the same name without conflict:
namespace App\Auth; class Base { private(namespace) function helper() { return "Base"; } } class Child extends Base { private(namespace) function helper() { return "Child"; } public function test() { $this->helper(); // Calls Child::helper() parent::helper(); // Calls Base::helper() (same namespace) $base = new Base(); $base->helper(); // Calls Base::helper() } }
However, redefining an inherited public or protected method as private(namespace) is an error:
class Base { public function helper() {} } class Child extends Base { private(namespace) function helper() {} // Error: Cannot reduce visibility }
Reflection
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
Precedent in Other Languages
Namespace/module/package-scoped visibility 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.
Backward Incompatible Changes
None. This is a purely additive feature:
- New syntax that was previously a parse error
- No changes to existing visibility behavior
- Fully opt-in
Proposed PHP Version
Next minor version (PHP 8.6 or 9.0)
RFC Impact
To SAPIs
None
To Existing Extensions
None
To Opcache
No changes needed. Visibility checks happen at call-site, not in opcache.
Performance
Minimal impact. One additional flag check at method/property access time (already checking visibility flags).
Future Scope
This RFC intentionally excludes:
- Class-level visibility:
private class Foo {}- This would require class resolution changes and is significantly more complex
- Can be proposed separately
- Constants:
private(namespace) const X = 1;- Constants have compile-time resolution complexity
- Protected(namespace): Combination of namespace and inheritance scoping
- Can be added later if desired
Open Issues
None at this time.
Proposed Voting Choices
Accept private(namespace) visibility for methods and properties? Yes/No (2/3 majority required)
Patches and Tests
Implementation: [Link to PR]
References
- C# internal modifier: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/internal
- Kotlin internal visibility: https://kotlinlang.org/docs/visibility-modifiers.html
- Rust visibility: https://doc.rust-lang.org/reference/visibility-and-privacy.html
Changelog
- 1.0: Initial version