Should Nette DI support readonly variables in PHP 8.1?
- David Grudl
- Nette Core | 8228
PHP 8.1 comes with an interesting new feature: readonly properties:
class Test
{
public readonly string $prop;
public function setProp(string $prop): void
{
$this->prop = $prop; // legal initialization
}
}
$test = new Test;
$test->setProp('abc');
echo $test->prop; // legal read
$test->prop = 'foo'; // throws exception: Cannot modify readonly property Test::$prop
Thus, once initialized, a variable cannot be overwritten by another value.
Surprisingly, assigning to $test->prop
throws an exception
even if the variable is not initialized:
$test = new Test;
$test->prop = 'foo';
// throws exception too: Cannot initialize readonly property Test::$prop from global scope
Even this one throws an exception:
class Child extends Test
{
public function __construct()
{
$this->prop = 'hello';
// throws exception: Cannot initialize readonly property Test::$prop from scope Child
}
}
I thought the readonly flag would be ideal for /** @inject */
properties, but since they cannot be written from outside, it cannot
be used.
… unless …
Unless the DI container bypasses it with a trick, which of course exist in PHP. The question is whether to teach it or not. Whether to simply allow the DI container to initialize public readonly variables when it requires a trick.
For the /** @inject */
ones it makes sense to me, on the other
hand it would be the first time DI would bypass the principles of the
language.
- Rick Strafy
- Nette Blogger | 81
That's very strange how they designed it, Kotlin has val
(something like readonly in php) or var
type of variable, when val
is used as class property, it needs to be initialized in constructor call or in
the definition itself, class just cannot be created with undefined
val
properties, and I expected readonly will do the the same in
PHP o.O
It would be cool to have #[Inject]
properties readonly, but
I don't know if it's worth to do it with hack, because if it's done, new PHP
version can bring some changes and this hack will no longer work. Another reason
would be that PHPStorm may show a warning that those properties were never set,
if not now then in newer versions, because it could analyze the code and find
out there is no possible way for that properties to be initialized.
Maybe another reason would be testing, lot of people use public injects only in base presenters, so I imagine in tests they somehow assign those presenter services (I never tested presenters before), and it would also needed to be assigned with that bypass.
So it would be nice, but it can bring more issues in the future.
Last edited by Rick Strafy (2021-10-05 18:39)
- Polki
- Member | 553
Since typed properties are there it does not make sense to me, because I can
be sure that variable has proper type.
When I use InjectMethod then everyone can call this method more then once so
right now that properties are not ReadOnly too.
So if I wanted to use ReadOnly property, then I used something like
this:
For PHP less than 8.1
private ?PropType $propName = null;
public function injectPropType(PropType $propName): void
{
if ($this->propName) {
throw new Exception();
}
$this->propName = $propName;
}
For PHP more than 8.0
private readonly PropType $propName;
public function injectPropClass(PropType $propName): void
{
$this->propName = $propName;
}
Otherwise I will always use
#[Inject] public PropType $propName;
Last edited by Polki (2021-10-05 18:53)
- Milo
- Nette Core | 1283
If I suppose we are talking about presenters only – in my apps it does not worth it.
Looks to me that readonly is addition to already defined public properties with #[Inject] attribute. But these are now easily replaceable with ctor promoted properties and such can be private and readonly legally.
In BasePresenters I use only final injectX() methods. Maybe there is a space for public readonly injection.
- David Grudl
- Nette Core | 8228
Actually, the point is that readonly is a flag that makes sense to use for all properties with dependencies passed by a constructor or inject method:
public function __construct(
private readonly PropType $propName,
) {
}
and
private readonly PropType $propName;
public function injectPropClass(PropType $propName): void
{
$this->propName = $propName;
}
So it would (seemingly) make sense to use it for #Inject
properties too. But … it will throw an exception.
So the question is whether to support this and allow it or not and simply state that the injected property must not be readonly, even if it is not (at first sight) logical.
If this is allowed, the second question arises. Should it be allowed even in
the case of manual initialization? For example, if the variable has
#Inject
attribute?
service:
-
create: Foo
setup:
- $readonlyProp = 123
- Slava.Aurim
- Member | 19
Nette has a very comfortable method for injecting dependencies into public properties with attribute [#Inject]. In my entities and base classes, I can use this attribute, and leave the constructor for the descendant classes. In this case, of course, I am worried that someone will be able to overwrite this service in a public property. And if it is possible to protect it from re-writing, it will be convenient.
Example:
class BaseEntity {
[#Inject]
public readonly BaseLogicService $service;
}
- Marek Bartoš
- Nette Blogger | 1275
PHP can solve this limitation with property accessors. I would prefer to not introduce more reflection or rebinding magic than necessary.
class Test
{
#[Inject]
public readonly Example $prop { public get; public set; };
}
Last edited by Marek Bartoš (2021-10-06 20:51)
- Slava.Aurim
- Member | 19
Property Accessors not implemented now in PHP, and appropriate PHP RFC: Property Accessors still under discussion.
As I see it, one of the most practical goals for readonly properties is to make a more elegant code by ridding the class of a lot of noise like banal getters and setters. As part of this goal, injecting into readonly properties by Nette DI will go very well together.
Example:
class PostImmutable
{
#[Inject]
public readonly LinkGenerator $linkGenerator;
public function __construct(
public readonly string $title,
public readonly Status $status,
public readonly ?DateTimeImmutable $publishedAt = null,
) {}
}