Skip to content

Commit 644917e

Browse files
authored
Support for polymorphic relations (#204)
1 parent 07e88dc commit 644917e

17 files changed

+498
-11
lines changed

src/Generators/MigrationGenerator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ protected function buildDefinition(Model $model)
176176
$definition .= self::INDENT . '$table->' . $model->softDeletesDataType() . '();' . PHP_EOL;
177177
}
178178

179+
if ($model->morphTo()) {
180+
$definition .= self::INDENT . sprintf('$table->unsignedBigInteger(\'%s\');', Str::lower($model->morphTo() . "_id")) . PHP_EOL;
181+
$definition .= self::INDENT . sprintf('$table->string(\'%s\');', Str::lower($model->morphTo() . "_type")) . PHP_EOL;
182+
}
183+
179184
if ($model->usesTimestamps()) {
180185
$definition .= self::INDENT . '$table->' . $model->timestampsDataType() . '();' . PHP_EOL;
181186
}

src/Generators/ModelGenerator.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,21 @@ private function buildRelationships(Model $model)
146146

147147
$name = Str::beforeLast($name, '_id');
148148
$class = Str::studly($class ?? $name);
149-
$relationship = sprintf("\$this->%s(%s::class)", $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class);
150149

151-
$method_name = $type === 'hasMany' || $type === 'belongsToMany' ? Str::plural($name) : $name;
150+
if ($type === 'morphTo') {
151+
$relationship = sprintf('$this->%s()', $type);
152+
} elseif ($type === 'morphMany' || $type === 'morphOne') {
153+
$relation = Str::of($name)->lower()->singular() . 'able';
154+
$relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class, $relation);
155+
} else {
156+
$relationship = sprintf('$this->%s(%s::class)', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class);
157+
}
158+
159+
if ($type === 'morphTo') {
160+
$method_name = Str::lower($class);
161+
} else {
162+
$method_name = in_array($type, ['hasMany', 'belongsToMany', 'morphMany']) ? Str::plural($name) : $name;
163+
}
152164
$method = str_replace('DummyName', Str::camel($method_name), $template);
153165
$method = str_replace('null', $relationship, $method);
154166

src/Lexers/ModelLexer.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ class ModelLexer implements Lexer
1212
'belongsto' => 'belongsTo',
1313
'hasone' => 'hasOne',
1414
'hasmany' => 'hasMany',
15-
'belongstomany' => 'belongsToMany'
15+
'belongstomany' => 'belongsToMany',
16+
'morphone' => 'morphOne',
17+
'morphmany' => 'morphMany',
18+
'morphto' => 'morphTo',
1619
];
1720

1821
private static $dataTypes = [
@@ -149,6 +152,10 @@ private function buildModel(string $name, array $columns)
149152
foreach ($columns['relationships'] as $type => $relationships) {
150153
foreach (explode(',', $relationships) as $reference) {
151154
$model->addRelationship(self::$relationships[strtolower($type)], trim($reference));
155+
156+
if ($type === 'morphTo') {
157+
$model->setMorphTo(trim($reference));
158+
}
152159
}
153160
}
154161
}

src/Models/Model.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Model
1111
private $primaryKey = 'id';
1212
private $timestamps = 'timestamps';
1313
private $softDeletes = false;
14+
private $morphTo;
1415
private $columns = [];
1516
private $relationships = [];
1617
private $pivotTables = [];
@@ -162,4 +163,14 @@ public function pivotTables(): array
162163
{
163164
return $this->pivotTables;
164165
}
166+
167+
public function setMorphTo(string $reference)
168+
{
169+
$this->morphTo = $reference;
170+
}
171+
172+
public function morphTo(): ?string
173+
{
174+
return $this->morphTo;
175+
}
165176
}

tests/Feature/Generator/MigrationGeneratorTest.php

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,8 @@ public function output_also_creates_constraints_for_pivot_table_migration_larave
305305
}
306306

307307
/**
308-
* @test
309-
*/
308+
* @test
309+
*/
310310
public function output_does_not_duplicate_pivot_table_migration()
311311
{
312312
$this->files->expects('stub')
@@ -334,8 +334,8 @@ public function output_does_not_duplicate_pivot_table_migration()
334334
}
335335

