Skip to content
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

Relation getQuery() return type loses model generic #2069

Open
rudiedirkx opened this issue Oct 13, 2024 · 10 comments
Open

Relation getQuery() return type loses model generic #2069

rudiedirkx opened this issue Oct 13, 2024 · 10 comments
Labels
bug Something isn't working

Comments

@rudiedirkx
Copy link

rudiedirkx commented Oct 13, 2024

  • Larastan Version: v2.9.8
  • Laravel Version: v11.23.5

Description

Phpstan understands the type of a model's relation query, but after getQuery() it loses the specific model/generic.

Laravel code where the issue was found

\PHPStan\dumpType($organization); // 🟩 App\Models\Organization
\PHPStan\dumpType($organization->site_checks()); // 🟩 Illuminate\Database\Eloquent\Relations\HasManyThrough<App\Models\SiteCheck>
\PHPStan\dumpType($organization->site_checks()->getQuery()); // 🟥 Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>

In the last dump, the model becomes an eloquent model instead of SiteCheck. Which is weird, because getQuery() in Laravel does have a return hint:

@return \Illuminate\Database\Eloquent\Builder<TRelatedModel>

and it still knew TRelatedModel the dump before.

I don't get it. I don't even know who's doing it wrong. Me, Larastan, Phpstan, or Laravel?

@rudiedirkx rudiedirkx added the bug Something isn't working label Oct 13, 2024
@rudiedirkx
Copy link
Author

rudiedirkx commented Oct 13, 2024

Eeeeh it's probably me, because somewhere else:

\PHPStan\dumpType($this->organization->sites()->getQuery()); // 🟩 Illuminate\Database\Eloquent\Builder<App\Models\Site>

It still knows about Site.

Maybe because my SiteCheck is also a generic, and my Organization::site_checks() doesn't specify that (because it doesn't know the specific):

// No @return here
public function site_checks() : Relations\HasManyThrough {
	return $this->hasManyThrough(SiteCheck::class, Site::class, 'organization_id', 'site_id');
}

@rudiedirkx
Copy link
Author

rudiedirkx commented Oct 13, 2024

But if I add a @return with the generic (Checked is still generic, because no idea which type it returns):

/** @return Relations\HasManyThrough<SiteCheck<Checked>> */
public function site_checks() : Relations\HasManyThrough {
	return $this->hasManyThrough(SiteCheck::class, Site::class, 'organization_id', 'site_id');
}

phpstan complains about its return value:

Method Organization::site_checks() should return Relations\HasManyThrough<SiteCheck<Checked>> but returns Relations\HasManyThrough<SiteCheck>.

because yeah kinda, because hasManyThrough() doesn't know about a potential generic. Is that Larastan's fault, because it doesn't take potential generics into account? How do I fix this?

@rudiedirkx
Copy link
Author

And with that @return the original problem holds: getQuery() loses the generic type SiteCheck.

@rudiedirkx
Copy link
Author

rudiedirkx commented Oct 13, 2024

Wow, generics are a lot of trouble with phpstan level 5+. Suddenly argument.type errors everywhere. Like these:

Parameter #1 $query of static method Watchlist::getChecks() expects Builder<SiteCheck<Checked>>, Builder<SiteCheck> given.

Come ooon! That's good enough!

@calebdw
Copy link
Contributor

calebdw commented Oct 14, 2024

Use the wildcard if you're not sure / don't care what the inner types will be:

/** @return Relations\HasManyThrough<SiteCheck<*>> */
public function site_checks() : Relations\HasManyThrough {
	return $this->hasManyThrough(SiteCheck::class, Site::class, 'organization_id', 'site_id');
}

@rudiedirkx
Copy link
Author

rudiedirkx commented Oct 14, 2024

That's good to know, but that doesn't solve any part of the problem. getQuery() still returns EloquentBuilder<EloquentModel> instead of EloquentBuilder<SiteCheck> (or even with generic: EloquentBuilder<SiteCheck<*>>). I think Larastan doesn't support generics in relationship return types. That's why sites() works (Site is not a generic), but site_checks() doesn't (SiteCheck is a generic). I think. The @return in

/** @return Relations\HasManyThrough<SiteCheck<*>> */
public function site_checks() : Relations\HasManyThrough {
	return $this->hasManyThrough(SiteCheck::class, Site::class, 'organization_id', 'site_id');
}

doesn't do/change anything. Larastan makes the return type Relations\HasManyThrough<SiteCheck> anyway. And then the generic is lost. And then getQuery() fails its own generic. Or something. I don't know Phpstan well enough.

@rudiedirkx
Copy link
Author

If I hack Larastan's ModelRelationsDynamicMethodReturnTypeExtension to skip Organization::site_checks() Phpstan will use the @return, and the first dumpType 'works':

Relations\HasManyThrough<SiteCheck<*>>

But that still doesn't fix the getQuery() type:

EloquentBuilder<EloquentModel>

It's still the base Model! Why doesn't it use SiteCheck or SiteCheck<*> ??

@calebdw
Copy link
Contributor

calebdw commented Oct 14, 2024

I'll take a look. Is that the only relation or does it do that for all relation types?

@rudiedirkx
Copy link
Author

rudiedirkx commented Oct 14, 2024

Omg I'm an idiot. Site also has a relationship to SiteCheck, and it works perfectly there. The problem isn't that SiteCheck is a generic, but the HasManyThrough relationship! Another through-relationship somewhere else has the same problem:

\PHPStan\dumpType($contact->sites()); // 🟩 Relations\HasManyThrough<Site>
\PHPStan\dumpType($contact->sites()->getQuery()); // 🟥 EloquentBuilder<EloquentModel>

Site is not a generic, but sites() is a HasManyThrough. That is the problem. I still don't know why, but that's progress 😆

@canvural
Copy link
Collaborator

@rudiedirkx Could you try the latest 2.x branch? It seems to be fixed cause I can't reproduce it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants