[PHP-DEV] RFC: Namespace-Scoped Visibility for Methods and Properties

Hi,

Did we take into account prefixed namespaces.

private(namespace \Users\Auth\*) function test() {}

And then Auth folder has other classes and folders with child namespaces.

Sent from my iPhone

On Nov 9, 2025, at 2:56 PM, Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

On 11/9/25 20:41, Rob Landers wrote:
class P {
    private(namespace) function x() {}
}
class C extends P {
    protected function x() {}
}
This behaves the same as overriding a private method with a protected/public one today: the parent’s method is private to its declaring class, so the second example is allowed.

This is unsound. As we have established, neither `private(namespace)` nor `protected` is a subset of each other.

Specifically allowing this breaks the following (everything is declared in the same namespace):

   class P {
       private(namespace) function x() { }
   }
   class C extends P {
       protected function x() { }
   }

   function f(P $p) {
       $p->x(); // legal, because f is in the same namespace as P.
   }

   f(new C()); // breaks, because C::x() is protected and thus not legal to access from f / the global scope.

Best regards
Tim Düsterhus

Hi

On 11/9/25 16:33, Larry Garfield wrote:

In particular, the syntax is a hard-no. It runs contrary to how the aviz syntax was defined, as others have already noted.

If you don't want to use `internal(set)` to save `internal` for some other use, that's fine. Come up with a different keyword. :slight_smile: But <allowed scope>(<operation>) is the syntax model that aviz established, and tossing extra parens in there just confuses things for everyone.

I agree with that.

If it really does need to be an entirely separate dimension of scoping, then I would argue it's too complex to capture in just keywords and Rowan's attribute proposal becomes even more compelling.

I disagree that this should be an Attribute for the inverse of the reasons of why `#[\Override]` *is* an attribute: PHP: rfc:marking_overriden_methods

Visibility is part of the public API and by making this an attribute, the code will seemingly appear to work in older PHP version that do not yet know about the attribute, but its behavior will differ.

It would of course also be syntactically / visually inconsistent with any other visibility definition.

Best regards
Tim Düsterhus

On Sun, Nov 9, 2025, at 20:55, Tim Düsterhus wrote:

Hi

On 11/9/25 20:41, Rob Landers wrote:

class P {
private(namespace) function x() {}
}

class C extends P {
protected function x() {}
}

This behaves the same as overriding a private method with a protected/public one today: the parent’s method is private to its declaring class, so the second example is allowed.

This is unsound. As we have established, neither private(namespace)
nor protected is a subset of each other.

Specifically allowing this breaks the following (everything is declared
in the same namespace):

class P {
private(namespace) function x() { }
}
class C extends P {
protected function x() { }
}

function f(P $p) {
$p->x(); // legal, because f is in the same namespace as P.
}

f(new C()); // breaks, because C::x() is protected and thus not
legal to access from f / the global scope.

Best regards
Tim Düsterhus

Initially, I treated private(namespace) like private for cross-namespace inheritance and overlooked how dynamic dispatch actually works in PHP. This means that a same-named method in a child would simply be a new method. But like you point out, that leads to a subtle problem.

Even though P::x() is a method that’s visible and intended for callers inside namespace A, the runtime dispatch would pick C::x(). That completely and utterly destroys substitutability for namespace internal callers, which is exactly what this visibility is meant to protect.

So … it seems we need a couple of additional rules here:

  1. If a parent has a private(namespace) method, then a subclass in a different namespace simply cannot declare a method with the exact same name. It’d be a compile time error to prevent shadowing and keep dispatch predictable. I’d also be open to other ways as well (such as always calling the namespaced method in the parent), but I’d need to see if that’s even possible in the engine.

  2. As you pointed out, protected and private(namespace) aren’t compatible; thus we should only allow the same or a superset. So, it should only allow public or private(namespace) when inheriting in the same namespace.

— Rob

Hi

On 11/9/25 18:20, Rob Landers wrote:

What this RFC does is provide a useful, enforceable intermediate step that doesn’t require the community to solve “packages” first. If we require a full module system before adding any visibility improvements, we’re essentially saying that no incremental progress is acceptable until the hardest part of the problem is fully solved. That’s historically not how PHP evolves.
[…]
Namespace visibility is intentionally small, intentionally conservative, and intentionally compatible with whatever direction modules eventually take. It’s the smallest useful step the language can take today, without blocking future work, while still being useful.

As I believe I had previously said before, I'm in favor of small RFCs and features *that compose well*. I am therefore generally sympathetic towards making small incremental improvements with a bigger picture in mind. Features that compose well make the language easier to learn and understand. This was an important part of the decision why Volker and I ultimately decided to use an array parameter for clone-with. And also why it was important to me that the pipe operator and partial-function application are independently usable feature

For this one I am however not sure if it ticks the “composes well” checkbox - that greatly depends on the syntax choice and how modules will look like if/when they eventually arrive.

I also believe that the current usefulness is debatable, especially when considering how folks are already structuring their applications as of now (as indicated by Rowan) and when also considering the ecosystem impact.

Your RFC appears to use the old template for the “RFC Impact” section which doesn't yet include the “Ecosystem Impact” subsection, but indicating that “significant OPcache changes” are required makes me wonder about the cost-benefit ratio.

Aviz establishes `visibility(operation)` as the pattern for asymmetric visibility, where the keyword controls the caller set and parentheses restrict the operation (get/set). That’s why `private(namespace)(set)` follows the same rule: the base visibility is still "private", and the parentheses narrows who may call it.

If we introduced a standalone keyword like `internal` or `nsviz`, we’d effectively be adding a new visibility class, not a refinement of `private` and would bring its own semantics, collision issues, and interactions with any future module work. This RFC aims to minimise surface area, which is why it treats namespace visibility as a refinement.

As noted in my reply in the thread from Faizan, calling this a refinement of `private` is not really accurate / doesn't work in practice.

If the community prefers prefix-based visibility or package-level visibility, that could be explored in a follow-up RFC. I’m not opposed to more expressive forms; I’m just not binding this RFC to a package model the language hasn’t defined yet.

To do so, the syntax would need to account for that. I have not yet seen a good proposal for that that doesn't end up as “symbol soup” that doesn't really fit the existing language syntactically.

And lastly, isn’t namespace visibility easy to bypass?

Yes — but so is private and protected. No PHP visibility is intended as a security boundary. This is a developer-intent boundary: encapsulation, static analysis, and making accidental misuse harder.

I fully agree with this part. It's important that bypassing these type of control mechanisms is a fairly explicit act that stands out in review, but it's not important to lock it down 100% - unless there are good (engine-related) reasons to do so.

Best regards
Tim Düsterhus

On Sun, Nov 9, 2025, at 21:51, Tim Düsterhus wrote:

For this one I am however not sure if it ticks the “composes well”
checkbox - that greatly depends on the syntax choice and how modules
will look like if/when they eventually arrive.

I understand the concern. Composability matters a lot, especially for features that touch visibility. My goal with this RFC is to take a boundary PHP already has (the lexical namespace) and make it enforceable without needing to answer the bigger “what’s a module/package?” question first.

Right now, different people in the ecosystem use namespaces in different ways: some treat them as hierarchical, some as flat prefixes, some map them to directory trees, some don’t. Trying to define prefix rules, upward/downward access, or package-like confinement gets us right back into the same conversation we’ve been stuck on. That’s why this RFC deliberately picks the simplest rule PHP could enforce today: exact namespace equality.

If a future RFC defines modules/packages, namespace-visibility can either:

  • fold into that boundary,
  • be superseded by it, or
  • be used inside it (e.g. internal for modules, private(namespace) within module internals).

Nothing in this RFC makes that harder.

Your RFC appears to use the old template for the “RFC Impact” section
which doesn’t yet include the “Ecosystem Impact” subsection, but
indicating that “significant OPcache changes” are required makes me
wonder about the cost-benefit ratio.

Thanks! I’ll look at the new template and call out ecosystem impact (this was originally written back in April/May?). On the OPcache point: “significant” is probably overstating it. The change is limited to persisting one additional interned string on zend_op_array and refcounting it correctly. The cost is paid at compile time, not at call time, so runtime performance impact should be negligible. I’ll reword this to be more precise.

Aviz establishes visibility(operation) as the pattern for asymmetric visibility, where the keyword controls the caller set and parentheses restrict the operation (get/set). That’s why private(namespace)(set) follows the same rule: the base visibility is still “private”, and the parentheses narrows who may call it.

If we introduced a standalone keyword like internal or nsviz, we’d effectively be adding a new visibility class, not a refinement of private and would bring its own semantics, collision issues, and interactions with any future module work. This RFC aims to minimise surface area, which is why it treats namespace visibility as a refinement.

As noted in my reply in the thread from Faizan, calling this a
refinement of private is not really accurate / doesn’t work in practice.

Agreed. After the discussion with you, Alex, and Larray, I think it’s clearer to describe private(namespace) as a distinct caller-set, not a subset of protected or private. I’ll update the RFC text to reflect that and disallow weird combinations (to be more clearly defined in the RFC).

If the community prefers prefix-based visibility or package-level visibility, that could be explored in a follow-up RFC. I’m not opposed to more expressive forms; I’m just not binding this RFC to a package model the language hasn’t defined yet.

To do so, the syntax would need to account for that. I have not yet seen
a good proposal for that that doesn’t end up as “symbol soup” that
doesn’t really fit the existing language syntactically.

My earliest version simply used namespace:

class P {
  namespace function x() {} 
}

It might make sense to return to that syntax if people don’t like the current syntax. I don’t have a strong attachment to the exact spelling, what matters is the semantics.

— Rob

On Sun, Nov 9, 2025, at 22:32, Rob Landers wrote:

On Sun, Nov 9, 2025, at 21:51, Tim Düsterhus wrote:

For this one I am however not sure if it ticks the “composes well”
checkbox - that greatly depends on the syntax choice and how modules
will look like if/when they eventually arrive.

I understand the concern. Composability matters a lot, especially for features that touch visibility. My goal with this RFC is to take a boundary PHP already has (the lexical namespace) and make it enforceable without needing to answer the bigger “what’s a module/package?” question first.

Right now, different people in the ecosystem use namespaces in different ways: some treat them as hierarchical, some as flat prefixes, some map them to directory trees, some don’t. Trying to define prefix rules, upward/downward access, or package-like confinement gets us right back into the same conversation we’ve been stuck on. That’s why this RFC deliberately picks the simplest rule PHP could enforce today: exact namespace equality.

If a future RFC defines modules/packages, namespace-visibility can either:

  • fold into that boundary,
  • be superseded by it, or
  • be used inside it (e.g. internal for modules, private(namespace) within module internals).

Nothing in this RFC makes that harder.

Your RFC appears to use the old template for the “RFC Impact” section
which doesn’t yet include the “Ecosystem Impact” subsection, but
indicating that “significant OPcache changes” are required makes me
wonder about the cost-benefit ratio.

Thanks! I’ll look at the new template and call out ecosystem impact (this was originally written back in April/May?). On the OPcache point: “significant” is probably overstating it. The change is limited to persisting one additional interned string on zend_op_array and refcounting it correctly. The cost is paid at compile time, not at call time, so runtime performance impact should be negligible. I’ll reword this to be more precise.

Aviz establishes visibility(operation) as the pattern for asymmetric visibility, where the keyword controls the caller set and parentheses restrict the operation (get/set). That’s why private(namespace)(set) follows the same rule: the base visibility is still “private”, and the parentheses narrows who may call it.

If we introduced a standalone keyword like internal or nsviz, we’d effectively be adding a new visibility class, not a refinement of private and would bring its own semantics, collision issues, and interactions with any future module work. This RFC aims to minimise surface area, which is why it treats namespace visibility as a refinement.

As noted in my reply in the thread from Faizan, calling this a
refinement of private is not really accurate / doesn’t work in practice.

Agreed. After the discussion with you, Alex, and Larray, I think it’s clearer to describe private(namespace) as a distinct caller-set, not a subset of protected or private. I’ll update the RFC text to reflect that and disallow weird combinations (to be more clearly defined in the RFC).

If the community prefers prefix-based visibility or package-level visibility, that could be explored in a follow-up RFC. I’m not opposed to more expressive forms; I’m just not binding this RFC to a package model the language hasn’t defined yet.

To do so, the syntax would need to account for that. I have not yet seen
a good proposal for that that doesn’t end up as “symbol soup” that
doesn’t really fit the existing language syntactically.

My earliest version simply used namespace:

