Repository: laravel-shift/blueprint (php)

Issue:
Many to Many Polymorphic
<!-- PLEASE COMPLETE THIS TEMPLATE -->

### Synopsis:
I would like to be able to dynamic generate many to many polymorphic relationships using blueprint. For example: Posts and Images have multiple Tags.

### Proposed Syntax:
```
models:
  Post:
    relationships:
    morphedToMany: Taggable

  Image:
    relationships:
    morphedToMany: Taggable

  Tag:
    relationships:
    morphedByMany: Taggable
```

### Expected Behavior:
Generates
- Models: Post, Image, Tag
- with relationships
- tables: posts, images, tags, taggables


Pull request description:
Resolves #482 Many to Many Polymorphic
Adds the missing relationships and the relevant intermediate table. Follows the convention of singular table name + 'able'.

Patch:
diff --git a/src/Generators/MigrationGenerator.php b/src/Generators/MigrationGenerator.php
index c281e2e..77a0e3a 100644
--- a/src/Generators/MigrationGenerator.php
+++ b/src/Generators/MigrationGenerator.php
@@ -59,7 +59,7 @@ class MigrationGenerator implements Generator
 
     public function output(Tree $tree, $overwrite = false): array
     {
-        $tables = ['tableNames' => [], 'pivotTableNames' => []];
+        $tables = ['tableNames' => [], 'pivotTableNames' => [], 'polymorphicManyToManyTables' => []];
 
         $stub = $this->filesystem->stub('migration.stub');
         /**
@@ -74,6 +74,12 @@ class MigrationGenerator implements Generator
                     $tables['pivotTableNames'][$pivotTableName] = $this->populatePivotStub($stub, $pivotSegments);
                 }
             }
+
+            if (!empty($model->polymorphicManyToManyTables())) {
+                foreach ($model->polymorphicManyToManyTables() as $tableName) {
+                    $tables['polymorphicManyToManyTables'][Str::lower(Str::plural(Str::singular($tableName).'able'))] = $this->populatePolyStub($stub, $tableName);
+                }
+            }
         }
 
         return $this->createMigrations($tables, $overwrite);
@@ -89,7 +95,7 @@ class MigrationGenerator implements Generator
         $output = [];
 
         $sequential_timestamp = \Carbon\Carbon::now()->copy()->subSeconds(
-            collect($tables['tableNames'])->merge($tables['pivotTableNames'])->count()
+            collect($tables['tableNames'])->merge($tables['pivotTableNames'])->merge($tables['polymorphicManyToManyTables'])->count()
         );
 
         foreach ($tables['tableNames'] as $tableName => $data) {
@@ -106,6 +112,14 @@ class MigrationGenerator implements Generator
 
             $output[$action][] = $path;
         }
+
+        foreach ($tables['polymorphicManyToManyTables'] as $tableName => $data) {
+            $path = $this->getTablePath($tableName, $sequential_timestamp->addSecond(), $overwrite);
+            $action = $this->filesystem->exists($path) ? 'updated' : 'created';
+            $this->filesystem->put($path, $data);
+            $output[$action][] = $path;
+        }
+
         return $output;
     }
 
@@ -116,7 +130,7 @@ class MigrationGenerator implements Generator
         $stub = str_replace('{{ definition }}', $this->buildDefinition($model), $stub);
 
         if (Blueprint::useReturnTypeHints()) {
-            $stub =  str_replace(['up()','down()'], ['up(): void','down(): void'], $stub);
+            $stub = str_replace(['up()', 'down()'], ['up(): void', 'down(): void'], $stub);
         }
 
         if ($this->hasForeignKeyConstraints) {
@@ -139,6 +153,23 @@ class MigrationGenerator implements Generator
         return $stub;
     }
 
+    protected function populatePolyStub(string $stub, string $parentTable)
+    {
+        $stub = str_replace('{{ class }}', $this->getPolyClassName($parentTable), $stub);
+        $stub = str_replace('{{ table }}', $this->getPolyTableName($parentTable), $stub);
+        $stub = str_replace('{{ definition }}', $this->buildPolyTableDefinition($parentTable), $stub);
+
+        if (Blueprint::useReturnTypeHints()) {
+            $stub = str_replace(['up()', 'down()'], ['up(): void', 'down(): void'], $stub);
+        }
+
+        if ($this->hasForeignKeyConstraints) {
+            $stub = $this->disableForeignKeyConstraints($stub);
+        }
+
+        return $stub;
+    }
+
     protected function buildDefinition(Model $model)
     {
         $definition = '';
@@ -286,6 +317,27 @@ class MigrationGenerator implements Generator
         return trim($definition);
     }
 
+    protected function buildPolyTableDefinition(string $parentTable)
+    {
+        $definition = '';
+
+        $references = 'id';
+        $on = Str::lower(Str::plural($parentTable));
+        $foreign = Str::lower(Str::singular($parentTable)) . '_' . $references;
+
+        if (config('blueprint.use_constraints')) {
+            $this->hasForeignKeyConstraints = true;
+            $definition .= $this->buildForeignKey($foreign, $on, 'id') . ';' . PHP_EOL;
+        } else {
+            $definition .= self::INDENT . '$table->foreignId(\'' . $foreign . '\');' . PHP_EOL;
+        }
+
+        $definition .= self::INDENT . sprintf('$table->unsignedBigInteger(\'%s\');', Str::lower(Str::singular($parentTable).'able' . '_id')) . PHP_EOL;
+        $definition .= self::INDENT . sprintf('$table->string(\'%s\');', Str::lower(Str::singular($parentTable).'able' . '_type')) . PHP_EOL;
+
+        return trim($definition);
+    }
+
     protected function buildForeignKey(string $column_name, ?string $on, string $type, array $attributes = [], array $modifiers = [])
     {
         if (is_null($on)) {
@@ -409,6 +461,11 @@ class MigrationGenerator implements Generator
         return 'Create' . Str::studly($this->getPivotTableName($segments)) . 'Table';
     }
 
+    protected function getPolyClassName(string $parentTable)
+    {
+        return 'Create' . Str::studly($this->getPolyTableName($parentTable)) . 'Table';
+    }
+
     protected function getPivotTableName(array $segments)
     {
         $isCustom = collect($segments)
@@ -435,6 +492,11 @@ class MigrationGenerator implements Generator
         return strtolower(implode('_', $segments));
     }
 
+    protected function getPolyTableName(string $parentTable)
+    {
+        return Str::plural(Str::lower(Str::singular($parentTable) . 'able'));
+    }
+
     private function shouldAddForeignKeyConstraint(\Blueprint\Models\Column $column)
     {
         if ($column->name() === 'id') {
diff --git a/src/Generators/ModelGenerator.php b/src/Generators/ModelGenerator.php
index 35102d7..db913a0 100644
--- a/src/Generators/ModelGenerator.php
+++ b/src/Generators/ModelGenerator.php
@@ -209,12 +209,15 @@ class ModelGenerator implements Generator
                 }
 
                 $fqcn = Str::startsWith($fqcn, '\\') ? $fqcn : '\\' . $fqcn;
+                $fqcn = Str::is($fqcn, "\\{$model->fullyQualifiedNamespace()}\\{$class_name}") ? $class_name : $fqcn;
 
                 if ($type === 'morphTo') {
                     $relationship = sprintf('$this->%s()', $type);
-                } elseif ($type === 'morphMany' || $type === 'morphOne') {
+                } elseif (in_array($type, ['morphMany', 'morphOne', 'morphToMany'])) {
                     $relation = Str::lower($is_model_fqn ? Str::singular(Str::afterLast($column_name, '\\')) : Str::singular($column_name)) . 'able';
                     $relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, $fqcn, $relation);
+                } elseif ($type === 'morphedByMany') {
+                    $relationship = sprintf('$this->%s(%s::class, \'%sable\')', $type, $fqcn, strtolower($model->name()));
                 } elseif (!is_null($key)) {
                     $relationship = sprintf('$this->%s(%s::class, \'%s\', \'%s\')', $type, $fqcn, $column_name, $key);
                 } elseif (!is_null($class) && $type === 'belongsToMany') {
@@ -226,7 +229,7 @@ class ModelGenerator implements Generator
 
                 if ($type === 'morphTo') {
                     $method_name = Str::lower($class_name);
-                } elseif (in_array($type, ['hasMany', 'belongsToMany', 'morphMany'])) {
+                } elseif (in_array($type, ['hasMany', 'belongsToMany', 'morphMany', 'morphToMany', 'morphedByMany'])) {
                     $method_name = Str::plural($is_model_fqn ? Str::afterLast($column_name, '\\') : $column_name);
                 }
 
diff --git a/src/Lexers/ModelLexer.php b/src/Lexers/ModelLexer.php
index baf268f..a02f6f1 100644
--- a/src/Lexers/ModelLexer.php
+++ b/src/Lexers/ModelLexer.php
@@ -18,6 +18,8 @@ class ModelLexer implements Lexer
         'morphone' => 'morphOne',
         'morphmany' => 'morphMany',
         'morphto' => 'morphTo',
+        'morphtomany' => 'morphToMany',
+        'morphedbymany' => 'morphedByMany',
     ];
 
     private static $dataTypes = [
diff --git a/src/Models/Model.php b/src/Models/Model.php
index 9d99832..41ed59d 100644
--- a/src/Models/Model.php
+++ b/src/Models/Model.php
@@ -15,6 +15,7 @@ class Model
     private $columns = [];
     private $relationships = [];
     private $pivotTables = [];
+    private $polymorphicManyToManyTables = [];
     private $indexes = [];
 
     /**
@@ -149,10 +150,18 @@ class Model
         if ($type === 'belongsToMany') {
             $this->addPivotTable($reference);
         }
+        if ($type === 'morphedByMany') {
+            $this->addPolymorphicManyToManyTable(Str::studly($this->tableName()));
+        }
 
         $this->relationships[$type][] = $reference;
     }
 
+    public function addPolymorphicManyToManyTable(string $reference)
+    {
+        $this->polymorphicManyToManyTables[] = class_basename($reference);
+    }
+
     public function addPivotTable(string $reference)
     {
         $segments = [$this->name(), class_basename($reference)];
@@ -175,6 +184,11 @@ class Model
         return $this->pivotTables;
     }
 
+    public function polymorphicManyToManyTables()
+    {
+        return $this->polymorphicManyToManyTables;
+    }
+
     public function setMorphTo(string $reference)
     {
         $this->morphTo = $reference;


Test patch:
diff --git a/tests/Feature/Generators/MigrationGeneratorTest.php b/tests/Feature/Generators/MigrationGeneratorTest.php
index b1bf9d5..5518454 100644
--- a/tests/Feature/Generators/MigrationGeneratorTest.php
+++ b/tests/Feature/Generators/MigrationGeneratorTest.php
@@ -409,6 +409,34 @@ class MigrationGeneratorTest extends TestCase
         $this->assertEquals(['created' => [$model_migration, $pivot_migration]], $this->subject->output($tree));
     }
 
+    /**
+     * @test
+     */
+    public function output_also_creates_many_to_many_polymorphic_intermediate_table_migration()
+    {
+        $this->filesystem->expects('stub')
+            ->with('migration.stub')
+            ->andReturn($this->stub('migration.stub'));
+
+        $now = Carbon::now();
+        Carbon::setTestNow($now);
+
+        $model_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_tags_table.php');
+        $poly_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_tagables_table.php');
+
+        $this->filesystem->expects('exists')->twice()->andReturn(false);
+
+        $this->filesystem->expects('put')
+            ->with($model_migration, $this->fixture('migrations/morphed-by-many.php'));
+        $this->filesystem->expects('put')
+            ->with($poly_migration, $this->fixture('migrations/morphed-by-many-intermediate.php'));
+
+        $tokens = $this->blueprint->parse($this->fixture('drafts/morphed-by-many.yaml'));
+        $tree = $this->blueprint->analyze($tokens);
+
+        $this->assertEquals(['created' => [$model_migration, $poly_migration]], $this->subject->output($tree));
+    }
+
     /**
      * @test
      */
diff --git a/tests/Feature/Generators/ModelGeneratorTest.php b/tests/Feature/Generators/ModelGeneratorTest.php
index 2f0ca9c..25424d6 100644
--- a/tests/Feature/Generators/ModelGeneratorTest.php
+++ b/tests/Feature/Generators/ModelGeneratorTest.php
@@ -269,6 +269,52 @@ class ModelGeneratorTest extends TestCase
         $this->assertEquals(['created' => ['app/Post.php', 'app/User.php', 'app/Image.php']], $this->subject->output($tree));
     }
 
+    /**
+     * @test
+     */
+    public function output_generates_morphtomany_relationship_with_intermediate_models()
+    {
+        $this->filesystem->expects('stub')
+            ->with($this->modelStub)
+            ->andReturn($this->stub($this->modelStub));
+        $this->filesystem->expects('stub')
+            ->times(3)
+            ->with('model.fillable.stub')
+            ->andReturn($this->stub('model.fillable.stub'));
+
+        $this->filesystem->expects('stub')
+            ->times(3)
+            ->with('model.casts.stub')
+            ->andReturn($this->stub('model.casts.stub'));
+        $this->filesystem->expects('stub')
+            ->times(3)
+            ->with('model.method.stub')
+            ->andReturn($this->stub('model.method.stub'));
+
+        $this->filesystem->expects('exists')
+            ->with('app')
+            ->andReturnTrue();
+        $this->filesystem->expects('put')
+            ->with('app/Post.php', $this->fixture('models/post-many-to-many-polymorphic-relationship.php'));
+
+        $this->filesystem->expects('exists')
+            ->with('app')
+            ->andReturnTrue();
+        $this->filesystem->expects('put')
+            ->with('app/Video.php', $this->fixture('models/video-many-to-many-polymorphic-relationship.php'));
+
+        $this->filesystem->expects('exists')
+            ->with('app')
+            ->andReturnTrue();
+        $this->filesystem->expects('put')
+            ->with('app/Tag.php', $this->fixture('models/tag-many-to-many-polymorphic-relationship.php'));
+
+        $tokens = $this->blueprint->parse($this->fixture('drafts/many-to-many-polymorphic-relationships.yaml'));
+        $tree = $this->blueprint->analyze($tokens);
+
+        $this->assertEquals(['created' => ['app/Post.php', 'app/Video.php', 'app/Tag.php']], $this->subject->output($tree));
+    }
+
     /**
      * @test
      */
diff --git a/tests/fixtures/drafts/many-to-many-polymorphic-relationships.yaml b/tests/fixtures/drafts/many-to-many-polymorphic-relationships.yaml
new file mode 100644
index 0000000..6c62b7c
--- /dev/null
+++ b/tests/fixtures/drafts/many-to-many-polymorphic-relationships.yaml
@@ -0,0 +1,15 @@
+models:
+  Post:
+    name: string
+    relationships:
+      morphToMany: Tag
+
+  Video:
+    name: string
+    relationships:
+      morphToMany: Tag
+
+  Tag:
+    name: string
+    relationships:
+      morphedByMany: Post, Video
diff --git a/tests/fixtures/drafts/morphed-by-many.yaml b/tests/fixtures/drafts/morphed-by-many.yaml
new file mode 100644
index 0000000..863c19c
--- /dev/null
+++ b/tests/fixtures/drafts/morphed-by-many.yaml
@@ -0,0 +1,5 @@
+models:
+  Tag:
+    name: string
+    relationships:
+      morphedByMany: Video
diff --git a/tests/fixtures/migrations/morphed-by-many-intermediate.php b/tests/fixtures/migrations/morphed-by-many-intermediate.php
new file mode 100644
index 0000000..1a104c4
--- /dev/null
+++ b/tests/fixtures/migrations/morphed-by-many-intermediate.php
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateTagablesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('tagables', function (Blueprint $table) {
+            $table->foreignId('tag_id');
+            $table->unsignedBigInteger('tagable_id');
+            $table->string('tagable_type');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('tagables');
+    }
+}
diff --git a/tests/fixtures/migrations/morphed-by-many.php b/tests/fixtures/migrations/morphed-by-many.php
new file mode 100644
index 0000000..3cc70b0
--- /dev/null
+++ b/tests/fixtures/migrations/morphed-by-many.php
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateTagsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('tags', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('tags');
+    }
+}
diff --git a/tests/fixtures/models/alias-relationships.php b/tests/fixtures/models/alias-relationships.php
index dc33026..2c671a9 100644
--- a/tests/fixtures/models/alias-relationships.php
+++ b/tests/fixtures/models/alias-relationships.php
@@ -29,16 +29,16 @@ class Salesman extends Model
 
     public function lead()
     {
-        return $this->hasOne(\App\User::class);
+        return $this->hasOne(User::class);
     }
 
     public function methodNames()
     {
-        return $this->hasMany(\App\ClassName::class);
+        return $this->hasMany(ClassName::class);
     }
 
     public function methodName()
     {
-        return $this->belongsTo(\App\ClassName::class);
+        return $this->belongsTo(ClassName::class);
     }
 }
