rfc:namespace_visibility

PHP RFC: Namespace-Scoped Visibility for Methods and Properties

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 everywhere
  • protected: 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:

  1. Make everything public - Pollutes API, no encapsulation
  2. Use protected + inheritance - Forces artificial inheritance hierarchies
  3. Put everything in one class - Creates god objects
  4. 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:

LanguageSyntax 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

Changelog

  • 1.0: Initial version
rfc/namespace_visibility.txt · Last modified: by withinboredom