Refringe's avatar

PHPStan - Morph Relationship with Interface and Trait

I have a MorphMany relationship defined in a trait, which has an interface and I'm struggling to add PHPStan docblocks that describe this relationship without error.

Here's the sort version of what I have:

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;
    }

Here's the current PHPStan error that I'm getting:

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

Any suggestions would be welcome.

0 likes
2 replies
LaryAI's avatar

The PHPStan error you’re seeing is due to how generics and template types are handled in PHPStan, especially with Eloquent relationships. The core of the issue is that PHPStan expects the second template parameter of MorphMany to be the concrete model class (e.g., App\Models\Mod), but your trait and interface use a template (TModel) which resolves to $this in the trait context. PHPStan cannot guarantee that $this is exactly App\Models\Mod due to how traits and generics interact.

Key points:

  • PHPStan’s Eloquent stubs define MorphMany<TRelated, TDeclaringModel>.
  • The second parameter (TDeclaringModel) should be the concrete model class, not $this or a template type.
  • Traits cannot enforce the concrete type, only the consuming class can.

Solution

1. Use the correct docblocks in the trait

In your trait, you can use static as the declaring model type in the docblock. This is the closest you can get to what PHPStan expects, as static will resolve to the consuming class.

/**
 * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Comment, static>
 */
public function comments(): MorphMany
{
    return $this->morphMany(Comment::class, 'commentable');
}

/**
 * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Comment, static>
 */
public function rootComments(): MorphMany
{
    return $this->morphMany(Comment::class, 'commentable')
        ->whereNull('parent_id')
        ->whereNull('root_id')
        ->orderBy('created_at', 'desc');
}

2. In the concrete model, override the docblocks

In your concrete model (Mod), you can override the docblocks to specify the exact types:

use App\Models\Comment;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Mod extends Model implements Commentable
{
    /** @use HasComments<self> */
    use HasComments;

    /**
     * @return MorphMany<Comment, Mod>
     */
    public function comments(): MorphMany
    {
        return parent::comments();
    }

    /**
     * @return MorphMany<Comment, Mod>
     */
    public function rootComments(): MorphMany
    {
        return parent::rootComments();
    }
}

3. (Optional) Suppress the error if needed

If you want to avoid duplicating the methods just for docblocks, you can suppress the PHPStan error with an inline ignore:

/** @phpstan-ignore-next-line */
use HasComments;

Or, in your phpstan.neon config, you can ignore this specific error if it’s not critical.


Summary

  • Use static as the declaring model type in trait docblocks.
  • Optionally, override the methods in the concrete class to specify the exact types for PHPStan.
  • This is a limitation of how PHPStan and PHP generics work with traits and Eloquent relationships.

References:


Final trait example:

/**
 * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Comment, static>
 */
public function comments(): MorphMany
{
    return $this->morphMany(Comment::class, 'commentable');
}

Final model example:

/**
 * @return \Illuminate\Database\Eloquent\Relations\MorphMany<\App\Models\Comment, \App\Models\Mod>
 */
public function comments(): MorphMany
{
    return parent::comments();
}

This should resolve the PHPStan error and provide correct type information.

Refringe's avatar

Adding an override for the purpose of static analysis seams dirty. I guess I'll just ignore the error. Huh.

Please or to participate in this conversation.