diff --git a/tests/fixtures/models/certificate-pascal-case-example.php b/tests/fixtures/models/certificate-pascal-case-example.php
index 3b37a51..e15d9b1 100644
--- a/tests/fixtures/models/certificate-pascal-case-example.php
+++ b/tests/fixtures/models/certificate-pascal-case-example.php
@@ -36,6 +36,6 @@ class Certificate extends Model
 
     public function certificateType()
     {
-        return $this->belongsTo(\App\CertificateType::class);
+        return $this->belongsTo(CertificateType::class);
     }
 }
diff --git a/tests/fixtures/models/certificate-type-pascal-case-example.php b/tests/fixtures/models/certificate-type-pascal-case-example.php
index f5f2fe4..51a22f2 100644
--- a/tests/fixtures/models/certificate-type-pascal-case-example.php
+++ b/tests/fixtures/models/certificate-type-pascal-case-example.php
@@ -29,6 +29,6 @@ class CertificateType extends Model
 
     public function certificates()
     {
-        return $this->hasMany(\App\Certificate::class);
+        return $this->hasMany(Certificate::class);
     }
 }
diff --git a/tests/fixtures/models/custom-pivot-table-name.php b/tests/fixtures/models/custom-pivot-table-name.php
index 8a2a9c4..91c7e33 100644
--- a/tests/fixtures/models/custom-pivot-table-name.php
+++ b/tests/fixtures/models/custom-pivot-table-name.php
@@ -38,6 +38,6 @@ class User extends Model
 
     public function accounts()
     {
-        return $this->belongsToMany(\App\Account::class, 'test');
+        return $this->belongsToMany(Account::class, 'test');
     }
 }
