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
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?
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
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]
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
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]
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 matchTo 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
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 likeprivate(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]
One advantage of keeping this RFC small is that it can be expanded
later without a BC break. For example, prefix matching (maybe
something likeprivate(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. :-) "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. :-) 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
One advantage of keeping this RFC small is that it can be expanded
later without a BC break. For example, prefix matching (maybe
something likeprivate(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. :-) "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 saveinternalfor some other use, that's fine. Come up with a different keyword. :-) 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:
[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
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 whyprivate(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
internalornsviz, we’d effectively be adding a new visibility class, not a refinement ofprivateand 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
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.
internalfor 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 whyprivate(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
internalornsviz, we’d effectively be adding a new visibility class, not a refinement ofprivateand 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 ofprivateis 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
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.
internalfor 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 whyprivate(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
internalornsviz, we’d effectively be adding a new visibility class, not a refinement ofprivateand 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 ofprivateis 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 https://wiki.php.net/rfc/namespace_visibility?utm_source=chatgpt.com
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
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.
internalfor 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 whyprivate(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
internalornsviz, we’d effectively be adding a new visibility class, not a refinement ofprivateand 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 ofprivateis 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 followprotectedrather thanprivate
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 betweenprotectedandprivate(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
protectedandprivate(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 withprotectedrather thanprivate, the spellingprivate(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 (namespaceorlocal), but I’d like to collect feedback before changing the RFC text.Updated RFC:
https://wiki.php.net/rfc/namespace_visibility https://wiki.php.net/rfc/namespace_visibility?utm_source=chatgpt.comImplementation:
https://github.com/php/php-src/pull/20421Thanks 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
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 saveinternalfor some other use, that's fine. Come up with a different keyword. :-) 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:
https://wiki.php.net/rfc/marking_overriden_methods#why_an_attribute_and_not_a_keyword
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
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
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]
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.
[image: --]
Faizan Akram Dar
[image: https://]faizanakram.me
http://faizanakram.me
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
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
https://faizanakram.me__
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
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.
Hi
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 Rob
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 Rob
__
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) < privateOtherwise 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:
-
protectedis 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:
-
publicwith any(set)visibility: C[any] ⊆ C[public] -
protectedwithprotected(set)orprivate(set)only: C[private] ⊆ C[protected] -
private(namespace)withprivate(namespace)(set)orprivate(set)only: C[private] ⊆ C[ns] -
privatewithprivate(set)only: C[private] ⊆ C[private] -
protectedwithprivate(namespace)(set): incomparable -
privatewithprivate(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
Hi Rob
__
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) < privateOtherwise 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.
--
AlexHi Alex,
I think you’re right, treating this as a simple linear hierarchy is misleading.
Protectedandprivate(namespace)*are *based on different axes:
protectedis inheritance-basedprivate(namespace)is namespace-basedSo, 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:
publicwith any(set)visibility: C[any] ⊆ C[public]protectedwithprotected(set)orprivate(set)only: C[private] ⊆ C[protected]private(namespace)withprivate(namespace)(set)orprivate(set)only: C[private] ⊆ C[ns]privatewithprivate(set)only: C[private] ⊆ C[private]protectedwithprivate(namespace)(set): incomparableprivatewithprivate(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
Hi
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
Hi
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
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,
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
Hi
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)norprotectedis 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
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)
norprotectedis 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 notlegal 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:
-
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. -
As you pointed out,
protectedandprivate(namespace)aren’t compatible; thus we should only allow the same or a superset. So, it should only allowpublicorprivate(namespace)when inheriting in the same namespace.
— Rob