336336
/**
337-
* @test
338-
*/
337+
* @test
338+
*/
339339
public function output_does_not_duplicate_pivot_table_migration_laravel6()
340340
{
341341
$app = \Mockery::mock();
@@ -422,6 +422,70 @@ public function output_creates_foreign_keys_with_nullable_chained_correctly_lara
422422
$this->assertEquals(['created' => [$model_migration]], $this->subject->output($tree));
423423
}
424424

425+
/**
426+
* @test
427+
*/
428+
public function output_works_with_polymorphic_relationships()
429+
{
430+
$this->files->expects('stub')
431+
->with('migration.stub')
432+
->andReturn(file_get_contents('stubs/migration.stub'));
433+
434+
$now = Carbon::now();
435+
Carbon::setTestNow($now);
436+
437+
$post_migration = str_replace('timestamp', $now->copy()->subSeconds(2)->format('Y_m_d_His'), 'database/migrations/timestamp_create_posts_table.php');
438+
$user_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_users_table.php');
439+
$image_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_images_table.php');
440+
441+
$this->files->expects('put')
442+
->with($post_migration, $this->fixture('migrations/polymorphic_relationships_posts_table.php'));
443+
$this->files->expects('put')
444+
->with($user_migration, $this->fixture('migrations/polymorphic_relationships_users_table.php'));
445+
$this->files->expects('put')
446+
->with($image_migration, $this->fixture('migrations/polymorphic_relationships_images_table.php'));
447+
448+
$tokens = $this->blueprint->parse($this->fixture('definitions/polymorphic-relationships.bp'));
449+
$tree = $this->blueprint->analyze($tokens);
450+
451+
$this->assertEquals(['created' => [$post_migration, $user_migration, $image_migration]], $this->subject->output($tree));
452+
}
453+
454+
/**
455+
* @test
456+
*/
457+
public function output_works_with_polymorphic_relationships_laravel6()
458+
{
459+
$app = \Mockery::mock();
460+
$app->shouldReceive('version')
461+
->withNoArgs()
462+
->andReturn('6.0.0');
463+
App::swap($app);
464+
465+
$this->files->expects('stub')
466+
->with('migration.stub')
467+
->andReturn(file_get_contents('stubs/migration.stub'));
468+
469+
$now = Carbon::now();
470+
Carbon::setTestNow($now);
471+
472+
$post_migration = str_replace('timestamp', $now->copy()->subSeconds(2)->format('Y_m_d_His'), 'database/migrations/timestamp_create_posts_table.php');
473+
$user_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_users_table.php');
474+
$image_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_images_table.php');
475+
476+
$this->files->expects('put')
477+
->with($post_migration, $this->fixture('migrations/polymorphic_relationships_posts_table_laravel6.php'));
478+
$this->files->expects('put')
479+
->with($user_migration, $this->fixture('migrations/polymorphic_relationships_users_table_laravel6.php'));
480+
$this->files->expects('put')
481+
->with($image_migration, $this->fixture('migrations/polymorphic_relationships_images_table_laravel6.php'));
482+
483+
$tokens = $this->blueprint->parse($this->fixture('definitions/polymorphic-relationships.bp'));
484+
$tree = $this->blueprint->analyze($tokens);
485+
486+
$this->assertEquals(['created' => [$post_migration, $user_migration, $image_migration]], $this->subject->output($tree));
487+
}
488+
425489
public function modelTreeDataProvider()
426490
{
427491
return [

tests/Feature/Generator/ModelGeneratorTest.php

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ public function output_generates_models($definition, $path, $model)
9393
}
9494

9595
/**
96-
* @test
97-
*/
96+
* @test
97+
*/
9898
public function output_works_for_pascal_case_definition()
9999
{
100100
$this->files->expects('stub')
@@ -164,6 +164,51 @@ public function output_generates_relationships()
164164
$this->assertEquals(['created' => ['app/Subscription.php']], $this->subject->output($tree));
165165
}
166166

167+
/**
168+
* @test
169+
*/
170+
public function output_generates_polymorphic_relationships()
171+
{
172+
$this->files->expects('stub')
173+
->with('model/class.stub')
174+
->andReturn(file_get_contents('stubs/model/class.stub'));
175+
$this->files->expects('stub')
176+
->times(3)
177+
->with('model/fillable.stub')
178+
->andReturn(file_get_contents('stubs/model/fillable.stub'));
179+
$this->files->expects('stub')
180+
->times(3)
181+
->with('model/casts.stub')
182+
->andReturn(file_get_contents('stubs/model/casts.stub'));
183+
$this->files->expects('stub')
184+
->times(3)
185+
->with('model/method.stub')
186+
->andReturn(file_get_contents('stubs/model/method.stub'));
187+
188+
$this->files->expects('exists')
189+
->with('app')
190+
->andReturnTrue();
191+
$this->files->expects('put')
192+
->with('app/Post.php', $this->fixture('models/post-polymorphic-relationship.php'));
193+
194+
$this->files->expects('exists')
195+
->with('app')
196+
->andReturnTrue();
197+
$this->files->expects('put')
198+
->with('app/User.php', $this->fixture('models/user-polymorphic-relationship.php'));
199+
200+
$this->files->expects('exists')
201+
->with('app')
202+
->andReturnTrue();
203+
$this->files->expects('put')
204+
->with('app/Image.php', $this->fixture('models/image-polymorphic-relationship.php'));
205+
206+
$tokens = $this->blueprint->parse($this->fixture('definitions/polymorphic-relationships.bp'));
207+
$tree = $this->blueprint->analyze($tokens);
208+
209+
$this->assertEquals(['created' => ['app/Post.php', 'app/User.php', 'app/Image.php']], $this->subject->output($tree));
210+
}
211+
167212
/**
168213
* @test
169214
*/
@@ -326,8 +371,8 @@ public function output_generates_models_with_guarded_property_when_config_option
326371
}
327372

