Skip to content

Commit 67cef6f

Browse files
authored
Support Many to Many Polymorphic (#522)
1 parent 9c06850 commit 67cef6f

13 files changed

+350
-5
lines changed

src/Generators/MigrationGenerator.php

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function __construct(Filesystem $filesystem)
5959

6060
public function output(Tree $tree, $overwrite = false): array
6161
{
62-
$tables = ['tableNames' => [], 'pivotTableNames' => []];
62+
$tables = ['tableNames' => [], 'pivotTableNames' => [], 'polymorphicManyToManyTables' => []];
6363

6464
$stub = $this->filesystem->stub('migration.stub');
6565
/**
@@ -74,6 +74,12 @@ public function output(Tree $tree, $overwrite = false): array
7474
$tables['pivotTableNames'][$pivotTableName] = $this->populatePivotStub($stub, $pivotSegments);
7575
}
7676
}
77+
78+
if (!empty($model->polymorphicManyToManyTables())) {
79+
foreach ($model->polymorphicManyToManyTables() as $tableName) {
80+
$tables['polymorphicManyToManyTables'][Str::lower(Str::plural(Str::singular($tableName).'able'))] = $this->populatePolyStub($stub, $tableName);
81+
}
82+
}
7783
}
7884

7985
return $this->createMigrations($tables, $overwrite);
@@ -89,7 +95,7 @@ protected function createMigrations(array $tables, $overwrite = false): array
8995
$output = [];
9096

9197
$sequential_timestamp = \Carbon\Carbon::now()->copy()->subSeconds(
92-
collect($tables['tableNames'])->merge($tables['pivotTableNames'])->count()
98+
collect($tables['tableNames'])->merge($tables['pivotTableNames'])->merge($tables['polymorphicManyToManyTables'])->count()
9399
);
94100

95101
foreach ($tables['tableNames'] as $tableName => $data) {
@@ -106,6 +112,14 @@ protected function createMigrations(array $tables, $overwrite = false): array
106112

107113
$output[$action][] = $path;
108114
}
115+
116+
foreach ($tables['polymorphicManyToManyTables'] as $tableName => $data) {
117+
$path = $this->getTablePath($tableName, $sequential_timestamp->addSecond(), $overwrite);
118+
$action = $this->filesystem->exists($path) ? 'updated' : 'created';
119+
$this->filesystem->put($path, $data);
120+
$output[$action][] = $path;
121+
}
122+
109123
return $output;
110124
}
111125

@@ -116,7 +130,7 @@ protected function populateStub(string $stub, Model $model)
116130
$stub = str_replace('{{ definition }}', $this->buildDefinition($model), $stub);
117131

118132
if (Blueprint::useReturnTypeHints()) {
119-
$stub = str_replace(['up()','down()'], ['up(): void','down(): void'], $stub);
133+
$stub = str_replace(['up()', 'down()'], ['up(): void', 'down(): void'], $stub);
120134
}
121135

122136
if ($this->hasForeignKeyConstraints) {
@@ -139,6 +153,23 @@ protected function populatePivotStub(string $stub, array $segments)
139153
return $stub;
140154
}
141155

156+
protected function populatePolyStub(string $stub, string $parentTable)
157+
{
158+
$stub = str_replace('{{ class }}', $this->getPolyClassName($parentTable), $stub);
159+
$stub = str_replace('{{ table }}', $this->getPolyTableName($parentTable), $stub);
160+
$stub = str_replace('{{ definition }}', $this->buildPolyTableDefinition($parentTable), $stub);
161+
162+
if (Blueprint::useReturnTypeHints()) {
163+
$stub = str_replace(['up()', 'down()'], ['up(): void', 'down(): void'], $stub);
164+
}
165+
166+
if ($this->hasForeignKeyConstraints) {
167+
$stub = $this->disableForeignKeyConstraints($stub);
168+
}
169+
170+
return $stub;
171+
}
172+
142173
protected function buildDefinition(Model $model)
143174
{
144175
$definition = '';
@@ -286,6 +317,27 @@ protected function buildPivotTableDefinition(array $segments)
286317
return trim($definition);
287318
}
288319

320+
protected function buildPolyTableDefinition(string $parentTable)
321+
{
322+
$definition = '';
323+
324+
$references = 'id';
325+
$on = Str::lower(Str::plural($parentTable));
326+
$foreign = Str::lower(Str::singular($parentTable)) . '_' . $references;
327+
328+
if (config('blueprint.use_constraints')) {
329+
$this->hasForeignKeyConstraints = true;
330+
$definition .= $this->buildForeignKey($foreign, $on, 'id') . ';' . PHP_EOL;
331+
} else {
332+
$definition .= self::INDENT . '$table->foreignId(\'' . $foreign . '\');' . PHP_EOL;
333+
}
334+
335+
$definition .= self::INDENT . sprintf('$table->unsignedBigInteger(\'%s\');', Str::lower(Str::singular($parentTable).'able' . '_id')) . PHP_EOL;
336+
$definition .= self::INDENT . sprintf('$table->string(\'%s\');', Str::lower(Str::singular($parentTable).'able' . '_type')) . PHP_EOL;
337+
338+
return trim($definition);
339+
}
340+
289341
protected function buildForeignKey(string $column_name, ?string $on, string $type, array $attributes = [], array $modifiers = [])
290342
{
291343
if (is_null($on)) {
@@ -409,6 +461,11 @@ protected function getPivotClassName(array $segments)
409461
return 'Create' . Str::studly($this->getPivotTableName($segments)) . 'Table';
410462
}
411463

464+
protected function getPolyClassName(string $parentTable)
465+
{
466+
return 'Create' . Str::studly($this->getPolyTableName($parentTable)) . 'Table';
467+
}
468+
412469
protected function getPivotTableName(array $segments)
413470
{
414471
$isCustom = collect($segments)
@@ -435,6 +492,11 @@ function ($name) {
435492
return strtolower(implode('_', $segments));
436493
}
437494

495+
protected function getPolyTableName(string $parentTable)
496+
{
497+
return Str::plural(Str::lower(Str::singular($parentTable) . 'able'));
498+
}
499+
438500
private function shouldAddForeignKeyConstraint(\Blueprint\Models\Column $column)
439501
{
440502
if ($column->name() === 'id') {

src/Generators/ModelGenerator.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,11 @@ protected function buildRelationships(Model $model)
213213

214214
if ($type === 'morphTo') {
215215
$relationship = sprintf('$this->%s()', $type);
216-
} elseif ($type === 'morphMany' || $type === 'morphOne') {
216+
} elseif (in_array($type, ['morphMany', 'morphOne', 'morphToMany'])) {
217217
$relation = Str::lower($is_model_fqn ? Str::singular(Str::afterLast($column_name, '\\')) : Str::singular($column_name)) . 'able';
218218
$relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, $fqcn, $relation);
219+
} elseif ($type === 'morphedByMany') {
220+
$relationship = sprintf('$this->%s(%s::class, \'%sable\')', $type, $fqcn, strtolower($model->name()));
219221
} elseif (!is_null($key)) {
220222
$relationship = sprintf('$this->%s(%s::class, \'%s\', \'%s\')', $type, $fqcn, $column_name, $key);
221223
} elseif (!is_null($class) && $type === 'belongsToMany') {
@@ -227,7 +229,7 @@ protected function buildRelationships(Model $model)
227229

228230
if ($type === 'morphTo') {
229231
$method_name = Str::lower($class_name);
230-
} elseif (in_array($type, ['hasMany', 'belongsToMany', 'morphMany'])) {
232+
} elseif (in_array($type, ['hasMany', 'belongsToMany', 'morphMany', 'morphToMany', 'morphedByMany'])) {
231233
$method_name = Str::plural($is_model_fqn ? Str::afterLast($column_name, '\\') : $column_name);
232234
}
233235

src/Lexers/ModelLexer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class ModelLexer implements Lexer
1818
'morphone' => 'morphOne',
1919
'morphmany' => 'morphMany',
2020
'morphto' => 'morphTo',
21+
'morphtomany' => 'morphToMany',
22+
'morphedbymany' => 'morphedByMany',
2123
];
2224

2325
private static $dataTypes = [

src/Models/Model.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class Model
1515
private $columns = [];
1616
private $relationships = [];
1717
private $pivotTables = [];
18+
private $polymorphicManyToManyTables = [];
1819
private $indexes = [];
1920

2021
/**
@@ -149,10 +150,18 @@ public function addRelationship(string $type, string $reference)
149150
if ($type === 'belongsToMany') {
150151
$this->addPivotTable($reference);
151152
}
153+
if ($type === 'morphedByMany') {
154+
$this->addPolymorphicManyToManyTable(Str::studly($this->tableName()));
155+
}
152156

153157
$this->relationships[$type][] = $reference;
154158
}
155159

160+
public function addPolymorphicManyToManyTable(string $reference)
161+
{
162+
$this->polymorphicManyToManyTables[] = class_basename($reference);
163+
}
164+
156165
public function addPivotTable(string $reference)
157166
{
158167
$segments = [$this->name(), class_basename($reference)];
@@ -175,6 +184,11 @@ public function pivotTables(): array
175184
return $this->pivotTables;
176185
}
177186

187+
public function polymorphicManyToManyTables()
188+
{
189+
return $this->polymorphicManyToManyTables;
190+
}
191+
178192
public function setMorphTo(string $reference)
179193
{
180194
$this->morphTo = $reference;

tests/Feature/Generators/MigrationGeneratorTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,34 @@ public function output_creates_pivot_table_migration_correctly_when_model_name_c
409409
$this->assertEquals(['created' => [$model_migration, $pivot_migration]], $this->subject->output($tree));
410410
}
411411

412+
/**
413+
* @test
414+
*/
415+
public function output_also_creates_many_to_many_polymorphic_intermediate_table_migration()
416+
{
417+
$this->filesystem->expects('stub')
418+
->with('migration.stub')
419+
->andReturn($this->stub('migration.stub'));
420+
421+
$now = Carbon::now();
422+
Carbon::setTestNow($now);
423+
424+
$model_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_tags_table.php');
425+
$poly_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_tagables_table.php');
426+
427+
$this->filesystem->expects('exists')->twice()->andReturn(false);
428+
429+
$this->filesystem->expects('put')
430+
->with($model_migration, $this->fixture('migrations/morphed-by-many.php'));
431+
$this->filesystem->expects('put')
432+
->with($poly_migration, $this->fixture('migrations/morphed-by-many-intermediate.php'));
433+
434+
$tokens = $this->blueprint->parse($this->fixture('drafts/morphed-by-many.yaml'));
435+
$tree = $this->blueprint->analyze($tokens);
436+
437+
$this->assertEquals(['created' => [$model_migration, $poly_migration]], $this->subject->output($tree));
438+
}
439+
412440
/**
413441
* @test
414442
*/

tests/Feature/Generators/ModelGeneratorTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,52 @@ public function output_generates_polymorphic_relationships()
269269
$this->assertEquals(['created' => ['app/Post.php', 'app/User.php', 'app/Image.php']], $this->subject->output($tree));
270270
}
271271

272+
/**
273+
* @test
274+
*/
275+
public function output_generates_morphtomany_relationship_with_intermediate_models()
276+
{
277+
$this->filesystem->expects('stub')
278+
->with($this->modelStub)
279+
->andReturn($this->stub($this->modelStub));
280+
$this->filesystem->expects('stub')
281+
->times(3)
282+
->with('model.fillable.stub')
283+
->andReturn($this->stub('model.fillable.stub'));
284+
285+
$this->filesystem->expects('stub')
286+
->times(3)
287+
->with('model.casts.stub')
288+
->andReturn($this->stub('model.casts.stub'));
289+
$this->filesystem->expects('stub')
290+
->times(3)
291+
->with('model.method.stub')
292+
->andReturn($this->stub('model.method.stub'));
293+
294+
$this->filesystem->expects('exists')
295+
->with('app')
296+
->andReturnTrue();
297+
$this->filesystem->expects('put')
298+
->with('app/Post.php', $this->fixture('models/post-many-to-many-polymorphic-relationship.php'));
299+
300+
$this->filesystem->expects('exists')
301+
->with('app')
302+
->andReturnTrue();
303+
$this->filesystem->expects('put')
304+
->with('app/Video.php', $this->fixture('models/video-many-to-many-polymorphic-relationship.php'));
305+
306+
$this->filesystem->expects('exists')
307+
->with('app')
308+
->andReturnTrue();
309+
$this->filesystem->expects('put')
310+
->with('app/Tag.php', $this->fixture('models/tag-many-to-many-polymorphic-relationship.php'));
311+
312+
$tokens = $this->blueprint->parse($this->fixture('drafts/many-to-many-polymorphic-relationships.yaml'));
313+
$tree = $this->blueprint->analyze($tokens);
314+
315+
$this->assertEquals(['created' => ['app/Post.php', 'app/Video.php', 'app/Tag.php']], $this->subject->output($tree));
316+
}
317+
272318
/**
273319
* @test
274320
*/
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
models:
2+
Post:
3+
name: string
4+
relationships:
5+
morphToMany: Tag
6+
7+
Video:
8+
name: string
9+
relationships:
10+
morphToMany: Tag
11+
12+
Tag:
13+
name: string
14+
relationships:
15+
morphedByMany: Post, Video
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
models:
2+
Tag:
3+
name: string
4+
relationships:
5+
morphedByMany: Video
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class CreateTagablesTable extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('tagables', function (Blueprint $table) {
17+
$table->foreignId('tag_id');
18+
$table->unsignedBigInteger('tagable_id');
19+
$table->string('tagable_type');
20+
});
21+
}
22+
23+
/**
24+
* Reverse the migrations.
25+
*
26+
* @return void
27+
*/
28+
public function down()
29+
{
30+
Schema::dropIfExists('tagables');
31+
}
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class CreateTagsTable extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('tags', function (Blueprint $table) {
17+
$table->id();
18+
$table->string('name');
19+
$table->timestamps();
20+
});
21+
}
22+
23+
/**
24+
* Reverse the migrations.
25+
*
26+
* @return void
27+
*/
28+
public function down()
29+
{
30+
Schema::dropIfExists('tags');
31+
}
32+
}

0 commit comments

Comments
 (0)