laradev99's avatar

PHPStan with relations in traits and interfaces

Hi!

I'm struggling with PHPStan (level 8).

I have an interface that defines a method that is a MorphMany relation, recipients(). I then have a trait that implements that method. I use that trait and interface on a few models. Here's a short version;

interface Alertee
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Recipient, $this>
     */
    public function recipients(): MorphMany;
}

trait IsAlertee
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Recipient, $this>
     */
    public function recipients(): MorphMany
    {
        return $this->morphMany(Recipient::class, 'alertee');
    }
}

class User extends Model implements Alertee
{
	use IsAlertee;
}

class Recipient extends Model
{

}

Having no docblock comments rightfully complains that the generic types are missing.

As you can see I've tried to use generics - but no mater what combination I try in the second parameter (TDeclaringModel) in the MorphMany docblock comment I get errors reported - I've tried various combinations of self and $this and can't figure it out.

If I remove the interface and trait and put the relation method directly on all models that should be using it, there are no errors.

What am I missing / not understanding?

0 likes
2 replies
tisuchi's avatar

@laradev99 Apply @template!

/**
 * @template TModel of \Illuminate\Database\Eloquent\Model
 */
interface Alertee
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Recipient, TModel>
     */
    public function recipients(): MorphMany;
}
use Illuminate\Database\Eloquent\Relations\MorphMany;

/**
 * @template TModel of \Illuminate\Database\Eloquent\Model
 * @mixin TModel
 */
trait IsAlertee
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Recipient, TModel>
     */
    public function recipients(): MorphMany
    {
        return $this->morphMany(Recipient::class, 'alertee');
    }
}
Refringe's avatar

@tisuchi I have a shockingly similar implementation and I'm running into this:

Method `App\Models\Mod::comments()` should return `Illuminate\Database\Eloquent\Relations\MorphMany<App\Models\Comment, App\Models\Mod>` but returns `Illuminate\Database\Eloquent\Relations\MorphMany<App\Models\Comment, $this(App\Models\Mod)>`.                                                          
         🪪 return.type  
         💡 Template type `TDeclaringModel` on class `Illuminate\Database\Eloquent\Relations\MorphMany` is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant

Am I missing something in the implementation?

namespace App\Contracts;

use Illuminate\Database\Eloquent\Relations\MorphMany;

/**
 * @template TModel of \Illuminate\Database\Eloquent\Model
 */
interface Commentable
{
    /**
     * Get all comments for this commentable model.
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Comment, TModel>
     */
    public function comments(): MorphMany;

    /**
     * Get all root comments for this commentable model.
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Comment, TModel>
     */
    public function rootComments(): MorphMany;
}
    /**
     * @implements Commentable<self>
     */
    class Mod extends Model implements Commentable
    {
        /** @use HasComments<self> */
        use HasComments;
    }

Please or to participate in this conversation.