class P {
  namespace function x() {} 
}

It might make sense to return to that syntax if people don’t like the current syntax. I don’t have a strong attachment to the exact spelling, what matters is the semantics.

— Rob

I’ve updated the RFC and the implementation with some significant clarifications and corrections:

  • Inheritance semantics now follow protected rather than private
    private(namespace) members are inherited and must follow normal signature-compatibility rules.
    Visibility is enforced based on the declaring namespace rather than the inheritance hierarchy.

  • Incompatible redeclarations are now clearly defined
    Transitions between protected and private(namespace) are disallowed in either direction.
    This avoids unsound cases where substitutability would be broken for callers in the declaring namespace.

  • Asymmetric visibility rules clarified
    protected and private(namespace) operate on different axes (inheritance vs namespace), so mixed AViz like
    protected private(namespace)(set) is now a compile-time error.

  • Expanded examples and error messages
    The RFC now includes clearer examples of the invalid cases, inheritance rules, and AViz combinations.

  • Syntax moved to an explicit open issue
    Because the semantics now line up with protected rather than private, the spelling private(namespace) may not be ideal.
    I’ve listed this in the “Open Issues” section and I’ll include some previously considered alternatives that preserve the semantics here:

    • namespace function x() {}

    • local function x() {}

    • private:ns function x() {}

    • protected:ns function x() {}

My personal preference is toward the simpler forms (namespace or local), but I’d like to collect feedback before changing the RFC text.

Updated RFC:
https://wiki.php.net/rfc/namespace_visibility

Implementation:
https://github.com/php/php-src/pull/20421

Thanks to everyone who pointed out the inheritance edge cases; those surfaced issues that needed to be addressed. Further feedback is welcome.

— Rob

On Sat, 8 Nov 2025, Rowan Tommins [IMSoP] wrote:

To spell those out, the prefix version could look like this:

namespace Acme\AuthLib\Somewhere\Deep\In\Package;
// ...
#[NamespacePrivate('Acme\AuthLib', includeChildren: true)]

Whatever the way to define a prefix is going to be, I disagree with that
being done through attributes.

Nowhere in the language do these annotations (yet) interfere with
how code can be run.

cheers,
Derick

On 11/11/2025 11:36, Derick Rethans wrote:

On Sat, 8 Nov 2025, Rowan Tommins [IMSoP] wrote:

To spell those out, the prefix version could look like this:

namespace Acme\AuthLib\Somewhere\Deep\In\Package;
// ...
#[NamespacePrivate('Acme\AuthLib', includeChildren: true)]

Whatever the way to define a prefix is going to be, I disagree with that
being done through attributes.

Nowhere in the language do these annotations (yet) interfere with
how code can be run.

The main reason my mind went in that direction is that people are building attributes for this in userspace already - set the actual visibility to public, and then use an attribute for static analysis to restrict it.

They also have a standard syntax for arguments, so it's easy to illustrate those without spending time imagining a new syntax.

But in general, I agree, a native implementation would ideally look and feel like it matched the existing "private" keyword.

--
Rowan Tommins
[IMSoP]

On Mon, Nov 10, 2025, at 20:19, Rob Landers wrote:

On Sun, Nov 9, 2025, at 22:32, Rob Landers wrote:

On Sun, Nov 9, 2025, at 21:51, Tim Düsterhus wrote:

For this one I am however not sure if it ticks the “composes well”
checkbox - that greatly depends on the syntax choice and how modules
will look like if/when they eventually arrive.

I understand the concern. Composability matters a lot, especially for features that touch visibility. My goal with this RFC is to take a boundary PHP already has (the lexical namespace) and make it enforceable without needing to answer the bigger “what’s a module/package?” question first.

Right now, different people in the ecosystem use namespaces in different ways: some treat them as hierarchical, some as flat prefixes, some map them to directory trees, some don’t. Trying to define prefix rules, upward/downward access, or package-like confinement gets us right back into the same conversation we’ve been stuck on. That’s why this RFC deliberately picks the simplest rule PHP could enforce today: exact namespace equality.

If a future RFC defines modules/packages, namespace-visibility can either:

  • fold into that boundary,
  • be superseded by it, or
  • be used inside it (e.g. internal for modules, private(namespace) within module internals).

Nothing in this RFC makes that harder.

Your RFC appears to use the old template for the “RFC Impact” section
which doesn’t yet include the “Ecosystem Impact” subsection, but
indicating that “significant OPcache changes” are required makes me
wonder about the cost-benefit ratio.

Thanks! I’ll look at the new template and call out ecosystem impact (this was originally written back in April/May?). On the OPcache point: “significant” is probably overstating it. The change is limited to persisting one additional interned string on zend_op_array and refcounting it correctly. The cost is paid at compile time, not at call time, so runtime performance impact should be negligible. I’ll reword this to be more precise.

Aviz establishes visibility(operation) as the pattern for asymmetric visibility, where the keyword controls the caller set and parentheses restrict the operation (get/set). That’s why private(namespace)(set) follows the same rule: the base visibility is still “private”, and the parentheses narrows who may call it.

If we introduced a standalone keyword like internal or nsviz, we’d effectively be adding a new visibility class, not a refinement of private and would bring its own semantics, collision issues, and interactions with any future module work. This RFC aims to minimise surface area, which is why it treats namespace visibility as a refinement.

As noted in my reply in the thread from Faizan, calling this a
refinement of private is not really accurate / doesn’t work in practice.

Agreed. After the discussion with you, Alex, and Larray, I think it’s clearer to describe private(namespace) as a distinct caller-set, not a subset of protected or private. I’ll update the RFC text to reflect that and disallow weird combinations (to be more clearly defined in the RFC).

If the community prefers prefix-based visibility or package-level visibility, that could be explored in a follow-up RFC. I’m not opposed to more expressive forms; I’m just not binding this RFC to a package model the language hasn’t defined yet.

To do so, the syntax would need to account for that. I have not yet seen
a good proposal for that that doesn’t end up as “symbol soup” that
doesn’t really fit the existing language syntactically.

My earliest version simply used namespace:

class P {
  namespace function x() {} 
}

It might make sense to return to that syntax if people don’t like the current syntax. I don’t have a strong attachment to the exact spelling, what matters is the semantics.

— Rob

I’ve updated the RFC and the implementation with some significant clarifications and corrections:

  • Inheritance semantics now follow protected rather than private
    private(namespace) members are inherited and must follow normal signature-compatibility rules.
    Visibility is enforced based on the declaring namespace rather than the inheritance hierarchy.

  • Incompatible redeclarations are now clearly defined
    Transitions between protected and private(namespace) are disallowed in either direction.
    This avoids unsound cases where substitutability would be broken for callers in the declaring namespace.

  • Asymmetric visibility rules clarified
    protected and private(namespace) operate on different axes (inheritance vs namespace), so mixed AViz like
    protected private(namespace)(set) is now a compile-time error.

  • Expanded examples and error messages
    The RFC now includes clearer examples of the invalid cases, inheritance rules, and AViz combinations.

  • Syntax moved to an explicit open issue
    Because the semantics now line up with protected rather than private, the spelling private(namespace) may not be ideal.
    I’ve listed this in the “Open Issues” section and I’ll include some previously considered alternatives that preserve the semantics here:

    • namespace function x() {}

    • local function x() {}

    • private:ns function x() {}

    • protected:ns function x() {}

My personal preference is toward the simpler forms (namespace or local), but I’d like to collect feedback before changing the RFC text.

Updated RFC:
https://wiki.php.net/rfc/namespace_visibility

Implementation:
https://github.com/php/php-src/pull/20421

Thanks to everyone who pointed out the inheritance edge cases; those surfaced issues that needed to be addressed. Further feedback is welcome.

— Rob

On my commute, I was exploring the syntax further and the feedback I’ve gotten so far. Specifically:

class A {
  local function x() {}
}

This is accssible only with the exact same namespace where it is declared (this RFC). I was reminded that this is very similar to “file private” but allows the boundary to extend across multiple files in the same namespace.

Then, we could have this in a followup RFC (which I’ve already started drafting) that doesn’t actually require local but provides a broader “package-like” scope without requiring PHP to define what a package is.

class A {
  namespace function x() {}
}

This would be accessible within the namespace it is declared, as well as parent and child namespaces (thus we don’t make any assumptions about hierarchy).

Using the following namespace structure:

