====== 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