====== PHP RFC: Namespace Prefix-Based Visibility for private(namespace) ======
* Version: 1.0
* Date: 2025-11-09
* Author: Rob Landers
* Status: Draft
* First Published at: http://wiki.php.net/rfc/namespace_prefix_visibility
* Requires: RFC Namespace-Scoped Visibility (http://wiki.php.net/rfc/namespace_visibility)
===== Introduction =====
This RFC proposes extending the ''%%private(namespace)%%'' visibility modifier to allow access from all namespaces that share a matching prefix with the declaring namespace. Under this proposal, a member declared in ''%%App\Auth\OAuth%%'' would be accessible from ''%%App%%'', ''%%App\Auth%%'', ''%%App\Auth\OAuth%%'', ''%%App\Auth\Session%%'', and any other namespace beginning with ''%%App\Auth\%%''.
This is a **relaxation** of the exact namespace matching rule introduced in the base ''%%private(namespace)%%'' RFC, enabling broader collaboration within namespace hierarchies while maintaining encapsulation boundaries.
===== Motivation =====
==== The Problem with Exact Matching ====
The original ''%%private(namespace)%%'' RFC requires an **exact namespace match** for access:
namespace App\Auth;
class SessionManager {
private(namespace) function validateToken(): bool {
return true;
}
}
namespace App\Auth\OAuth;
class OAuthProvider {
public function login(SessionManager $session): void {
// Fatal error: Cannot access private(namespace) method
// App\Auth\SessionManager::validateToken() from scope App\Auth\OAuth
$session->validateToken();
}
}
While this provides strong encapsulation, it creates artificial barriers within logically related code:
- **Fragmented module boundaries**: Sub-namespaces like ''%%App\Auth\OAuth%%'' and ''%%App\Auth\Session%%'' are conceptually part of the same authentication module, but cannot share internal APIs
- **Forced public exposure**: Developers must make methods ''%%public%%'' to share them across sub-namespaces, polluting the public API
- **Organizational inflexibility**: Teams must either flatten their namespace structure (losing organization) or accept weaker encapsulation
- **Inconsistent with package semantics**: In most languages (Java, C#, Kotlin), package-private visibility applies to the package and all sub-packages
==== Real-World Example ====
Consider an authentication module with multiple sub-components:
namespace App\Auth;
class AuthManager {
private(namespace) function createSession(User $user): Session {
// Core session creation logic
return new Session($user);
}
}
namespace App\Auth\OAuth;
class OAuthProvider {
public function authenticate(string $token): Session {
$user = $this->fetchUserFromToken($token);
$auth = new \App\Auth\AuthManager();
// ERROR: Cannot access createSession() - different namespace
return $auth->createSession($user);
}
}
namespace App\Auth\Session;
class SessionStore {
public function refresh(Session $session): void {
$auth = new \App\Auth\AuthManager();
// ERROR: Cannot access createSession() - different namespace
return $auth->createSession($session->getUser());
}
}
The workarounds are all problematic:
- **Flatten namespaces**: Put everything in ''%%App\Auth%%'' - loses organization
- **Make methods public**: Exposes internal APIs to the entire application
- **Use ''%%protected%%'' + inheritance**: Forces artificial class hierarchies
- **Current exact-match ''%%private(namespace)%%''**: Forces code duplication or architectural compromises
===== Proposal =====
Extend ''%%private(namespace)%%'' to allow access from **all namespaces that share a matching prefix** with the declaring namespace.
==== Scoping Rules ====
**New Rule: Prefix matching instead of exact matching**
A ''%%private(namespace)%%'' member declared in namespace ''%%N%%'' is accessible from:
- The exact namespace ''%%N%%''
- Any parent namespace of ''%%N%%'' (moving up the hierarchy)
- Any child namespace of ''%%N%%'' (moving down the hierarchy)
- Any sibling namespace at any level below ''%%N%%''
In formal terms, namespace ''%%C%%'' can access a member declared in namespace ''%%D%%'' if and only if:
* ''%%C%%'' starts with ''%%D\%%'', OR
* ''%%D%%'' starts with ''%%C\%%'', OR
* ''%%C == D%%''
This creates a **namespace tree** where all descendants of a namespace can access each other's ''%%private(namespace)%%'' members.
==== Examples ====
namespace App\Auth;
class SessionManager {
private(namespace) function validateToken(): bool {
return true;
}
}
**Accessible from same namespace (unchanged):**
namespace App\Auth;
class SessionStore {
public function check(SessionManager $session): void {
$session->validateToken(); // ✓ OK - exact match
}
}
**Accessible from child namespaces (NEW):**
namespace App\Auth\OAuth;
class OAuthProvider {
public function login(SessionManager $session): void {
$session->validateToken(); // ✓ OK - App\Auth\OAuth starts with App\Auth\
}
}
namespace App\Auth\Session\Storage;
class DatabaseStorage {
public function validate(SessionManager $session): void {
$session->validateToken(); // ✓ OK - App\Auth\Session\Storage starts with App\Auth\
}
}
**Accessible from parent namespaces (NEW):**
namespace App\Auth\OAuth;
class OAuthSession {
private(namespace) function refreshToken(): void { /* ... */ }
}
namespace App\Auth;
class AuthManager {
public function refresh(OAuthSession $session): void {
$session->refreshToken(); // ✓ OK - App\Auth is parent of App\Auth\OAuth
}
}
namespace App;
class Bootstrap {
public function init(OAuthSession $session): void {
$session->refreshToken(); // ✓ OK - App is parent of App\Auth\OAuth
}
}
**Not accessible from unrelated namespaces (unchanged):**
namespace App\Billing;
class PaymentProcessor {
public function process(SessionManager $session): void {
$session->validateToken();
// ✗ Fatal error: Cannot access private(namespace) method
// App\Auth\SessionManager::validateToken() from scope App\Billing
}
}
namespace Other;
class ExternalCode {
public function test(SessionManager $session): void {
$session->validateToken();
// ✗ Fatal error: Cannot access private(namespace) method
// App\Auth\SessionManager::validateToken() from scope Other
}
}
==== Visual Representation ====
Consider this namespace hierarchy:
App\
├── Auth\
│ ├── SessionManager (declares private(namespace) validateToken())
│ ├── OAuth\
│ │ └── OAuthProvider
│ └── Session\
│ └── SessionStore
├── Billing\
│ └── PaymentProcessor
└── Controllers\
└── LoginController
With **exact matching** (original RFC):
* ✓ ''%%App\Auth\*%%'' can access ''%%validateToken()%%''
* ✗ ''%%App\Auth\OAuth\*%%'' cannot access
* ✗ ''%%App\Auth\Session\*%%'' cannot access
* ✗ ''%%App\Billing\*%%'' cannot access
With **prefix matching** (this RFC):
* ✓ ''%%App\Auth\*%%'' can access ''%%validateToken()%%''
* ✓ ''%%App\Auth\OAuth\*%%'' **can access** (child namespace)
* ✓ ''%%App\Auth\Session\*%%'' **can access** (child namespace)
* ✓ ''%%App\*%%'' **can access** (parent namespace)
* ✗ ''%%App\Billing\*%%'' cannot access (different branch)
* ✗ ''%%App\Controllers\*%%'' cannot access (different branch)
==== Algorithm ====
The namespace visibility check becomes:
bool is_namespace_accessible(const char *caller_ns, const char *member_ns) {
if (caller_ns == NULL || member_ns == NULL) {
return false;
}
size_t caller_len = strlen(caller_ns);
size_t member_len = strlen(member_ns);
// Exact match
if (caller_len == member_len && strcmp(caller_ns, member_ns) == 0) {
return true;
}
// Caller is child of member namespace (member_ns is prefix of caller_ns)
// e.g., caller="App\Auth\OAuth", member="App\Auth"
if (caller_len > member_len &&
strncmp(caller_ns, member_ns, member_len) == 0 &&
caller_ns[member_len] == '\\') {
return true;
}
// Caller is parent of member namespace (caller_ns is prefix of member_ns)
// e.g., caller="App\Auth", member="App\Auth\OAuth"
if (member_len > caller_len &&
strncmp(member_ns, caller_ns, caller_len) == 0 &&
member_ns[caller_len] == '\\') {
return true;
}
return false;
}
==== Edge Cases ====
**Global namespace:**
namespace App\Auth;
class Session {
private(namespace) function foo() {}
}
namespace {
$s = new \App\Auth\Session();
$s->foo();
// ✗ Fatal error: Global namespace does not match App\Auth
}
The global namespace (empty string) does not match any namespaced code. This prevents accidental global access.
**Single-level namespace:**
namespace App;
class Base {
private(namespace) function helper() {}
}
namespace App\Auth;
class Child extends Base {
public function test() {
$this->helper(); // ✓ OK - App\Auth starts with App\
}
}
**Namespace separator edge case:**
namespace App;
class Foo {
private(namespace) function test() {}
}
namespace Application;
class Bar {
public function run(Foo $foo) {
$foo->test();
// ✗ Fatal error: "Application" does not start with "App\"
}
}
The check requires a full namespace segment match - ''%%Application%%'' is not treated as a child of ''%%App%%''.
===== Impact on Original RFC Semantics =====
==== Unchanged Behaviors ====
The following behaviors from the original RFC remain **identical**:
- **Asymmetric visibility**: All rules for ''%%private(namespace)(set)%%'' remain the same
- **Inheritance**: Members are inherited, visibility is based on declaring namespace
- **Visibility reduction**: Cannot reduce visibility of inherited ''%%public%%''/''%%protected%%'' members
- **Traits**: Trait members use the receiver class's namespace
- **Reflection**: Reflection still bypasses namespace visibility
- **Closures and callables**: All callable semantics remain unchanged
- **eval()**: Namespace context rules remain the same
==== Changed Behavior ====
**Only one change**: The namespace matching algorithm.
**Before (exact match):**
caller_namespace == member_namespace
**After (prefix match):**
caller_namespace == member_namespace
|| caller_namespace starts with member_namespace\
|| member_namespace starts with caller_namespace\
===== Liskov Substitution Principle (LSP) Considerations =====
The LSP analysis from the original RFC still applies, with one important extension:
**Original RFC**: A child class in a different namespace breaks LSP for namespace-internal operations.
**This RFC**: A child class in a different **namespace branch** breaks LSP for namespace-internal operations.
namespace App\Auth;
class SessionManager {
private(namespace) function validateToken() { /* ... */ }
}
class SessionStore {
public function check(SessionManager $session): bool {
return $session->validateToken(); // Expects access
}
}
namespace App\Auth\OAuth;
// ✓ OK - LSP preserved (same namespace tree)
class OAuthSession extends SessionManager {}
$store = new SessionStore();
$store->check(new OAuthSession()); // Works!
namespace App\Billing;
// ✗ Breaks LSP (different namespace branch)
class BillingSession extends SessionManager {}
$store = new SessionStore();
$store->check(new BillingSession());
// Fatal error: Cannot access private(namespace) method
**Design rationale**: Classes within the same namespace tree (e.g., ''%%App\Auth\*%%'') form a cohesive module. Classes outside that tree (e.g., ''%%App\Billing\*%%'') are external and should not be substitutable for namespace-internal operations.
===== Comparison with Other Languages =====
==== Java (package-private) ====
Java's package-private visibility (no modifier) allows access within the same package **and sub-packages**:
package com.app.auth;
class SessionManager {
void validateToken() {} // package-private
}
package com.app.auth.oauth;
class OAuthProvider {
void login() {
SessionManager session = new SessionManager();
session.validateToken(); // ✓ OK - same package tree
}
}
This RFC brings PHP's ''%%private(namespace)%%'' closer to Java's semantics.
==== C# (internal) ====
C# ''%%internal%%'' is assembly-scoped, but nested namespaces within an assembly can access ''%%internal%%'' members:
namespace App.Auth {
internal class SessionManager {
internal void ValidateToken() {}
}
}
namespace App.Auth.OAuth {
class OAuthProvider {
void Login() {
var session = new SessionManager();
session.ValidateToken(); // ✓ OK - same assembly
}
}
}
C# doesn't restrict by namespace within an assembly - all namespaces in the assembly can access ''%%internal%%'' members.
==== Rust (pub(crate) and pub(super)) ====
Rust has fine-grained visibility control:
mod app {
mod auth {
pub(super) fn validate_token() {} // Accessible to parent (app)
pub(crate) fn other_function() {} // Accessible to entire crate
mod oauth {
pub fn login() {
super::validate_token(); // ✓ OK - can access pub(super)
}
}
}
}
This RFC's prefix matching is similar to Rust's ''%%pub(in path)%%'' where ''%%path%%'' is the declaring namespace.
==== Summary ====
^Language ^Package/Module Hierarchy ^Access Scope ^
|Java |''%%com.app.auth.oauth%%''|Package + sub-packages (prefix match) |
|C# |''%%App.Auth.OAuth%%'' |Entire assembly (no namespace restriction) |
|Kotlin |Module-scoped |Entire module (no namespace restriction) |
|Rust |''%%app::auth::oauth%%'' |Configurable (''%%pub(super)%%'', ''%%pub(crate)%%'', ''%%pub(in path)%%'')|
|**PHP (this RFC)**|''%%App\Auth\OAuth%%'' |**Namespace tree (prefix match)** |
PHP with this RFC would be **most similar to Java**, with namespace hierarchies defining access boundaries.
===== Backward Compatibility =====
This RFC is **not** backward compatible with the original ''%%private(namespace)%%'' RFC if both are adopted.
==== Breaking Change ====
Code that relied on exact namespace matching for security boundaries will now allow broader access:
namespace App\Auth;
class CriticalSecurity {
private(namespace) function destroyAllSessions(): void {
// DANGEROUS OPERATION
}
}
namespace App\Auth\Public;
// Under original RFC: Cannot access (different namespace)
// Under this RFC: CAN access (child namespace)
class PublicController {
public function logout(CriticalSecurity $security): void {
$security->destroyAllSessions(); // Now allowed!
}
}
==== Migration Path ====
If the original ''%%private(namespace)%%'' RFC ships first, this RFC would need to either:
- **Replace the behavior entirely** (breaking change, requires major version)
- **Add a new syntax** (e.g., ''%%private(namespace:tree)%%'' vs ''%%private(namespace:exact)%%'')
- **Use an INI setting** to control matching behavior (not recommended)
===== Proposed PHP Version =====
* PHP 8.6, or 9.0
===== RFC Impact =====
All impacts are identical to the original ''%%private(namespace)%%'' RFC, as the implementation change is limited to the namespace matching function.
==== To Core ====
The only change needed is in ''%%zend_check_private_namespace_access()%%'' (or equivalent):
// Before (exact match)
if (strcmp(caller_ns, member_ns) == 0) {
return true;
}
// After (prefix match)
if (is_namespace_accessible(caller_ns, member_ns)) {
return true;
}
==== Performance ====
**Negligible impact.** The prefix matching adds two additional string comparisons in the worst case:
- Exact match check (fast path)
- Child namespace check (one ''%%strncmp%%'')
- Parent namespace check (one ''%%strncmp%%'')
Total overhead: ~10-20 CPU cycles per visibility check (measured in nanoseconds on modern CPUs).
===== Alternatives Considered =====
==== Alternative 1: Keep Exact Matching (Status Quo) ====
**Pros:**
* Simpler semantics
* Stronger encapsulation guarantees
* No ambiguity about access boundaries
**Cons:**
* Forces flat namespace structures or public APIs
* Inconsistent with most other languages
* Less useful in practice for large codebases
==== Alternative 2: New Syntax for Prefix Matching ====
Add a separate modifier for prefix matching:
private(namespace:tree) function foo() {} // Prefix match
private(namespace:exact) function bar() {} // Exact match (explicit)
private(namespace) function baz() {} // Defaults to... tree or exact?
**Pros:**
* Allows both behaviors to coexist
* Explicit about intent
**Cons:**
* More complex syntax
* Two similar features increase cognitive load
* Default behavior is ambiguous
==== Alternative 3: Configurable Namespace Depth ====
Allow specifying how many namespace levels to include:
private(namespace:1) function foo() {} // Current namespace + 1 level
private(namespace:*) function bar() {} // All levels (prefix match)
**Pros:**
* Fine-grained control
**Cons:**
* Overly complex
* Unclear use cases for partial depth
* Hard to understand at a glance
==== Recommendation ====
**Adopt prefix matching as the default behavior** for ''%%private(namespace)%%''. This provides the best balance of:
* **Simplicity**: One clear rule (prefix match)
* **Utility**: Supports real-world namespace organization
* **Consistency**: Aligns with Java's package-private semantics
===== Open Issues =====
==== Issue 1: Should parent namespaces have access? ====
This RFC proposes that parent namespaces **can** access child namespace members:
namespace App\Auth\OAuth;
class OAuthSession {
private(namespace) function refresh() {}
}
namespace App\Auth;
class Manager {
public function test(OAuthSession $session) {
$session->refresh(); // Should this work?
}
}
**Arguments for allowing:**
* Consistent with "namespace tree" semantics
* Parent namespaces are more general, so they conceptually "own" child namespaces
* Aligns with Rust's ''%%pub(super)%%'' + ''%%pub(crate)%%'' combined behavior
**Arguments against:**
* May allow overly broad access (parent can reach into child internals)
* Child namespaces are often more specialized, so parents shouldn't need their internals
**Current proposal**: Allow parent access (as specified above). This can be revisited during discussion.
==== Issue 2: Global namespace edge case ====
Should code in the global namespace have access to ''%%private(namespace)%%'' members in namespaced code if they're in the " same file"?
// file.php
namespace App\Auth;
class Session {
private(namespace) function foo() {}
}
namespace {
$s = new \App\Auth\Session();
$s->foo(); // Should this work?
}
**Current proposal**: No - global namespace does not match any namespaced code. This is consistent and prevents accidental global access.
===== Future Scope =====
==== Potential Extensions ====
- **''%%private(namespace:exact)%%''**: Add explicit exact-match syntax if both behaviors are desired
- **''%%protected(namespace)%%''**: Combine namespace and inheritance scoping
- **Namespace visibility for constants**: Extend to ''%%const%%'' declarations
- **Namespace visibility for classes**: ''%%private(namespace) class Foo {}%%''
===== Proposed Voting Choices =====
Change ''%%private(namespace)%%'' visibility to use prefix matching instead of exact matching? Yes/No (2/3 majority required)
===== Implementation Considerations =====
The implementation requires minimal changes to the existing ''%%private(namespace)%%'' implementation:
- **Modify namespace matching function** in ''%%zend_API.c%%'' or wherever visibility checks occur
- **Update error messages** to reflect "namespace tree" instead of "exact namespace"
- **Update tests** to cover prefix matching scenarios
Example implementation of the matching function is provided in the "Algorithm" section above.
===== References =====
* Original RFC: Namespace-Scoped Visibility (http://wiki.php.net/rfc/namespace_visibility)
* Java package-private access: https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html
* Rust visibility: https://doc.rust-lang.org/reference/visibility-and-privacy.html
* C# internal modifier: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/internal
===== Changelog =====
* 1.0: Initial version