App\
├── Auth\
│   ├── SessionManager (declares namespace function validateToken())
│   ├── OAuth\
│   │   └── OAuthProvider
│   └── Session\
│       └── SessionStore
├── Billing\
│   └── PaymentProcessor
└── Controllers\
    └── LoginController

The following can access validateToken() :

  • App\Auth* (So, SessionStore/OAuthProvider)
  • App* (parent namespace)

But the following cannot access it:

  • App\Billing*
  • App\Controllers*
  • \ (global namespace)

The global namespace is a special case where namespace and local behave exactly the same.

Before rewriting the RFC around this, I’d like to guage whether people feel like the keyword based approach is a clearer direction than private(namespace).

— Rob

Hi

On 11/10/25 20:19, Rob Landers wrote:

Updated RFC:
PHP: rfc:namespace_visibility;

From the RFC I'm seeing that it's legal to redefine methods in different namespaces? I've only figured that out with the last example in the “Preventing Visibility Reduction and Incompatible Redeclaration” section, it might have been hinted at with “However, visibility is enforced based on the namespace where they were declared, not on the inheritance hierarchy:” but if it did, I didn't understand it.

I also don't believe it's useful to allow redefining namespace-scoped methods from a different namespace, since they would be *severely* restricted and also confusing. Consider:

     namespace Foo {
         class P {
             private(namespace) function x() { }
         }
     }

     namespace Bar {
         class C extends Foo\P {
             private(namespace) function x() {
                 parent::x(); // illegal
             }
         }

         $c = new C();
         $c->x(); // also illegal, despite C being in the current namespace, so everything should be accessible.
     }

I stay with my previous note that cross-namespace inheritance becomes unsound (and confusing) once namespace-scoped symbols are involved.

I feel that namespace-scoping needs to be restricted to top-level symbols to be easy to reason about. This also brings me to the next point: What about free-standing functions and constants? public/protected/private doesn't make sense for them, since they are not part of a inheritance hierarchy. But namespace-scope would be meaningful.

All in all, I feel that file-private symbols would solve many of the same problems, but be much easier to reason about. The SessionManager from your example could just be file-private.

Best regards
Tim Düsterhus

On Sun, Nov 23, 2025, at 17:00, Tim Düsterhus wrote:

Hi

On 11/10/25 20:19, Rob Landers wrote:

Updated RFC:
https://wiki.php.net/rfc/namespace_visibility <https://wiki.php.net/rfc/namespace_visibility?utm_source=chatgpt.com>

From the RFC I’m seeing that it’s legal to redefine methods in
different namespaces? I’ve only figured that out with the last example
in the “Preventing Visibility Reduction and Incompatible Redeclaration”
section, it might have been hinted at with “However, visibility is
enforced based on the namespace where they were declared, not on the
inheritance hierarchy:” but if it did, I didn’t understand it.

No, it should be illegal, but it looks like the current RFC text doesn’t say that. Basically, private(namespace) is incompatible with private(namespace) if the original namespace doesn’t match the current namespace. I’ll update the RFC later this week.

I feel that namespace-scoping needs to be restricted to top-level
symbols to be easy to reason about. This also brings me to the next
point: What about free-standing functions and constants?
public/protected/private doesn’t make sense for them, since they are not
part of a inheritance hierarchy. But namespace-scope would be meaningful.

That would need https://github.com/php/php-src/pull/20490 (subtle wink emoji)

All in all, I feel that file-private symbols would solve many of the
same problems, but be much easier to reason about. The SessionManager
from your example could just be file-private.

You can use private(namespace) as effectively file-private, just use a namespace in a single file. And, if you need to refactor, you don’t need to be constrained to a single file.

— Rob

Hi

Am 2025-11-23 20:25, schrieb Rob Landers:

All in all, I feel that file-private symbols would solve many of the
same problems, but be much easier to reason about. The SessionManager
from your example could just be file-private.

You can use private(namespace) as effectively file-private, just use a namespace in a single file. And, if you need to refactor, you don’t need to be constrained to a single file.

The value of file-private for me is avoiding *naming collisions* for “helper functionality”, not restricting the API. For this purpose single-file namespace with namespace-private symbols would provide no value over just using a “hard to guess” class or function name, since in either case “external” code could also define symbols in that namespace.

Best regards
Tim Düsterhus