Alternatives for encapsulation when using composition instead of inheritance

php.internals

Olle Härstedt

5 years ago
Hi list! Don't know if this already exists, but maybe we could consider brainstorming or gather alternatives for different types of encapsulation when using composition? Currently, encapsulation like private and protected is only possible when using inheritance - when using composition, that is, class A is using (aggregating) class B, your only alternative is public, so full visibility (or getters/setters, which also is full visibility, since you can't limit who's using those methods). There are four alternatives at least: * Friend classes, like in C++: Class B is annotated to be a friend of A, so class A can see the content of class B but no other class can. * Package private, like in Java: Class A and B are defined in the same package, so they can see each other's fields that are defined as "internal" * Namespace private: Class A and B are defined in the same namespace, so they can see each other's properties that are defined as "internal" * Attribute for namespace private: Like above, but not possible right now since it requires that namespace is added to the PHP runtime Functionality like this should not be handled by static analysis, because library authors need to be able to trust it. Psalm supports already @internal and @psalm-internal, but of course not everyone is using Psalm/Phpstan/other analyzer. The problem with package private is that PHP does not have packages. The problem with namespace private is that it cannot handle namespace siblings. Another problem is that it can be "hacked" by client code if the client code decides to use another vendor's namespace (a weird thing to do, though). The run-time hit should not be bigger, since we're already checking each object property for private. Correct me if I'm wrong. The risk/benefit/effort should be considered for all alternatives. I'd argue that something is better than nothing, so far as it does not block future work. :) Maybe adding namespaces to runtime could be a first step? Olle

Christoph Becker

5 years ago
On 26.08.2021 at 15:47, Olle Härstedt wrote:
> * Friend classes, like in C++: Class B is annotated to be a friend of > A, so class A can see the content of class B but no other class can.
<https://wiki.php.net/rfc/friend-classes> has been declined.
> * Namespace private: Class A and B are defined in the same namespace, > so they can see each other's properties that are defined as "internal"
<https://wiki.php.net/rfc/namespace-visibility> appears to be inactive.
-- Christoph M. Becker

Olle Härstedt

5 years ago
2021-08-26 16:20 GMT+02:00, Christoph M. Becker <cmbecker69@gmx.de>:
> On 26.08.2021 at 15:47, Olle Härstedt wrote: > >> * Friend classes, like in C++: Class B is annotated to be a friend of >> A, so class A can see the content of class B but no other class can. > > <https://wiki.php.net/rfc/friend-classes> has been declined. > >> * Namespace private: Class A and B are defined in the same namespace, >> so they can see each other's properties that are defined as "internal" > > <https://wiki.php.net/rfc/namespace-visibility> appears to be inactive. > > -- > Christoph M. Becker >
Thanks! I'll check the belonging discussions on externals.io Olle

Rowan Collins

5 years ago
On 26/08/2021 14:47, Olle Härstedt wrote:
> Don't know if this already exists, but maybe we could consider > brainstorming or gather alternatives for different types of > encapsulation when using composition?
Although not quite equivalent to your suggestions, these threads on making delegation to an encapsulated object might be interesting to you: *  https://externals.io/message/103353 * https://github.com/php/php-src/pull/5168 If we had something like that, then perhaps there could be a "delegatable" visibility, which was the equivalent for "protected", but when delegating rather than inheriting, e.g. class Foo {     private int $a = 1;     delegatable int $b = 2; } class Bar {     private delegate Foo $foo;     public function __construct(Foo $foo) {         $this->foo = $foo;         echo $this->a; // Error: can't access private property of a different class         echo $this->b; // allowed: access is to a "delegatable" property, in a "delegated" context     } }
-- Rowan Tommins [IMSoP]

Olle Härstedt

5 years ago
2021-08-26 20:30 GMT+02:00, Rowan Tommins <rowan.collins@gmail.com>:
> On 26/08/2021 14:47, Olle Härstedt wrote: >> Don't know if this already exists, but maybe we could consider >> brainstorming or gather alternatives for different types of >> encapsulation when using composition? > > > Although not quite equivalent to your suggestions, these threads on > making delegation to an encapsulated object might be interesting to you: > > * https://externals.io/message/103353 > * https://github.com/php/php-src/pull/5168
Right, there are indeed a bunch of attempts and suggestions, some downvoted, others abandoned. I'm thinking adding namespaces to runtime might be a logical first step. That would allow attributes to add logic like internal properties for a certain namespace. Makes me wonder why that never happened for this PR: https://github.com/php/php-src/pull/947 Asking anyone on the list, would it be possible to make an opcode out of "namespace Foo\Bar" and letting it set a flag in the runtime system when run? Then make the flag accessible with a constant like __NAMESPACE_RUNTIME__ or whatever. Ooooh, there's an interesting case with "goto" and namespace visibility here: https://github.com/php/php-src/pull/947#issuecomment-419821198 That would mean every zval has to carry in which namespace it was defined inside. I wonder how that would affect the memory footprint of PHP.
> If we had something like that, then perhaps there could be a > "delegatable" visibility, which was the equivalent for "protected", but > when delegating rather than inheriting, e.g. > > class Foo { > private int $a = 1; > delegatable int $b = 2; > } > > class Bar { > private delegate Foo $foo; > public function __construct(Foo $foo) { > $this->foo = $foo; > echo $this->a; // Error: can't access private property of a > different class > echo $this->b; // allowed: access is to a "delegatable" > property, in a "delegated" context > } > }
The Foo class has to decide who to give access to, otherwise it's the same as public access. Olle

Olle Härstedt

5 years ago
2021-08-26 22:20 GMT+02:00, Olle Härstedt <olleharstedt@gmail.com>:
> 2021-08-26 20:30 GMT+02:00, Rowan Tommins <rowan.collins@gmail.com>: >> On 26/08/2021 14:47, Olle Härstedt wrote: >>> Don't know if this already exists, but maybe we could consider >>> brainstorming or gather alternatives for different types of >>> encapsulation when using composition? >> >> >> Although not quite equivalent to your suggestions, these threads on >> making delegation to an encapsulated object might be interesting to you: >> >> * https://externals.io/message/103353 >> * https://github.com/php/php-src/pull/5168 > > Right, there are indeed a bunch of attempts and suggestions, some > downvoted, others abandoned. > > I'm thinking adding namespaces to runtime might be a logical first > step. That would allow attributes to add logic like internal > properties for a certain namespace. Makes me wonder why that never > happened for this PR: https://github.com/php/php-src/pull/947 > > Asking anyone on the list, would it be possible to make an opcode out > of "namespace Foo\Bar" and letting it set a flag in the runtime system > when run? Then make the flag accessible with a constant like > __NAMESPACE_RUNTIME__ or whatever. > > Ooooh, there's an interesting case with "goto" and namespace > visibility here: > https://github.com/php/php-src/pull/947#issuecomment-419821198 > > That would mean every zval has to carry in which namespace it was > defined inside. I wonder how that would affect the memory footprint of > PHP. > >> If we had something like that, then perhaps there could be a >> "delegatable" visibility, which was the equivalent for "protected", but >> when delegating rather than inheriting, e.g. >> >> class Foo { >> private int $a = 1; >> delegatable int $b = 2; >> } >> >> class Bar { >> private delegate Foo $foo; >> public function __construct(Foo $foo) { >> $this->foo = $foo; >> echo $this->a; // Error: can't access private property of a >> different class >> echo $this->b; // allowed: access is to a "delegatable" >> property, in a "delegated" context >> } >> } > > The Foo class has to decide who to give access to, otherwise it's the > same as public access. > > Olle
Whops, Nikic already answered this in 2016:
> The goto here will jump from code in namespace A to code in namespace B without "directly" crossing the boundary. For this reason just inserting opcodes when a namespace starts or ends will quite cut it.
https://github.com/php/php-src/pull/947#issuecomment-224934615 So, deprecate goto when...? Olle

Rowan Collins

5 years ago
On 26 August 2021 21:20:35 BST, "Olle Härstedt" <olleharstedt@gmail.com> wrote:
>The Foo class has to decide who to give access to, otherwise it's the >same as public access.
It does decide who has access: any class that declares it as "delegated". In exactly the same way, "protected" gives access to any class that declares it as a "parent", and namespace visibility would give access to any class that declares itself in the same namespace. None of those actually limit access to a named list of classes, but all of them document an intended use, and catch mistakes where that intent isn't followed. Regards,
-- Rowan Tommins [IMSoP]

Alexandru Pătrănescu

5 years ago
On Fri, Aug 27, 2021 at 12:53 AM Rowan Tommins <rowan.collins@gmail.com> wrote:
> On 26 August 2021 21:20:35 BST, "Olle Härstedt" <olleharstedt@gmail.com> > wrote: > >The Foo class has to decide who to give access to, otherwise it's the > >same as public access. > > > It does decide who has access: any class that declares it as "delegated". > In exactly the same way, "protected" gives access to any class that > declares it as a "parent", and namespace visibility would give access to > any class that declares itself in the same namespace. > > None of those actually limit access to a named list of classes, but all of > them document an intended use, and catch mistakes where that intent isn't > followed. >
Well I wouldn't say they're quite the same. You can definitely limit the usages more with the "private" keyword. Or with declaring the class "final" for "protected" visibility, not allowing a "parent". I think something along these lines was asked. To have a way to decide the limitation by the original library, not by the one that decides to use it. Now... I also think that we might not necessarily need this. So far it looks like @internal works well enough. If a library is marking a class or method with @internal, it means it is not part of the public API and they might change it in the future. Static tool analysis like phpstan and psalm produce errors for using it. Also editors will warn you if you are using it. For all purposes you should treat it as private. If we really want the PHP runtime to produce a warning/error, even if this check makes sense mostly statically, we can think about a similar metadata with attributes with something like #[PackageInternal(namespacePrefix: 'Acme\Foo')] that can be applied to methods, classes and even properties and would trigger those warnings/errors when used from another namespace. This leaves the decision point on the library authors, not on the users of it. Regards, Alex

Mike Schinkel

5 years ago
> On Aug 26, 2021, at 9:47 AM, Olle Härstedt <olleharstedt@gmail.com> wrote: > > Don't know if this already exists, but maybe we could consider > brainstorming or gather alternatives for different types of > encapsulation when using composition? Currently, encapsulation like > private and protected is only possible when using inheritance - when > using composition, that is, class A is using (aggregating) class B, > your only alternative is public, so full visibility (or > getters/setters, which also is full visibility, since you can't limit > who's using those methods).
When you say "composition" I think of traits, but it does not appear that is what you mean? I have often wanted to be able to create a trait-private property so that I could write a `trait` and be confident that the classes that use it don't start using its internal properties and tie my hands in terms of being able to upgrade the trait to better internal approaches as I evolve the trait. Would trait-private address any part of what you need? Or would other trait-specific visibility address what you need? And if not, what about traits makes them not-applicable here? Maybe we could also address that? -Mike