Should Nette DI support readonly variables in PHP 8.1?

David Grudl
Nette Core | 7540
+
0
-

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 | 21
+
+2
-

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 | 445
+
0
-

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 | 1202
+
+1
-

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 | 7540
+
0
-

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:
	-
		factory: Foo
		setup:
			- $readonlyProp = 123
Milo
Nette Core | 1202
+
+3
-

I understand.

I think when people will learn that public readonly is only shortcut for private and getter, injection will look strange.

Will see in few next years :-)

Slava.Aurim
Member | 18
+
0
-

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 | 501
+
0
-

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 | 18
+
0
-

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,
    ) {}
}