diff --git a/tests/fixtures/models/foreign-key-shorthand-phpdoc.php b/tests/fixtures/models/foreign-key-shorthand-phpdoc.php
index dc69056..1ec483d 100644
--- a/tests/fixtures/models/foreign-key-shorthand-phpdoc.php
+++ b/tests/fixtures/models/foreign-key-shorthand-phpdoc.php
@@ -45,7 +45,7 @@ class Comment extends Model
      */
     public function post()
     {
-        return $this->belongsTo(\App\Post::class);
+        return $this->belongsTo(Post::class);
     }
 
     /**
@@ -53,7 +53,7 @@ class Comment extends Model
      */
     public function author()
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 
     /**
@@ -61,6 +61,6 @@ class Comment extends Model
      */
     public function country()
     {
-        return $this->belongsTo(\App\Country::class, 'ccid', 'code');
+        return $this->belongsTo(Country::class, 'ccid', 'code');
     }
 }
diff --git a/tests/fixtures/models/model-configured.php b/tests/fixtures/models/model-configured.php
index b47f8bd..2251d05 100644
--- a/tests/fixtures/models/model-configured.php
+++ b/tests/fixtures/models/model-configured.php
@@ -32,11 +32,11 @@ class Comment extends Model
 
     public function post()
     {
-        return $this->belongsTo(\Some\App\Models\Post::class);
+        return $this->belongsTo(Post::class);
     }
 
     public function author()
     {
-        return $this->belongsTo(\Some\App\Models\User::class);
+        return $this->belongsTo(User::class);
     }
 }