328373
/**
329-
* @test
330-
*/
374+
* @test
375+
*/
331376
public function output_generates_models_with_custom_namespace_correctly()
332377
{
333378
$definition = 'definitions/custom-models-namespace.bp';

tests/Feature/Lexers/ModelLexerTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,42 @@ public function it_stores_relationships()
499499
$this->assertEquals(['Duration', 'Transaction:tid'], $relationships['hasOne']);
500500
}
501501

502+
/**
503+
* @test
504+
*/
505+
public function it_enables_morphable_and_set_its_reference()
506+
{
507+
$tokens = [
508+
'models' => [
509+
'Model' => [
510+
'relationships' => [
511+
'morphTo' => 'Morphable',
512+
]
513+
],
514+
],
515+
];
516+
517+
$actual = $this->subject->analyze($tokens);
518+
519+
$this->assertIsArray($actual['models']);
520+
$this->assertCount(1, $actual['models']);
521+
522+
$model = $actual['models']['Model'];
523+
$this->assertEquals('Model', $model->name());
524+
$this->assertEquals('Morphable', $model->morphTo());
525+
$this->assertTrue($model->usesTimestamps());
526+
527+
$columns = $model->columns();
528+
$this->assertCount(1, $columns);
529+
$this->assertEquals('id', $columns['id']->name());
530+
$this->assertEquals('id', $columns['id']->dataType());
531+
$this->assertEquals([], $columns['id']->modifiers());
532+
533+
$relationships = $model->relationships();
534+
$this->assertCount(1, $relationships);
535+
$this->assertEquals(['Morphable'], $relationships['morphTo']);
536+
}
537+
502538
public function dataTypeAttributesDataProvider()
503539
{
504540
return [
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:400
4+
relationships:
5+
morphMany: Image
6+
7+
User:
8+
name: string:400
9+
relationships:
10+
morphMany: Image
11+
12+
Image:
13+
url: string:400
14+
relationships:
15+
morphTo: Imageable
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class CreateImagesTable extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('images', function (Blueprint $table) {
17+
$table->id();
18+
$table->string('url', 400);
19+
$table->unsignedBigInteger('imageable_id');
20+
$table->string('imageable_type');
21+
$table->timestamps();
22+
});
23+
}
24+
25+
/**
26+
* Reverse the migrations.
27+
*
28+
* @return void
29+
*/
30+
public function down()
31+
{
32+
Schema::dropIfExists('images');
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class CreateImagesTable extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('images', function (Blueprint $table) {
17+
$table->bigIncrements('id');
18+
$table->string('url', 400);
19+
$table->unsignedBigInteger('imageable_id');
20+
$table->string('imageable_type');
21+
$table->timestamps();
22+
});
23+
}
24+
25+
/**
26+
* Reverse the migrations.
27+
*
28+
* @return void
29+
*/
30+
public function down()
31+
{
32+
Schema::dropIfExists('images');
33+
}
34+
}

0 commit comments

Comments
 (0)