phpstan: PHP8.2 - Interface property annotation not found inside class

Bug report

Similar to #8537, when a property is documented in an interface @property annotation, it’s not recognized by classes using this interface. This is not an issue on PHP 8.0 and 8.1.

Code snippet that reproduces the problem

https://phpstan.org/r/75fa6a47-cbc4-4eb3-b5eb-cf4851f6b805

Access to an undefined property SomeInterface::$var.

Expected output

No errors!

Did PHPStan help you today? Did it make you happy in any way?

PHPStan helped me become a better PHP dev 😃

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 3
  • Comments: 24 (7 by maintainers)

Commits related to this issue

Most upvoted comments

The hack @JasonEleventeen applied should be unnecessary.

I am in a similar situation that interfaces are annotated with @property for Eloquent models in Laravel. Adding the properties are very useful when writing packages for models that can be replaced with your own implementation but need to abide to the interface.

Applying the @mixin \Eloquent to the interface should inherit the magic methods __set and _get from the Eloquent model for the interface.

The documentation indicates that the annotated properties are detected if these magic methods are present: Have __get and/or __set magic methods for handling dynamic properties.

Therefore, I expect it to work now with the mixin applied.

However, it still does not work. Applying the hack from @JasonEleventeen does fix it though. Despite the Eloquent model also implementing the magic method __isset.

Furthermore, it is not detecting any magic properties from the @mixin \Eloquent model anymore either. An example is that it no longer understands what ->exists or ->wasRecentlyCreated is anymore when accessing these properties.

This seems more like a bug than a feature request. @ondrejmirtes

Example code.

/**
 * @property-read Collection<int,Statusable> $statuses
 *
 * @mixin \Eloquent */
interface StatusableInterface
{
    public function statuses(): HasMany;
}

Access to an undefined property App\Interfaces\StatusableInterface::$statuses

Adding #[\AllowDynamicProperties] works for interfaces as well as shown here: https://phpstan.org/r/ecc5a472-e2a3-4c8a-a314-51124b72edee although I don’t know how to feel about this given

it doesn’t mean that all interfaces should declare this property

I guess it would be good if phpstan would report if indeed given property was defined on the interface but is not defined on the class, ie if https://phpstan.org/r/57b63210-3875-4f28-871c-cf97d91ef779 would error


edit: apologies - idk if I was running 8.2 or not, but today I’m getting Fatal error: Cannot apply #[AllowDynamicProperties] to interface; I don’t know why that’s not happening on share link

This is now possible to solve with @phpstan-require-extends and @phpstan-require-implements.

See an example: https://phpstan.org/r/2dec4d59-482d-4894-ba05-0c2453e28f2b

When the typehinted interface is implemented by a class, it requires the class to extend a class in @phpstan-require-extends.

The class can then declare the property as native or via other means like @property.

This is fixable by patching https://github.com/phpstan/phpstan-src/blob/19c838bbe9576c83c95ebb1f9fedd5e5b8664306/src/Reflection/ClassReflection.php#L380

    public function hasProperty(string $propertyName): bool
{
	if ($this->isEnum()) {
		return $this->hasNativeProperty($propertyName);
	}

	foreach ($this->propertiesClassReflectionExtensions as $i => $extension) {
		if ($i > 0 && (!$this->allowsDynamicPropertiesExtensions() && !$this->isInterface())) {
			continue;
		}
		if ($extension->hasProperty($this, $propertyName)) {
			return true;
		}
	}

	return false;
}

would a PR to allow this be accepted? Could put it behind a feature Flag if required

@solomonjames please open a PR in the vendor lib for that change too if you can and didn’t do already 😊

Considering my case is mostly Database Models, I could use the Universal object crates, add each model Interface in my config and the error would be suppressed. However, this isn’t a perfect solution. If my Foo interface only declare @property int $id, and “Universal object crates” is used, $foo->bar won’t throw an exception (I can’t add a playground to demonstrate this because config can’t be declared on Playground). It’s true because of how Laravel define its setter, assigning a dynamic var to the implementing class won’t throw an error on the PHP execution. However, PHPStan should still obey the interface declaration when using $foo->bar.

No other solution provided by the new blog post seams applicable in this case. As @eithed noted, AllowDynamicProperties is not an option, as PHP return this error when dealing with Interfaces (and PHPStan should probably too) :

PHP Fatal error:  Cannot apply #[AllowDynamicProperties] to interface

Consider this other example :

This is still an issue IMO. On the new blog post, in Add @property PHPDoc, a warning about this issue should be added too.

I’m hesitant to support this, because @property above an interface doesn’t mean anything - it doesn’t mean that all interfaces should declare this property…