diff --git a/tests/fixtures/models/model-relationships-morphone-morphmany-with-fqn.php b/tests/fixtures/models/model-relationships-morphone-morphmany-with-fqn.php
index 78f0580..50ff7e2 100644
--- a/tests/fixtures/models/model-relationships-morphone-morphmany-with-fqn.php
+++ b/tests/fixtures/models/model-relationships-morphone-morphmany-with-fqn.php
@@ -45,6 +45,6 @@ class Flag extends Model
 
     public function user()
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 }
diff --git a/tests/fixtures/models/model-relationships-with-full-namespace.php b/tests/fixtures/models/model-relationships-with-full-namespace.php
index ac8170e..c8c00d0 100644
--- a/tests/fixtures/models/model-relationships-with-full-namespace.php
+++ b/tests/fixtures/models/model-relationships-with-full-namespace.php
@@ -51,11 +51,11 @@ class Recurrency extends Model
 
     public function user()
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 
     public function product()
     {
-        return $this->belongsTo(\App\Product::class);
+        return $this->belongsTo(Product::class);
     }
 }
diff --git a/tests/fixtures/models/model-relationships.php b/tests/fixtures/models/model-relationships.php
index 859ecb8..2518744 100644
--- a/tests/fixtures/models/model-relationships.php
+++ b/tests/fixtures/models/model-relationships.php
@@ -31,31 +31,31 @@ class Subscription extends Model
 
     public function teams()
     {
-        return $this->belongsToMany(\App\Team::class);
+        return $this->belongsToMany(Team::class);
     }
 
     public function orders()
     {
-        return $this->hasMany(\App\Order::class);
+        return $this->hasMany(Order::class);
     }
 
     public function duration()
     {
-        return $this->hasOne(\App\Duration::class);
+        return $this->hasOne(Duration::class);
     }
 
     public function transaction()
     {
-        return $this->hasOne(\App\Transaction::class);
+        return $this->hasOne(Transaction::class);
     }
 
     public function user()
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 
     public function product()
     {
-        return $this->belongsTo(\App\Product::class);
+        return $this->belongsTo(Product::class);
     }
 }
diff --git a/tests/fixtures/models/nested-models.php b/tests/fixtures/models/nested-models.php
index 696e762..1a127d5 100644
--- a/tests/fixtures/models/nested-models.php
+++ b/tests/fixtures/models/nested-models.php
@@ -34,7 +34,7 @@ class ScreeningQuestion extends Model
 
     public function report()
     {
-        return $this->belongsTo(\App\Models\Screening\Report::class);
+        return $this->belongsTo(Report::class);
     }
 
     public function appointmentType()
