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

Hello Internals,

I’d like to introduce an RFC for discussion: https://wiki.php.net/rfc/namespace_visibility which proposes a new visibility modifier: private(namespace).

This idea has appeared several times in previous threads but never progressed to a formal proposal (from what I could find). My hope is that with defined semantics, examples, and implementation details, we can evaluate it properly and see whether there’s support for moving forward. Feedback is very welcome.

Sincerely,

Rob Landers

On Sat, 8 Nov 2025 at 12:44, Rob Landers <rob@bottled.codes> wrote:

Hello Internals,

I’d like to introduce an RFC for discussion: PHP: rfc:namespace_visibility which proposes a new visibility modifier: private(namespace).

This idea has appeared several times in previous threads but never progressed to a formal proposal (from what I could find). My hope is that with defined semantics, examples, and implementation details, we can evaluate it properly and see whether there’s support for moving forward. Feedback is very welcome.

Sincerely,

Rob Landers

I like the idea except for the name. Why not use the internal keyword
like C# does?

On Sat, Nov 8, 2025, at 14:30, Kamil Tekiela wrote:

On Sat, 8 Nov 2025 at 12:44, Rob Landers <rob@bottled.codes> wrote:

Hello Internals,

I’d like to introduce an RFC for discussion: https://wiki.php.net/rfc/namespace_visibility which proposes a new visibility modifier: private(namespace).

This idea has appeared several times in previous threads but never progressed to a formal proposal (from what I could find). My hope is that with defined semantics, examples, and implementation details, we can evaluate it properly and see whether there’s support for moving forward. Feedback is very welcome.

Sincerely,

Rob Landers

I like the idea except for the name. Why not use the internal keyword
like C# does?

Hi, thanks for the feedback.

The main reason I didn’t choose “internal” is that it implies some broader boundary than PHP currently defines. In languages like C#/Kotlin/Swift, “internal” is tied to a module or assembly, not a namespace. PHP doesn’t have a formal module/package boundary today, and I really prefer not to turn this RFC into a discussion about defining one.

private(namespace) describes exactly what it does: it widens private to code in the same namespace and nothing beyond that. It doesn’t imply any other boundary or hierarchy. It’s also consistent with asymmetric visibility (public(set)/private(set)/etc), without introducing a new keyword.

If we ever introduce modules or packages in the future, internal could certainly be layered on top, or become syntactic sugar over private(namespace) — or some part of it. Much like readonly is now almost syntactic sugar over “public private(set)”, other than the write-once restriction.

For now, I’m aiming to focus on making internal APIs easier to express without inventing a new packaging system.

— Rob

On 08/11/2025 12:43, Rob Landers wrote:

I’d like to introduce an RFC for discussion: PHP: rfc:namespace_visibility which proposes a new visibility modifier: private(namespace).

Hi Rob,

Thanks for putting together the RFC.

My main concern with this and similar single-keyword proposals is that its only useful if people lay out their code in a particular way, rather than fitting with the variety of namespace hierarchies seen in the wild.

For instance, in your example, you have two classes:

- App\Auth\SessionManager
- App\Auth\SessionStore

But if this was library with a set of interchangeable session stores, it might well lay them out like this:

- Acme\AuthLib\Session\Manager
- Acme\AuthLib\Session\Store\SessionStoreInterface
- Acme\AuthLib\Session\Store\DatabaseSessionStore
- Acme\AuthLib\Session\Store\FileSystemSessionStore
- etc

In that case, "current namespace plus children" would work, and I'd be interested in your reasoning for requiring an exact match instead.

But even that might not be enough, if for some reason it looked like this:

- Acme\AuthLib\Services\SessionManager
- Acme\AuthLib\Implementations\SessionStore\SessionStoreInterface
- Acme\AuthLib\Implementations\SessionStore\DatabaseSessionStore
- Acme\AuthLib\Implementations\SessionStore\FileSystemSessionStore
- etc

Here, what we want is visibilty for everything inside "Acme\AuthLib", not only in "Acme\AuthLib\Services".

Since, as you say in another reply, we don't have a standard definition of "module", "package", or "assembly", I think we need a keyword or attribute which takes as a parameter either the namespace prefix, or the number of levels to match.

--
Rowan Tommins
[IMSoP]

Hi,

I like the proposal however the choice of syntax seems inconsistent.

Traditionally visibility is defined by a single keyword like private, protected, etc, each implicitly defining the scope from which the property is accessible.

Aviz (PHP 8.4) added the possibility of defining the operations that can be performed in the given scope like (get) or (set), the scope however is still defined by the existing keywords (private for class scope and protected for child scope).
As such, I’d suggest to use a dedicated keyword for namespace level visibility like “internal” or “ns-private”, the operations will still be defined within braces ().

An excerpt from future scope of aviz rfc differentiating between visibility and operations.

At this time, there are only two possible operations to scope: read and write. In concept, additional operations could be added with their own visibility controls. Possible examples include:
protected(&get) - Vary whether a reference to a property can be obtained independently of getting the value. (Would override the set visibility if used.)
private(setref) - Allows a property to be set by reference only from certain scopes.

[

Faizan Akram Dar

](http://faizanakram.me)

On Sat, 8 Nov 2025, 14:21 Rob Landers, rob@bottled.codes wrote:

Hello Internals,

I’d like to introduce an RFC for discussion: https://wiki.php.net/rfc/namespace_visibility which proposes a new visibility modifier: private(namespace).

This idea has appeared several times in previous threads but never progressed to a formal proposal (from what I could find). My hope is that with defined semantics, examples, and implementation details, we can evaluate it properly and see whether there’s support for moving forward. Feedback is very welcome.

Sincerely,

Rob Landers

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

On 08/11/2025 12:43, Rob Landers wrote:

I’d like to introduce an RFC for discussion:
https://wiki.php.net/rfc/namespace_visibility which proposes a new
visibility modifier: private(namespace).

Hi Rob,

Thanks for putting together the RFC.

My main concern with this and similar single-keyword proposals is that
its only useful if people lay out their code in a particular way, rather
than fitting with the variety of namespace hierarchies seen in the wild.

For instance, in your example, you have two classes:

  • App\Auth\SessionManager
  • App\Auth\SessionStore

But if this was library with a set of interchangeable session stores, it
might well lay them out like this:

  • Acme\AuthLib\Session\Manager
  • Acme\AuthLib\Session\Store\SessionStoreInterface
  • Acme\AuthLib\Session\Store\DatabaseSessionStore
  • Acme\AuthLib\Session\Store\FileSystemSessionStore
  • etc

In that case, “current namespace plus children” would work, and I’d be
interested in your reasoning for requiring an exact match instead.

But even that might not be enough, if for some reason it looked like this:

  • Acme\AuthLib\Services\SessionManager
  • Acme\AuthLib\Implementations\SessionStore\SessionStoreInterface
  • Acme\AuthLib\Implementations\SessionStore\DatabaseSessionStore
  • Acme\AuthLib\Implementations\SessionStore\FileSystemSessionStore
  • etc

Here, what we want is visibilty for everything inside “Acme\AuthLib”,
not only in “Acme\AuthLib\Services”.

Since, as you say in another reply, we don’t have a standard definition
of “module”, “package”, or “assembly”, I think we need a keyword or
attribute which takes as a parameter either the namespace prefix, or the
number of levels to match.


Rowan Tommins
[IMSoP]

Hi Rowan,

Thanks for the detailed feedback. The exact-match requirement is intentional rather than incidental. In my nested classes RFC, I assumed namespaces behaved hierarchically, and it was pointed out that isn’t always guaranteed. So, one of the main design goals with this RFC was to make a boundary that was crisp and unsurprising: if two pieces of code have the same lexical namespace, access is allowed; if not, it isn’t. There’s no need to consider namespace depth, prefixes, or directory layout.

The moment we allow “namespace prefixes”, we introduce questions that need to be fleshed out separately and with care. Do we treat “Acme\AuthLib\Session” and “Acme\AuthLib\Services” as related? Even if they come from different vendors and are unrelated in any way? Are namespaces hierarchical or just flat strings that happen to contain separators?

Today namespaces are a name resolution mechanism, not a semantic hierarchy. Matching by prefix would start treating them as a package system, and I explicitly am trying to avoid that in this RFC.

By restricting the rule to exact namespace equality, the feature is straightforward to explain and to understand. It also prevents “accidental” access because two unrelated namespaces happen to share a prefix.

If we ever define a module/package boundary in PHP, a recursive or prefix-based visibility could be layered on top. For this RFC, I’m aiming for a feature that provides enforceable encapsulation using the boundaries PHP already defines, albeit, loosely through name resolution.

If you have a concrete, unambiguous rule for prefix-based access that wouldn’t cause surprises or make assumptions about how people organise their codebases, I’d be happy to discuss it. Right now, every version I’ve explored would reopen the “module”/“package” debate from this summer.

— Rob

On Sat, Nov 8, 2025, at 18:43, Faizan Akram Dar wrote:

Hi,

I like the proposal however the choice of syntax seems inconsistent.

Traditionally visibility is defined by a single keyword like private, protected, etc, each implicitly defining the scope from which the property is accessible.

Aviz (PHP 8.4) added the possibility of defining the operations that can be performed in the given scope like (get) or (set), the scope however is still defined by the existing keywords (private for class scope and protected for child scope).
As such, I’d suggest to use a dedicated keyword for namespace level visibility like “internal” or “ns-private”, the operations will still be defined within braces ().

An excerpt from future scope of aviz rfc differentiating between visibility and operations.

At this time, there are only two possible operations to scope: read and write. In concept, additional operations could be added with their own visibility controls. Possible examples include:
protected(&get) - Vary whether a reference to a property can be obtained independently of getting the value. (Would override the set visibility if used.)
private(setref) - Allows a property to be set by reference only from certain scopes.

Faizan Akram Dar
faizanakram.me

On Sat, 8 Nov 2025, 14:21 Rob Landers, rob@bottled.codes wrote:

Hello Internals,

I’d like to introduce an RFC for discussion: https://wiki.php.net/rfc/namespace_visibility which proposes a new visibility modifier: private(namespace).

This idea has appeared several times in previous threads but never progressed to a formal proposal (from what I could find). My hope is that with defined semantics, examples, and implementation details, we can evaluate it properly and see whether there’s support for moving forward. Feedback is very welcome.

Sincerely,

Rob Landers

Please remember to bottom post!

The reason I chose private(namespace) rather than a new keyword is that the semantics are fundamentally a widened form of private. Access is granted to code in the exact same namespace and nowhere else. There’s no additional scope concept beyond that.

Introducing a new keyword like internal or ns-private suggests a new typeof boundary rather than a refinement on existing ones. In other languages, internal et al. is tied to a module, assembly, crate, and not a namespace. PHP doesn’t have a formal module boundary today, and adopting the term internal would imply we’re defining one.

Sidenote:
When I proposed nested classes earlier this year, the conversation quickly shifted into what “packages” or “modules” should mean for PHP, and that ended up becoming a much larger debate. I’d like to avoid pulling that discussion into this RFC. It’s a related, but orthogonal topic and could absolutely be explored in a separate thread.

private(namespace) follows the same syntactic pattern introduced in asymmetric visibility: a base keyword with a parenthesised refinement. In AViz, the refinement controls operations (set/get) and here it refines which callers are allowed. The syntax feels familiar and doesn’t require introducing a new keyword.

If we end up introducing modules/packages in a future RFC, a dedicated keyword like internal could absolutely build on top of this. For now, the goal of this RFC is to make a small, well-defined improvement that can be improved upon in future RFCs.

— Rob

On Sat, 8 Nov 2025 at 17:18 Rob Landers rob@bottled.codes wrote:

Sidenote:

When I proposed nested classes earlier this year, the conversation quickly shifted into what “packages” or “modules” should mean for PHP, and that ended up becoming a much larger debate. I’d like to avoid pulling that discussion into this RFC. It’s a related, but orthogonal topic and could absolutely be explored in a separate thread.

— Rob

I understand the sentiment. I’ve participated in some of these debates. It seems like reaching consensus is impossible because it’s not possible for a solution to be perfect and there’s no consensus in choosing the least worst option.

Unfortunately, namespace visibility, nested classes, class friendship and even some long debated performance optimizations which may or may not allow linking symbols and may or may not help Generics all sit around on finding a way to go from namespaces to packages.

This alternative, to me, seems more likely to make things worse rather than better and I really hope PHP can figure out a package system.

On 08/11/2025 20:08, Rob Landers wrote:

The moment we allow "namespace prefixes", we introduce questions that need to be fleshed out separately and with care. Do we treat "Acme\AuthLib\Session" and "Acme\AuthLib\Services" as related? Even if they come from different vendors and are unrelated in any way? Are namespaces hierarchical or just flat strings that happen to contain separators?

While I appreciate the desire to keep things simple, I don't think we can avoid asking those questions, we can only propose answers. In your current RFC, the answers are:

- No two namespaces are related, even if their names imply a hierarchy.
- Two "internal" classes in the same namespace can see each other even if they are written by different vendors.

Today namespaces are a name resolution mechanism, not a semantic hierarchy. Matching by prefix would start treating them as a package system, and I explicitly am trying to avoid that in this RFC.

I don't think that's true. The language already recognises the namespace separator, and that if you are in namespace "Acme\AuthLib", an unqualified reference to "Services\SessionManager" refers to "Acme\AuthLib\Services\SessionManager". That doesn't require any concept of "package", but it is clearly based on the conception of namespaces as a hierarchy.

By restricting the rule to exact namespace equality, the feature is straightforward to explain and to understand. It also prevents "accidental" access because two unrelated namespaces happen to share a prefix.

My concern is that by restricting it so much, we would prevent most of the real-world use cases for it, since the reality is that people use much more complex namespace hierarchies.

If you have a concrete, unambiguous rule for prefix-based access that wouldn’t cause surprises or make assumptions about how people organise their codebases, I’d be happy to discuss it. Right now, every version I’ve explored would reopen the "module"/"package" debate from this summer.

I mentioned, briefly, two possibilities:

I think we need a keyword or attribute which takes as a parameter either the namespace prefix, or the number of levels to match

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

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

A number of levels version could look like this:

namespace Acme\AuthLib\Somewhere\Deep\In\Package;
// ...
#[NamespacePrivate(minLevels: 2, maxLevels: null)]

There's all sorts of variations on how those arguments could be presented, keywords vs attributes, etc; but the key point is the language is not defining where the boundary is, it is requiring the user to do so.

The prefix-based approach could be expanded into an allow-list that didn't require any relationship to the current namespace at all:

namespace Acme\AuthLib\Somewhere\Deep\In\Package;
// ...
#[AllowNamespace('Acme\AuthLib\*')]
#[AllowNamespace('Zeppo\FrameworkCore\*')]

Or even an allow-deny rule system:

#[AllowNamespace('Acme\AuthLib\*')]
#[DenyNamespace('Acme\AuthLib\Plugins\*')]

At that point, it's admittedly rather complex, but it makes *even fewer* assumptions about what constitutes a "package".

--
Rowan Tommins
[IMSoP]

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

On 08/11/2025 20:08, Rob Landers wrote:

The moment we allow “namespace prefixes”, we introduce questions that
need to be fleshed out separately and with care. Do we treat
“Acme\AuthLib\Session” and “Acme\AuthLib\Services” as related? Even if
they come from different vendors and are unrelated in any way? Are
namespaces hierarchical or just flat strings that happen to contain
separators?

While I appreciate the desire to keep things simple, I don’t think we
can avoid asking those questions, we can only propose answers. In your
current RFC, the answers are:

  • No two namespaces are related, even if their names imply a hierarchy.
  • Two “internal” classes in the same namespace can see each other even
    if they are written by different vendors.

Today namespaces are a name resolution mechanism, not a semantic
hierarchy. Matching by prefix would start treating them as a package
system, and I explicitly am trying to avoid that in this RFC.

I don’t think that’s true. The language already recognises the namespace
separator, and that if you are in namespace “Acme\AuthLib”, an
unqualified reference to “Services\SessionManager” refers
to “Acme\AuthLib\Services\SessionManager”. That doesn’t require any
concept of “package”, but it is clearly based on the conception of
namespaces as a hierarchy.

By restricting the rule to exact namespace equality, the feature is
straightforward to explain and to understand. It also prevents
“accidental” access because two unrelated namespaces happen to share a
prefix.

My concern is that by restricting it so much, we would prevent most of
the real-world use cases for it, since the reality is that people use
much more complex namespace hierarchies.

If you have a concrete, unambiguous rule for prefix-based access that
wouldn’t cause surprises or make assumptions about how people organise
their codebases, I’d be happy to discuss it. Right now, every version
I’ve explored would reopen the “module”/“package” debate from this summer.

I mentioned, briefly, two possibilities:

I think we need a keyword or attribute which takes as a parameter
either the namespace prefix, or the number of levels to match

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

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

A number of levels version could look like this:

namespace Acme\AuthLib\Somewhere\Deep\In\Package;
// …
#[NamespacePrivate(minLevels: 2, maxLevels: null)]

There’s all sorts of variations on how those arguments could be
presented, keywords vs attributes, etc; but the key point is the
language is not defining where the boundary is, it is requiring the user
to do so.

The prefix-based approach could be expanded into an allow-list that
didn’t require any relationship to the current namespace at all:

namespace Acme\AuthLib\Somewhere\Deep\In\Package;
// …
#[AllowNamespace(‘Acme\AuthLib*’)]
#[AllowNamespace(‘Zeppo\FrameworkCore*’)]

Or even an allow-deny rule system:

#[AllowNamespace(‘Acme\AuthLib*’)]
#[DenyNamespace(‘Acme\AuthLib\Plugins*’)]

At that point, it’s admittedly rather complex, but it makes even fewer
assumptions about what constitutes a “package”.


Rowan Tommins
[IMSoP]

Hi Rowan,

Thanks for expanding the examples.

Those approaches: prefix matching, wildcard matching, level-based matching, or allow/deny lists are all significantly more expressive than what this RFC is aiming for. They move the discussion from “namespace -private visibility” into designing a generalised access-control system or a de-facto package model. That’s a much wider problem space, and would need its own design work (and, likely, its own RFC).

The goal of this RFC is intentionally minimal: take the boundary that already exists in the language (the lexical namespace) and allow private visibility to extend across it. Exact namespace equality keeps the rule unambiguous, requires no new access-control model, and avoids assumptions about how developers structure hierarchies.

I agree that broader forms of scoping could be useful, but they should be in their own proposal. One advantage of keeping this RFC small is that it can be expanded later without a BC break. For example, prefix matching (maybe something like private(namespace: Acme\AuthLib)) or a protected-namespace variant could be layered on top if the community wants to go in that direction.

This RFC isn’t intended to solve the entire space; it’s the smallest useful step that seems to fairly cleanly fit into PHP’s current model without too much churn on the engine.

— Rob

On 08/11/2025 21:56, Rob Landers wrote:

Those approaches: prefix matching, wildcard matching, level-based matching, or allow/deny lists are all significantly more expressive than what this RFC is aiming for.

Yes. That's why I think they're a better idea.

They move the discussion from "namespace -private visibility" into designing a generalised access-control system or a de-facto package model.

No. They explicitly *avoid* defining any kind of package model, by letting the user mention *whatever prefix they like*. That might match the root namespace they mention in composer.json, but it might just be a particular set of classes in a giant spaghetti application with no conception of "package" whatsoever.

The goal of this RFC is intentionally minimal: take the boundary that already exists in the language (the lexical namespace)

From the language's point of view, "namespace Foo\Bar; $x = new Baz;" is interchangeable with "namespace Foo; $x = new Bar\Baz;"

Namespaces are inherently hierarchical, and *any* cut-off point in that hierarchy is an equally natural boundary.

Exact namespace equality keeps the rule unambiguous

What is ambiguous about any of the examples I showed?

requires no new access-control model

I have no idea what you mean by this. We don't currently have any namespace-based access-control, and we're addding it. How does "must match exactly" vs "must match given prefix" change that?

avoids assumptions about how developers structure hierarchies

On the contrary, it assumes that a developer will structure their hierarchy so that related classes are in the same namespace, and not any parent or child namespaces.

My suggestion, on the other hand, truly makes no assumption: it lets the developer specify exactly the structure they want.

One advantage of keeping this RFC small is that it can be expanded later without a BC break. For example, prefix matching (maybe something like `private(namespace: Acme\AuthLib)`) or a protected-namespace variant could be layered on top if the community wants to go in that direction.

That's a reasonable argument, but the risk is that if we don't consider what that future scope would look like, we can end up with syntax or semantics that makes our lives unnecessarily difficult later. As I understand it, this happened to some extent with the "readonly" keyword.

This RFC isn’t intended to solve the entire space; it’s the smallest useful step that seems to fairly cleanly fit into PHP’s current model without too much churn on the engine.

I think my fundamental question is: is it actually that useful? How often, in practice, do people want exact namespace matching, vs something more flexible?

--
Rowan Tommins
[IMSoP]

Hi Rob

On Sat, Nov 8, 2025, 14:46 Rob Landers rob@bottled.codes wrote:

Hello Internals,

I’d like to introduce an RFC for discussion: https://wiki.php.net/rfc/namespace_visibility which proposes a new visibility modifier: private(namespace).

This idea has appeared several times in previous threads but never progressed to a formal proposal (from what I could find). My hope is that with defined semantics, examples, and implementation details, we can evaluate it properly and see whether there’s support for moving forward. Feedback is very welcome.

Nice work on this.

I have one issue:

Visibility hierarchy: public < protected < private(namespace) < private

I think is not a correct view of the real problem space, as the protected and private namespace scopes are separate sets that might have things in common but can also be distinct.

I think the correct way to model it, is to have two hierarchies:
public < protected < private

public < private(namespace) < private

Otherwise you can have things like
protected private(namespace)(set)
that is unclear how it should be handled.
Can you clarify what is the right-now expected get and set allowance for these cases:

  • child classes in the same namespace
  • child classes in another namespace
  • non-child classes in the same namespace

My suggestion is to not allow mixing protected and private namespace for aviz.


Alex

On Sun, Nov 9, 2025, at 07:09, Alexandru Pătrănescu wrote:

Hi Rob

On Sat, Nov 8, 2025, 14:46 Rob Landers rob@bottled.codes wrote:

Hello Internals,

I’d like to introduce an RFC for discussion: https://wiki.php.net/rfc/namespace_visibility which proposes a new visibility modifier: private(namespace).

This idea has appeared several times in previous threads but never progressed to a formal proposal (from what I could find). My hope is that with defined semantics, examples, and implementation details, we can evaluate it properly and see whether there’s support for moving forward. Feedback is very welcome.

Nice work on this.

I have one issue:

Visibility hierarchy: public < protected < private(namespace) < private

I think is not a correct view of the real problem space, as the protected and private namespace scopes are separate sets that might have things in common but can also be distinct.

I think the correct way to model it, is to have two hierarchies:
public < protected < private

public < private(namespace) < private

Otherwise you can have things like
protected private(namespace)(set)
that is unclear how it should be handled.
Can you clarify what is the right-now expected get and set allowance for these cases:

  • child classes in the same namespace
  • child classes in another namespace
  • non-child classes in the same namespace

My suggestion is to not allow mixing protected and private namespace for aviz.


Alex

Hi Alex,

I think you’re right, treating this as a simple linear hierarchy is misleading. Protected and private(namespace) are based on different axes:

  • protected is inheritance-based
  • private(namespace) is namespace-based

So, for protected private(namespace):

  • child class in the same namespace: read + write
  • child class in a different namespace: read-only
  • non-child class in the same namespace: forbidden

Formally, we can consider the caller sets:

  • C[public]
  • C[protected] (declaring class ∪ subclasses)
  • C[ns] (all code in the exact declaring namespace)
  • C[private] (declaring class only)

We have two partial orders:

  • C[public] ⊇ C[protected] ⊇ C[private]
  • C[public] ⊇ C[ns] ⊇ C[private]

In general, C[protected] and C[ns] are incomparable (neither is a subset of the other).

For asymmetric properties, the (set) visibility must satisfy C[set] ⊇ C[base]. If C[set] and C[base] are incomparable or otherwise not a subset, it’s a compile-time error.

That yields:

  • public with any (set) visibility: C[any] ⊆ C[public]

  • protected with protected(set) or private(set) only: C[private] ⊆ C[protected]

  • private(namespace) with private(namespace)(set) or private(set) only: C[private] ⊆ C[ns]

  • private with private(set) only: C[private] ⊆ C[private]

  • protected with private(namespace)(set): incomparable

  • private with private(namespace)(set): C[ns] ⊈ C[private]

I’ll update the RFC to drop the linear hierarchy and update with the subset rule explicitly with some examples.

— Rob

On Sun, Nov 9, 2025, at 10:45, Rob Landers wrote:

On Sun, Nov 9, 2025, at 07:09, Alexandru Pătrănescu wrote:

Hi Rob

On Sat, Nov 8, 2025, 14:46 Rob Landers rob@bottled.codes wrote:

Hello Internals,

I’d like to introduce an RFC for discussion: https://wiki.php.net/rfc/namespace_visibility which proposes a new visibility modifier: private(namespace).

This idea has appeared several times in previous threads but never progressed to a formal proposal (from what I could find). My hope is that with defined semantics, examples, and implementation details, we can evaluate it properly and see whether there’s support for moving forward. Feedback is very welcome.

Nice work on this.

I have one issue:

Visibility hierarchy: public < protected < private(namespace) < private

I think is not a correct view of the real problem space, as the protected and private namespace scopes are separate sets that might have things in common but can also be distinct.

I think the correct way to model it, is to have two hierarchies:
public < protected < private

public < private(namespace) < private

Otherwise you can have things like
protected private(namespace)(set)
that is unclear how it should be handled.
Can you clarify what is the right-now expected get and set allowance for these cases:

  • child classes in the same namespace
  • child classes in another namespace
  • non-child classes in the same namespace

My suggestion is to not allow mixing protected and private namespace for aviz.


Alex

Hi Alex,

I think you’re right, treating this as a simple linear hierarchy is misleading. Protected and private(namespace) are based on different axes:

  • protected is inheritance-based
  • private(namespace) is namespace-based

So, for protected private(namespace):

  • child class in the same namespace: read + write
  • child class in a different namespace: read-only
  • non-child class in the same namespace: forbidden

Formally, we can consider the caller sets:

  • C[public]
  • C[protected] (declaring class ∪ subclasses)
  • C[ns] (all code in the exact declaring namespace)
  • C[private] (declaring class only)

We have two partial orders:

  • C[public] ⊇ C[protected] ⊇ C[private]
  • C[public] ⊇ C[ns] ⊇ C[private]

In general, C[protected] and C[ns] are incomparable (neither is a subset of the other).

For asymmetric properties, the (set) visibility must satisfy C[set] ⊇ C[base]. If C[set] and C[base] are incomparable or otherwise not a subset, it’s a compile-time error.

That yields:

  • public with any (set) visibility: C[any] ⊆ C[public]
  • protected with protected(set) or private(set) only: C[private] ⊆ C[protected]
  • private(namespace) with private(namespace)(set) or private(set) only: C[private] ⊆ C[ns]
  • private with private(set) only: C[private] ⊆ C[private]
  • protected with private(namespace)(set): incomparable
  • private with private(namespace)(set): C[ns] ⊈ C[private]

I’ll update the RFC to drop the linear hierarchy and update with the subset rule explicitly with some examples.

— Rob

I’ve updated the RFC and implementation accordingly along with some editorial changes.

— Rob

On Sat, Nov 8, 2025, at 5:13 PM, Rowan Tommins [IMSoP] wrote:

On 08/11/2025 21:56, Rob Landers wrote:

One advantage of keeping this RFC small is that it can be expanded
later without a BC break. For example, prefix matching (maybe
something like `private(namespace: Acme\AuthLib)`) or a
protected-namespace variant could be layered on top if the community
wants to go in that direction.

That's a reasonable argument, but the risk is that if we don't consider
what that future scope would look like, we can end up with syntax or
semantics that makes our lives unnecessarily difficult later. As I
understand it, this happened to some extent with the "readonly" keyword.

Exactly the example I was thinking of. :slight_smile: "Junior version first" approaches sometimes work out (FCC doesn't seem to have caused issues for PFA in practice), and other times not (readonly was a major PITA for aviz, and even delayed it for a release as we figured out how they should interact). I'd much rather know what the "full solution" looks like and plan for it, even if it's in incremental steps, than throw a junior version at the wall and hope for the best.

This RFC isn’t intended to solve the entire space; it’s the smallest
useful step that seems to fairly cleanly fit into PHP’s current model
without too much churn on the engine.

I think my fundamental question is: is it actually that useful? How
often, in practice, do people want exact namespace matching, vs
something more flexible?

In practice, the extra-class boundary I most often want is "my composer package." I rarely need to protect things beyond that.

I am broadly in favor of extra-class visibility controls, whether they go through a module system or something else. Whether the current RFC is a good way of doing so, I am not convinced.

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.

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.

That said, I'm not convinced it is a separate dimension yet. I'd put "nsviz" between public and protected, not protected and private. The assumption is that your "allowed" scope is something you control. So if you don't want to use that property from child classes in your own namespace... just don't. If you don't want to allow it to be used by child classes in another namespace... then nsviz(get) solves that problem.

I will also note that while it's typical for namespace and file path to be 1:1, there's nothing that requires it. That means any strictly namespace-based visibility is trivial to bypass.

For example:

// vendor/foo/bar/Baz.php

namespace Foo\Bar\Baz;

class Careful {
  nsviz string $secret;
}

// /app/User.php

namespace Foo\Bar;

class Invader {
  public function hax(Careful $c): string {
    return $c->secret;
}
}

namespace App;

class User {
  public function doStuff(Careful $c) {
    $secret = new Foo\Bar\Invader($c)->hax();
   // Boom, we've read the value.
  }
}

Whether that is a design flaw or a feature is, I suppose, debatable.

--Larry Garfield

On Sun, Nov 9, 2025, at 16:33, Larry Garfield wrote:

On Sat, Nov 8, 2025, at 5:13 PM, Rowan Tommins [IMSoP] wrote:

On 08/11/2025 21:56, Rob Landers wrote:

One advantage of keeping this RFC small is that it can be expanded
later without a BC break. For example, prefix matching (maybe
something like private(namespace: Acme\AuthLib)) or a
protected-namespace variant could be layered on top if the community
wants to go in that direction.

That’s a reasonable argument, but the risk is that if we don’t consider
what that future scope would look like, we can end up with syntax or
semantics that makes our lives unnecessarily difficult later. As I
understand it, this happened to some extent with the “readonly” keyword.

Exactly the example I was thinking of. :slight_smile: “Junior version first” approaches sometimes work out (FCC doesn’t seem to have caused issues for PFA in practice), and other times not (readonly was a major PITA for aviz, and even delayed it for a release as we figured out how they should interact). I’d much rather know what the “full solution” looks like and plan for it, even if it’s in incremental steps, than throw a junior version at the wall and hope for the best.

This RFC isn’t intended to solve the entire space; it’s the smallest
useful step that seems to fairly cleanly fit into PHP’s current model
without too much churn on the engine.

I think my fundamental question is: is it actually that useful? How
often, in practice, do people want exact namespace matching, vs
something more flexible?

In practice, the extra-class boundary I most often want is “my composer package.” I rarely need to protect things beyond that.

I am broadly in favor of extra-class visibility controls, whether they go through a module system or something else. Whether the current RFC is a good way of doing so, I am not convinced.

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 () is the syntax model that aviz established, and tossing extra parens in there just confuses things for everyone.

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.

That said, I’m not convinced it is a separate dimension yet. I’d put “nsviz” between public and protected, not protected and private. The assumption is that your “allowed” scope is something you control. So if you don’t want to use that property from child classes in your own namespace… just don’t. If you don’t want to allow it to be used by child classes in another namespace… then nsviz(get) solves that problem.

I will also note that while it’s typical for namespace and file path to be 1:1, there’s nothing that requires it. That means any strictly namespace-based visibility is trivial to bypass.

For example:

[snip]

Whether that is a design flaw or a feature is, I suppose, debatable.

–Larry Garfield

Hi Larry (and everyone else following the thread),

I think we’re all aligned that PHP would benefit from a proper module or package boundary. Many features people want — including namespace visibility — become cleaner once such a boundary exists. One doesn’t strictly require the other, but they definitely complement each other. The difficulty is that PHP currently has no agreed-upon definition of what a “module” or “package” is:

  • Composer root namespaces?

  • Physical directories?

  • Compilation units?

  • Attributes?

  • Something else entirely?

Because of that, there’s no existing boundary we can safely build on without first settling that much larger question. That’s why this RFC deliberately stays within a boundary the language already defines: the lexical namespace. It doesn’t try to define packages or introduce any new scoping model. It also doesn’t prevent a future RFC from doing so.

If the community reaches consensus on a formal module/package boundary later, namespace-level visibility can coexist with it or even be folded into it. Nothing in this RFC blocks that path.

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.

To keep this discussion focused:

If someone has concerns about this RFC’s semantics, performance, syntax, implementation, error messaging, interaction with inheritance/traits, or BC impact — that’s absolutely fair and I’d like to address them:

But I’d like to avoid turning this into a thread about defining a package/specification/module system. That’s a valid discussion, and I’d happily participate in another thread, but it’s not one this RFC is trying to solve.

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.


Now, to reply to your specific points about this RFC:

First, syntax vs. AViz.

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.

That said, if the consensus emerges around a new keyword, that’s something I can adjust. The important behaviour is the caller rule, not the exact token spelling.

Second, is exact-match useful enough?

Yes, there are plenty of codebases where internal helpers live side-by-side in one namespace, especially in domain-driven or layered architectures where namespaces are the boundaries. Today, teams either make them public, wrap them in a service class, use @internal, or wire them together via reflection or debug_backtrace.

All of those come with tradeoffs. Exact-match namespace visibility offers a simple enforceable boundary without designing a whole package system or assuming anything about code hierarchy. It’s not the most expressive possible rule, but it’s useful and predictable.

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.

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.

If a future module/package system introduces stronger enforcement, this RFC can layer underneath it without a conflict.

Sincerely,

— Rob

Hi

On 11/8/25 21:09, Rob Landers wrote:

Please remember to bottom post!

And please also remember to cut the quotes to the relevant part instead of just full quoting.

The reason I chose private(namespace) rather than a new keyword is that the semantics are fundamentally a widened form of private.

This is not accurate. As Alexandru also noted, it rather is an alternative to `protected`.

As I believe I had also noted for the nested classes RFC, `private` is special in that it does affect any other class at all. For all intents and purposes it is not visible to the outside and new private symbols can be introduced without taking care of possible parent or child classes.

From what I understand this is different for namespace-visibility as suggested in the RFC. Since the method is externally visible, changes to the signature affect both external callers and child classes, which need to maintain a compatible signature.

In fact it has a rather odd effect on child classes in a different namespace in that some methods are just unavailable to them. It is probably necessarily to restrict inheriting from classes with namespace-restricted symbols to classes from the same namespace (i.e. making the class itself namespace-final so to say).

Best regards
Tim Düsterhus

Hi

On 11/9/25 16:07, Rob Landers wrote:

I’ve updated the RFC and implementation accordingly along with some editorial changes.

The RFC now mentions:

Combinations that mix protected and private with private(namespace)(set) are a compile-time error because their allowed caller sets aren’t subsets of each other.

Which is not particularly explicit in what “mix” means. I initially missed the `(set)` in the `private(namespace)(set)` until I read the ML discussion.

From what I see the RFC does not discuss the following at all:

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

and vice versa:

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

Best regards
Tim Düsterhus

On Sun, Nov 9, 2025, at 19:01, Tim Düsterhus wrote:

Hi

On 11/9/25 16:07, Rob Landers wrote:

I’ve updated the RFC and implementation accordingly along with some editorial changes.

From what I see the RFC does not discuss the following at all:

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

and vice versa:

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

Best regards
Tim Düsterhus

Good catch! The RFC should spell out these cases directly. The behaviour follows the same rule PHP already applies to private during inheritance:

class P {
    protected function x() {}
}

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

Reducing visibility is an error. This is rejected for the same reason that redefining a protected method as private is rejected today: C::x() would be less visible than P::x().

private(namespace) doesn’t introduce any new ambiguity here. The name even implies that visibility will be reduced and lives below protected in the caller set, so reducing visibility is not allowed.

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.

I’ll update the RFC to explicitly document both of these cases so the inheritance rules are unambiguous.

Thanks for pointing it out.

— Rob

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