Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Eloquent generics #54573

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

Closed
rcerljenko opened this issue Feb 12, 2025 · 11 comments
Closed

Eloquent generics #54573

rcerljenko opened this issue Feb 12, 2025 · 11 comments

Comments

@rcerljenko
Copy link

Laravel Version

11.42.0

PHP Version

8.4.3

Database Driver & Version

No response

Description

Hi,

With the latest update we started to receive these errors.

This scope is a part of the trait that any model can use.

public function scopeSearch(Builder $query, ?string $search = null): Builder
{
	return $query->whereHas('user', static function (Builder $query) use ($search): void {
		/**
		 * @var Builder<static> $query
		 */
		$query->search($search);
	});
}
PHPDoc tag @var with type Illuminate\Database\Eloquent\Builder<static(App\Models\Admin)> is not subtype of native type Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>.
- varTag.nativeType

Steps To Reproduce

Copy this code in a trait and use it on some model.

public function scopeSearch(Builder $query, ?string $search = null): Builder
{
	return $query->whereHas('user', static function (Builder $query) use ($search): void {
		/**
		 * @var Builder<static> $query
		 */
		$query->search($search);
	});
}
@rcerljenko
Copy link
Author

@calebdw you will probably have a good starting point on this

@rcerljenko
Copy link
Author

i suspect this is the related PR...

#54452

@calebdw
Copy link
Contributor

calebdw commented Feb 12, 2025

You need the generics on the input parameter:

/**
 * @param Builder<static> $query
 * @return Builder<static>
 */
public function scopeSearch(Builder $query, ?string $search = null): Builder
{
	return $query->whereHas('user', static function (Builder $query) use ($search): void {
		$query->search($search);
	});
}

@rcerljenko
Copy link
Author

Hi,

I did that but now it says this:

Call to an undefined method Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>::search().

@calebdw
Copy link
Contributor

calebdw commented Feb 12, 2025

I have a Larastan PR to be able to detect the dynamic generic type: larastan/larastan#2048

It hasn't been merged yet but I've also published it on my fork if you'd like to check it out: https://github.com/calebdw/larastan

@calebdw
Copy link
Contributor

calebdw commented Feb 12, 2025

A few more notes:

  • your type hint of Builder<static> inside the whereHas('user', ...) is incorrect---this is going to have type Builder<User> (whatever the specific model is)
  • parameter types are contravarient---that means you can only widen, not narrow---which is why you see the native type PHPStan warning. In this case that's fine to do so but you'll need to manually ignore the warning if you choose to
  • the linked PR ([11.x] feat: add better closure typing in QueriesRelationships #54452) takes advantage of relationships that are passed to the method and can figure out the proper inner generics. If you pass strings then there's no way to determine what the inner type is without the Larastan PR ([3.x] feat: support dynamic relation closures larastan/larastan#2048)

All that said, something like this should work out of the box

/**
 * @param Builder<static> $query
 * @return Builder<static>
 */
public function scopeSearch(Builder $query, ?string $search = null): Builder
{
    $user = $query->getModel()->user();

    return $query->whereHas($user, fn ($q) => $q->search($search));
}

@rcerljenko
Copy link
Author

Hi,

I'll try it out later today and let you know.. thx!

@rcerljenko
Copy link
Author

HI,

I tried the second approach that you mentioned and I managed to fix most of the problems. However, morph relations are still a problem.

Given the example:

public function item(): MorphTo
{
	return $this->morphTo();
}

public function scopeAvailable(Builder $query): Builder
{
	/**
	 * @var Builder<static> $query
	 */
	return $query->whereHas($query->getModel()->item(), static function (Builder $query): void {
		$query->available();
	});
}

scopeAvailable also exists on all morphed models via item relation. PHPstan returns this:

Call to an undefined method Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>::available().

It's the same when Builder generic is typed as a function input param.

@calebdw
Copy link
Contributor

calebdw commented Feb 12, 2025

MorphTo is inherently dynamic and there's no way to safely know what model is being returned. If you only have a few models then you could do something like:

/** @return MorphTo<Model1|Model2> */
public function item(): MorphTo
{
    return $this->morphTo();
}

however, you will have to manually ignore the warning that is produced by doing so.

You might have better mileage using the where*Morph* methods:

return $query->whereHasMorph($query->getModel()->item(), [Model1::class, Model2::class], static function (Builder $query): void {
    $query->available();
});

This however, will require the Larastan PR (or my fork) to be able to properly detect the builder type.

This is not a framework issue, but just has to do with properly typing the generics in your code. This issue can be closed but feel free to open discussions at the Larastan repo if you have more questions---I'm happy to help 😃

@rcerljenko
Copy link
Author

Thanks for the explanation. So you're saying that there's no way to properly type morphs for now?
I mean I could type all possible models as you said but that's not a solution for this.
I want to be able to properly type relations and not to worry about forgotten model typings on morph definitions.

@calebdw
Copy link
Contributor

calebdw commented Feb 12, 2025

Morphs are dynamic...how can you know what model it is and that is has the scope you're trying to call?

You can always just manually ignore the PHPStan warning with @phpstan-ignore

@laravel laravel locked and limited conversation to collaborators Feb 13, 2025
@crynobone crynobone converted this issue into discussion #54590 Feb 13, 2025

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants