[Vote] New in initializers

php.internals

Nikita Popov

5 years ago
Hi internals, I've opened voting on https://wiki.php.net/rfc/new_in_initializers. Voting will close on 2021-07-14. Note that relative to the original RFC, new support is limited to parameter default values, attribute arguments, static variable initializers and global constant initializers, and not supported in property initializers and class constant initializers. The discussion thread https://externals.io/message/113347 has some extensive information on how we got here. Regards, Nikita

Nicolas Grekas

5 years ago
Hi NIkita, I've opened voting on https://wiki.php.net/rfc/new_in_initializers. Voting
> will close on 2021-07-14. > > Note that relative to the original RFC, new support is limited to parameter > default values, attribute arguments, static variable initializers and > global constant initializers, and not supported in property initializers > and class constant initializers. The discussion thread > https://externals.io/message/113347 has some extensive information on how > we got here. >
I voted yes and I'm happy this will come to PHP. I realized I still have one concern that I want to share here, related to attributes: The RFC breaks the possibility to parse the arguments of an attribute in a generic and safe way. What I mean is that right now, attributes can be inspected while the corresponding classes are not installed, due eg to a missing optional dependency. This behavior is what makes attributes truly declarative: one can ignore what they don't care about. Extra semantics can be carried out by classes without making the related attributes a mandatory dependency. I think there is a way to preserve this behavior and that we should look for it. If I may propose one: we might add a new ReflectionAttribute::getUninitializedArguments() method, that would return the same as ReflectionAttribute::getArguments(), except that it would put a ReflectionAttribute (or similar) instance in place of objects in the data structure. As a corollary, we might also want to enforce that only child classes of the Attribute class can be nested inside another Attribute (at least if we want to reuse ReflectionAttribute as a placeholder.) WDYT? Nicolas

Benjamin Eberlei

5 years ago
On Tue, Jul 6, 2021 at 9:31 AM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:
> Hi NIkita, > > I've opened voting on https://wiki.php.net/rfc/new_in_initializers. Voting > > will close on 2021-07-14. > > > > Note that relative to the original RFC, new support is limited to > parameter > > default values, attribute arguments, static variable initializers and > > global constant initializers, and not supported in property initializers > > and class constant initializers. The discussion thread > > https://externals.io/message/113347 has some extensive information on > how > > we got here. > > > > I voted yes and I'm happy this will come to PHP. > > I realized I still have one concern that I want to share here, related to > attributes: > The RFC breaks the possibility to parse the arguments of an attribute in a > generic and safe way. > What I mean is that right now, attributes can be inspected while the > corresponding classes are not installed, due eg to a missing optional > dependency. >
This is not 100% correct, you can have an attribte #[Foo(Foo::class)] and then calling ReflectionAttribute::getArguments would also require to resolve the type Foo. So this is not different than what could happen right now already. What you can do is use `ReflectionAttribute::getName()` to filter the attributes you want to work on and only then call getArguments().
> This behavior is what makes attributes truly declarative: one can ignore > what they don't care about. Extra semantics can be carried out by classes > without making the related attributes a mandatory dependency. > > I think there is a way to preserve this behavior and that we should look > for it. > > If I may propose one: we might add a new > ReflectionAttribute::getUninitializedArguments() method, that would return > the same as ReflectionAttribute::getArguments(), except that it would put a > ReflectionAttribute (or similar) instance in place of objects in the data > structure. As a corollary, we might also want to enforce that only child > classes of the Attribute class can be nested inside another Attribute (at > least if we want to reuse ReflectionAttribute as a placeholder.) >
A function like this could return all arguments that are not AST Nodes but "literals" (instances of scalar / array types). Foo::class or new Foo() are both AST Node types that are resolved the same way in `getArguments`.

Nicolas Grekas

5 years ago
> > I've opened voting on https://wiki.php.net/rfc/new_in_initializers. Voting >> > will close on 2021-07-14. >> > >> > Note that relative to the original RFC, new support is limited to >> parameter >> > default values, attribute arguments, static variable initializers and >> > global constant initializers, and not supported in property initializers >> > and class constant initializers. The discussion thread >> > https://externals.io/message/113347 has some extensive information on >> how >> > we got here. >> > >> >> I voted yes and I'm happy this will come to PHP. >> >> I realized I still have one concern that I want to share here, related to >> attributes: >> The RFC breaks the possibility to parse the arguments of an attribute in a >> generic and safe way. >> What I mean is that right now, attributes can be inspected while the >> corresponding classes are not installed, due eg to a missing optional >> dependency. >> > > This is not 100% correct, you can have an attribte #[Foo(Foo::class)] and > then calling ReflectionAttribute::getArguments would also require to > resolve the type Foo. So this is not different than what could happen right > now already. >
Right. Yet this can be easily worked around by using a string literal, so at least this issue can be avoided with a CS fixer when it matters. What you can do is use `ReflectionAttribute::getName()` to filter the
> attributes you want to work on and only then call getArguments(). >
Yes, my comment is about nested instances, not the root ones. #[Any(new Foo, new Bar)] <= Foo and Bar classes have to be defined, but this should be inspectable without instantiation, like root attributes.
> This behavior is what makes attributes truly declarative: one can ignore >> what they don't care about. Extra semantics can be carried out by classes >> without making the related attributes a mandatory dependency. >> >> I think there is a way to preserve this behavior and that we should look >> for it. >> >> If I may propose one: we might add a new >> ReflectionAttribute::getUninitializedArguments() method, that would return >> the same as ReflectionAttribute::getArguments(), except that it would put >> a >> ReflectionAttribute (or similar) instance in place of objects in the data >> structure. As a corollary, we might also want to enforce that only child >> classes of the Attribute class can be nested inside another Attribute (at >> least if we want to reuse ReflectionAttribute as a placeholder.) >> > > A function like this could return all arguments that are not AST Nodes but > "literals" (instances of scalar / array types). > Foo::class or new Foo() are both AST Node types that are resolved the same > way in `getArguments`. >
I guess so, but I'm not sure to get the exact point you want to make here :)

Rowan Collins

5 years ago
On 06/07/2021 11:31, Benjamin Eberlei wrote:
> This is not 100% correct, you can have an attribte #[Foo(Foo::class)] and > then calling ReflectionAttribute::getArguments would also require to > resolve the type Foo. So this is not different than what could happen right > now already.
Despite its name, "::class" doesn't care about class definitions, it just performs a string substitution based on the "namespace" and "use" statements in the current file. In most cases, that happens entirely at compile time, so the following two source files compile identically: Short form: namespace Somebody\Something; #[ Foo( Foo::class ) ] class Whatever {} Expanded form: namespace Somebody\Something; #[ \Somebody\Something\Foo( '\Somebody\Something\Foo' ) ] class Whatever {} There is no need for the class \Somebody\Something\Foo to actually exist in either case, the argument is just a string: https://3v4l.org/bgNa2#v8.0.8 Regards,
-- Rowan Tommins [IMSoP]

Benjamin Eberlei

5 years ago
On Tue, Jul 6, 2021 at 12:58 PM Rowan Tommins <rowan.collins@gmail.com> wrote:
> On 06/07/2021 11:31, Benjamin Eberlei wrote: > > This is not 100% correct, you can have an attribte #[Foo(Foo::class)] and > > then calling ReflectionAttribute::getArguments would also require to > > resolve the type Foo. So this is not different than what could happen > right > > now already. > > > Despite its name, "::class" doesn't care about class definitions, it > just performs a string substitution based on the "namespace" and "use" > statements in the current file. > > In most cases, that happens entirely at compile time, so the following > two source files compile identically: >
Hah, I realized after sending the example was bad :) I should have used an example using actual constants (vs magic ones): #[Foo(Foo::BAR)] This would trigger autoloading and resolving during getArguments()

Nicolas Grekas

5 years ago
> > > This is not 100% correct, you can have an attribte #[Foo(Foo::class)] > and > > > then calling ReflectionAttribute::getArguments would also require to > > > resolve the type Foo. So this is not different than what could happen > > right > > > now already. > > > > > > Despite its name, "::class" doesn't care about class definitions, it > > just performs a string substitution based on the "namespace" and "use" > > statements in the current file. > > > > In most cases, that happens entirely at compile time, so the following > > two source files compile identically: > > > > Hah, I realized after sending the example was bad :) I should have used an > example using actual constants (vs magic ones): > > #[Foo(Foo::BAR)] > > This would trigger autoloading and resolving during getArguments() >
Right! Extending on my proposal, getUninitializedArguments() could return a ReflectionConstant in place of such values.

Nikita Popov

5 years ago
On Tue, Jul 6, 2021 at 2:30 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:
> > > > This is not 100% correct, you can have an attribte #[Foo(Foo::class)] >> and >> > > then calling ReflectionAttribute::getArguments would also require to >> > > resolve the type Foo. So this is not different than what could happen >> > right >> > > now already. >> > >> > >> > Despite its name, "::class" doesn't care about class definitions, it >> > just performs a string substitution based on the "namespace" and "use" >> > statements in the current file. >> > >> > In most cases, that happens entirely at compile time, so the following >> > two source files compile identically: >> > >> >> Hah, I realized after sending the example was bad :) I should have used an >> example using actual constants (vs magic ones): >> >> #[Foo(Foo::BAR)] >> >> This would trigger autoloading and resolving during getArguments() >> > > > Right! > > Extending on my proposal, getUninitializedArguments() could return a > ReflectionConstant in place of such values. >
This doesn't extend to any more complex scenario: It's not just #[Foo(A::B)], it could also be #[Foo(A::FLAG_1 | A::FLAG_2)] and so on. The only way to do this is to go all the way back to Dmitry's attribute proposal (https://wiki.php.net/rfc/attributes) which allows fetching the AST of attribute arguments. That could represent arbitrary arguments without evaluating them. (In fact, that proposal also allowed attribute arguments that PHP cannot constant-evaluate at all.) I also think that viewing this as "nested attributes" is not quite the right way to think about it. Yes, the Assert\All use case is nested attributes, but that's just a special case. More generally this just allows you to use an object as an attribute argument, and that object does not necessarily have to be an attribute itself. To give a silly example, if we were to write argument and return types as attributes, you could have something like #[ReturnType(new IntersectionType(Foo::class, Bar::class))], where IntersectionType is just the representation of a particular type, but is not (and shouldn't be) an attribute itself. Regards, Nikita

Nicolas Grekas

5 years ago
Le mar. 6 juil. 2021 à 15:38, Nikita Popov <nikita.ppv@gmail.com> a écrit :
> On Tue, Jul 6, 2021 at 2:30 PM Nicolas Grekas < > nicolas.grekas+php@gmail.com> wrote: > >> >> > > This is not 100% correct, you can have an attribte #[Foo(Foo::class)] >>> and >>> > > then calling ReflectionAttribute::getArguments would also require to >>> > > resolve the type Foo. So this is not different than what could happen >>> > right >>> > > now already. >>> > >>> > >>> > Despite its name, "::class" doesn't care about class definitions, it >>> > just performs a string substitution based on the "namespace" and "use" >>> > statements in the current file. >>> > >>> > In most cases, that happens entirely at compile time, so the following >>> > two source files compile identically: >>> > >>> >>> Hah, I realized after sending the example was bad :) I should have used >>> an >>> example using actual constants (vs magic ones): >>> >>> #[Foo(Foo::BAR)] >>> >>> This would trigger autoloading and resolving during getArguments() >>> >> >> >> Right! >> >> Extending on my proposal, getUninitializedArguments() could return a >> ReflectionConstant in place of such values. >> > > This doesn't extend to any more complex scenario: It's not just > #[Foo(A::B)], it could also be #[Foo(A::FLAG_1 | A::FLAG_2)] and so on. The > only way to do this is to go all the way back to Dmitry's attribute > proposal (https://wiki.php.net/rfc/attributes) which allows fetching the > AST of attribute arguments. That could represent arbitrary arguments > without evaluating them. (In fact, that proposal also allowed attribute > arguments that PHP cannot constant-evaluate at all.) > > I also think that viewing this as "nested attributes" is not quite the > right way to think about it. Yes, the Assert\All use case is nested > attributes, but that's just a special case. More generally this just allows > you to use an object as an attribute argument, and that object does not > necessarily have to be an attribute itself. To give a silly example, if we > were to write argument and return types as attributes, you could have > something like #[ReturnType(new IntersectionType(Foo::class, Bar::class))], > where IntersectionType is just the representation of a particular type, but > is not (and shouldn't be) an attribute itself. >
Types are a good example of a structure that the language already knows how to parse without actually requiring all symbols to be loaded: Foo|Bar doesn't require having both types loaded to work. I think we don't need to account for constants in my scenario. ReflectionAttribute::getUninitializedArguments() would then only replace objects with uninitialized placeholders that represent their class and arguments. That should cover the need.

Nikita Popov

5 years ago
On Wed, Jun 30, 2021 at 9:55 AM Nikita Popov <nikita.ppv@gmail.com> wrote:
> Hi internals, > > I've opened voting on https://wiki.php.net/rfc/new_in_initializers. > Voting will close on 2021-07-14. > > Note that relative to the original RFC, new support is limited to > parameter default values, attribute arguments, static variable initializers > and global constant initializers, and not supported in property > initializers and class constant initializers. The discussion thread > https://externals.io/message/113347 has some extensive information on how > we got here. >
This RFC has been accepted with 43 votes in favor and 2 against. Regards, Nikita