diff --git a/tests/fixtures/models/post-many-to-many-polymorphic-relationship.php b/tests/fixtures/models/post-many-to-many-polymorphic-relationship.php
new file mode 100644
index 0000000..7a96590
--- /dev/null
+++ b/tests/fixtures/models/post-many-to-many-polymorphic-relationship.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Post extends Model
+{
+    use HasFactory;
+
+    /**
+     * The attributes that are mass assignable.
+     *
+     * @var array
+     */
+    protected $fillable = [
+        'name',
+    ];
+
+    /**
+     * The attributes that should be cast to native types.
+     *
+     * @var array
+     */
+    protected $casts = [
+        'id' => 'integer',
+    ];
+
+    public function tags()
+    {
+        return $this->morphToMany(\App\Tag::class, 'tagable');
+    }
+}
diff --git a/tests/fixtures/models/post-polymorphic-relationship.php b/tests/fixtures/models/post-polymorphic-relationship.php
index 3bf515b..fba8617 100644
--- a/tests/fixtures/models/post-polymorphic-relationship.php
+++ b/tests/fixtures/models/post-polymorphic-relationship.php
@@ -29,6 +29,6 @@ class Post extends Model
 
     public function images()
     {
-        return $this->morphMany(\App\Image::class, 'imageable');
+        return $this->morphMany(Image::class, 'imageable');
     }
 }
diff --git a/tests/fixtures/models/readme-example-phpdoc.php b/tests/fixtures/models/readme-example-phpdoc.php
index 5a02d9b..ca0f876 100644
--- a/tests/fixtures/models/readme-example-phpdoc.php
+++ b/tests/fixtures/models/readme-example-phpdoc.php
@@ -46,6 +46,6 @@ class Post extends Model
      */
     public function author()
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 }
diff --git a/tests/fixtures/models/readme-example.php b/tests/fixtures/models/readme-example.php
index bc6312d..34d8f13 100644
--- a/tests/fixtures/models/readme-example.php
+++ b/tests/fixtures/models/readme-example.php
@@ -34,6 +34,6 @@ class Post extends Model
 
     public function author()
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 }
diff --git a/tests/fixtures/models/relationships-phpdoc.php b/tests/fixtures/models/relationships-phpdoc.php
index c9ffb39..24ede25 100644
--- a/tests/fixtures/models/relationships-phpdoc.php
+++ b/tests/fixtures/models/relationships-phpdoc.php
@@ -42,7 +42,7 @@ class Comment extends Model
      */
     public function post()
     {
-        return $this->belongsTo(\App\Post::class);
+        return $this->belongsTo(Post::class);
     }
 
     /**
@@ -50,6 +50,6 @@ class Comment extends Model
      */
     public function author()
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 }
diff --git a/tests/fixtures/models/relationships.php b/tests/fixtures/models/relationships.php
index 0cc2f8c..1fe416f 100644
--- a/tests/fixtures/models/relationships.php
+++ b/tests/fixtures/models/relationships.php
@@ -32,11 +32,11 @@ class Comment extends Model
 
     public function post()
     {
-        return $this->belongsTo(\App\Post::class);
+        return $this->belongsTo(Post::class);
     }
 
     public function author()
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 }
diff --git a/tests/fixtures/models/return-type-declarations.php b/tests/fixtures/models/return-type-declarations.php
index 241156c..a29c66b 100644
--- a/tests/fixtures/models/return-type-declarations.php
+++ b/tests/fixtures/models/return-type-declarations.php
@@ -43,21 +43,21 @@ class Term extends Model
 
     public function organizers(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
     {
-        return $this->belongsToMany(\App\Organizer::class);
+        return $this->belongsToMany(Organizer::class);
     }
 
     public function events(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
     {
-        return $this->belongsToMany(\App\Event::class);
+        return $this->belongsToMany(Event::class);
     }
 
     public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 
     public function team(): \Illuminate\Database\Eloquent\Relations\BelongsTo
     {
-        return $this->belongsTo(\App\Team::class);
+        return $this->belongsTo(Team::class);
     }
 }
diff --git a/tests/fixtures/models/soft-deletes-phpdoc.php b/tests/fixtures/models/soft-deletes-phpdoc.php
index 7ec4eea..df32ee1 100644
--- a/tests/fixtures/models/soft-deletes-phpdoc.php
+++ b/tests/fixtures/models/soft-deletes-phpdoc.php
@@ -41,6 +41,6 @@ class Comment extends Model
      */
     public function post()
     {
-        return $this->belongsTo(\App\Post::class);
+        return $this->belongsTo(Post::class);
     }
 }
diff --git a/tests/fixtures/models/soft-deletes.php b/tests/fixtures/models/soft-deletes.php
index ec2b301..202b1be 100644
--- a/tests/fixtures/models/soft-deletes.php
+++ b/tests/fixtures/models/soft-deletes.php
@@ -31,6 +31,6 @@ class Comment extends Model
 
     public function post()
     {
-        return $this->belongsTo(\App\Post::class);
+        return $this->belongsTo(Post::class);
     }
 }
diff --git a/tests/fixtures/models/tag-many-to-many-polymorphic-relationship.php b/tests/fixtures/models/tag-many-to-many-polymorphic-relationship.php
new file mode 100644
index 0000000..a9460dd
--- /dev/null
+++ b/tests/fixtures/models/tag-many-to-many-polymorphic-relationship.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Tag extends Model
+{
+    use HasFactory;
+
+    /**
+     * The attributes that are mass assignable.
+     *
+     * @var array
+     */
+    protected $fillable = [
+        'name',
+    ];
+
+    /**
+     * The attributes that should be cast to native types.
+     *
+     * @var array
+     */
+    protected $casts = [
+        'id' => 'integer',
+    ];
+
+    public function posts()
+    {
+        return $this->morphedByMany(\App\Post::class, 'tagable');
+    }
+
+    public function videos()
+    {
+        return $this->morphedByMany(\App\Video::class, 'tagable');
+    }
+}
diff --git a/tests/fixtures/models/unconventional.php b/tests/fixtures/models/unconventional.php
index d242a7f..f43464b 100644
--- a/tests/fixtures/models/unconventional.php
+++ b/tests/fixtures/models/unconventional.php
@@ -35,11 +35,11 @@ class Team extends Model
 
     public function owner()
     {
-        return $this->belongsTo(\App\Owner::class);
+        return $this->belongsTo(Owner::class);
     }
 
     public function manager()
     {
-        return $this->belongsTo(\App\User::class);
+        return $this->belongsTo(User::class);
     }
 }
diff --git a/tests/fixtures/models/user-polymorphic-relationship.php b/tests/fixtures/models/user-polymorphic-relationship.php
index 122530c..af8ee55 100644
--- a/tests/fixtures/models/user-polymorphic-relationship.php
+++ b/tests/fixtures/models/user-polymorphic-relationship.php
@@ -29,6 +29,6 @@ class User extends Model
 
     public function images()
     {
-        return $this->morphMany(\App\Image::class, 'imageable');
+        return $this->morphMany(Image::class, 'imageable');
     }
 }
diff --git a/tests/fixtures/models/video-many-to-many-polymorphic-relationship.php b/tests/fixtures/models/video-many-to-many-polymorphic-relationship.php
new file mode 100644
index 0000000..fb39074
--- /dev/null
+++ b/tests/fixtures/models/video-many-to-many-polymorphic-relationship.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Video extends Model
+{
+    use HasFactory;
+
+    /**
+     * The attributes that are mass assignable.
+     *
+     * @var array
+     */
+    protected $fillable = [
+        'name',
+    ];
+
+    /**
+     * The attributes that should be cast to native types.
+     *
+     * @var array
+     */
+    protected $casts = [
+        'id' => 'integer',
+    ];
+
+    public function tags()
+    {
+        return $this->morphToMany(\App\Tag::class, 'tagable');
+    }
+}