New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[PHP^8.2] Access to an undefined property when type hinting interface with @property
#10302
Comments
Hi, Why are you putting |
I'm using the interface for a type hint because you can't type-hint on traits. This is a Laravel project where I have an interface like this: /**
* @property-read Collection<JobBatch>|null $batches
* @property-read int|null $batches_count
* @property-read Collection<JobBatch>|null $running
* @property-read int|null $running_count
* @property-read bool $busy
* @mixin Model
*/
interface BatchAware
{
/**
* @return MorphToMany<JobBatch>
*/
public function batches(): MorphToMany;
/**
* @return MorphToMany<JobBatch>
*/
public function running(): MorphToMany;
/**
* @return Attribute<bool, void>
*/
public function busy(): Attribute;
} Which is implemented through a trait: /**
* @mixin BatchAware
*/
trait HasJobBatches
{
/**
* @return MorphToMany<JobBatch>
*/
public function batches(): MorphToMany
{
return $this->morphToMany(JobBatch::class, 'batchable');
}
/**
* @return MorphToMany<JobBatch>
*/
public function running(): MorphToMany
{
return $this->batches()
->whereNull('finished_at')
->whereNull('cancelled_at');
}
/**
* @return Attribute<bool, void>
*/
public function busy(): Attribute
{
return Attribute::make(
fn () => ($this->running_count ?? $this->loadCount('running')->running_count) > 0
);
}
} That marries up like this: class SomeModel extends Model implements BatchAware
{
use HasJobBatches;
// ...
} Then elsewhere I'm trying to add code that will handle any /**
* @param Collection<BatchAware>|array<BatchAware> $items
* @return string[]
*/
protected function batchIds(Collection|array $items): array
{
return collect($items)
->flatMap(fn (BatchAware $item) => $item->running->pluck('id')->all())
->unique()
->all();
} |
If you type |
Yes, it would, but the point is that I want to add this functionality to multiple models, then elsewhere I want to be able to run code on any model that happens to have this interface. I don't want to have to do something like this: /**
* @param Collection<SomeModel|AnotherModel|MoreModels|EvenMoreModels|AdditionalModels>|array<SomeModel|AnotherModel|MoreModels|EvenMoreModels|AdditionalModels> $items
* @return string[]
*/
protected function batchIds(Collection|array $items): array
{
return collect($items)
->flatMap(fn (SomeModel|AnotherModel|MoreModels|EvenMoreModels|AdditionalModels $item) => $item->running->pluck('id')->all())
->unique()
->all();
} The interface is used to indicate to the application/IDE that the specific properties will be available on any |
The problem is that a model can implement |
A question: if this class actually has the properties like class SomeModel extends Model implements BatchAware
{
use HasJobBatches;
// ...
} |
Yeah, I fully understand that, but the interface itself still makes some promises (namely it enforces methods that happen to return specific relations). Because we lack "true" generics obviously we can't guarantee that I haven't added PHP Can't guarantee these things to be 100% true, but within the domain of my application I'm confident enough that they are true, so it's a frustration that PHPStan won't trust my documentation. |
Through the "Magic" of Laravel (I am actually running Larastan for the added rules/coverage, but this is not a Laravel specific use case). In short, within Laravel, if you have a method that returns a Relation (such as class Model
{
protected array $attributes = [];
public function __get(string $name): mixed
{
if (isset($this->attributes[$name]) {
return $this->attributes[$name];
}
if (method_exists($this, $name)) {
$this->attributes[$name] = $this->$name()->get();
return $this->$name;
}
if (str_ends_with($name, '_count')) {
$relation = substr($name, 0, -6);
if (method_exists($this, relation)) {
$this->attributes[$name] = $this->$relation()->count();
return $this->$name;
}
}
}
} Obviously there's a lot more to it - e.g. reflection to ensure that the method actually returns a relation, etc. etc. etc., but ensuring that a methtod exists which returns a relation is enough vertainty needed to ensure that there are a number of properties "available". In this instance, the |
Having the exact same issue with larastan. Have you managed to make it work yet? |
Only with this :( // @phpstan-ignore-next-line see: https://github.com/phpstan/phpstan/issues/10302 |
Ah ok thats unfortunately not an option for me due to thousands of occurances. I was hopeing we could solve it more in line without ignoring with a |
Yes, I'm thinking I'd allow this for interfaces with |
In the future this will be possible to solve with |
@ondrejmirtes thank you, would this also work with included interfaces or traits from third party libs inside vendor? |
This is now possible to solve with Here's an example: https://phpstan.org/r/2dec4d59-482d-4894-ba05-0c2453e28f2b @mattvb91 If you have any questions please first try it out and then report if it doesn't work as expected. |
since psalm does not allow access to properties/methods based on the interface type, I just opened a feature request on psalm so we can discuss getting feature parity in this regard |
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Bug report
Similar to #8537 there is still an issue when a variable is type-hinted with an interface that has a
@property
, or@property-read
declaration. From PHP8.2 onwards PHPStan is unable to recognise these declarations on interfaces.Code snippet that reproduces the problem
https://phpstan.org/r/a199e998-8445-4590-9942-fe4b04a704bd
Expected output
No errors!
Did PHPStan help you today? Did it make you happy in any way?
No response
The text was updated successfully, but these errors were encountered: