diff --git a/src/Generators/MigrationGenerator.php b/src/Generators/MigrationGenerator.php index 1673b118..7e84bf8b 100644 --- a/src/Generators/MigrationGenerator.php +++ b/src/Generators/MigrationGenerator.php @@ -176,6 +176,11 @@ protected function buildDefinition(Model $model) $definition .= self::INDENT . '$table->' . $model->softDeletesDataType() . '();' . PHP_EOL; } + if ($model->morphTo()) { + $definition .= self::INDENT . sprintf('$table->unsignedBigInteger(\'%s\');', Str::lower($model->morphTo() . "_id")) . PHP_EOL; + $definition .= self::INDENT . sprintf('$table->string(\'%s\');', Str::lower($model->morphTo() . "_type")) . PHP_EOL; + } + if ($model->usesTimestamps()) { $definition .= self::INDENT . '$table->' . $model->timestampsDataType() . '();' . PHP_EOL; } diff --git a/src/Generators/ModelGenerator.php b/src/Generators/ModelGenerator.php index ccf59a2a..68e79b0b 100644 --- a/src/Generators/ModelGenerator.php +++ b/src/Generators/ModelGenerator.php @@ -146,9 +146,21 @@ private function buildRelationships(Model $model) $name = Str::beforeLast($name, '_id'); $class = Str::studly($class ?? $name); - $relationship = sprintf("\$this->%s(%s::class)", $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class); - $method_name = $type === 'hasMany' || $type === 'belongsToMany' ? Str::plural($name) : $name; + if ($type === 'morphTo') { + $relationship = sprintf('$this->%s()', $type); + } elseif ($type === 'morphMany' || $type === 'morphOne') { + $relation = Str::of($name)->lower()->singular() . 'able'; + $relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class, $relation); + } else { + $relationship = sprintf('$this->%s(%s::class)', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class); + } + + if ($type === 'morphTo') { + $method_name = Str::lower($class); + } else { + $method_name = in_array($type, ['hasMany', 'belongsToMany', 'morphMany']) ? Str::plural($name) : $name; + } $method = str_replace('DummyName', Str::camel($method_name), $template); $method = str_replace('null', $relationship, $method); diff --git a/src/Lexers/ModelLexer.php b/src/Lexers/ModelLexer.php index 333cda7f..0a7d217d 100644 --- a/src/Lexers/ModelLexer.php +++ b/src/Lexers/ModelLexer.php @@ -12,7 +12,10 @@ class ModelLexer implements Lexer 'belongsto' => 'belongsTo', 'hasone' => 'hasOne', 'hasmany' => 'hasMany', - 'belongstomany' => 'belongsToMany' + 'belongstomany' => 'belongsToMany', + 'morphone' => 'morphOne', + 'morphmany' => 'morphMany', + 'morphto' => 'morphTo', ]; private static $dataTypes = [ @@ -149,6 +152,10 @@ private function buildModel(string $name, array $columns) foreach ($columns['relationships'] as $type => $relationships) { foreach (explode(',', $relationships) as $reference) { $model->addRelationship(self::$relationships[strtolower($type)], trim($reference)); + + if ($type === 'morphTo') { + $model->setMorphTo(trim($reference)); + } } } } diff --git a/src/Models/Model.php b/src/Models/Model.php index 4ff3005d..d40f3210 100644 --- a/src/Models/Model.php +++ b/src/Models/Model.php @@ -11,6 +11,7 @@ class Model private $primaryKey = 'id'; private $timestamps = 'timestamps'; private $softDeletes = false; + private $morphTo; private $columns = []; private $relationships = []; private $pivotTables = []; @@ -162,4 +163,14 @@ public function pivotTables(): array { return $this->pivotTables; } + + public function setMorphTo(string $reference) + { + $this->morphTo = $reference; + } + + public function morphTo(): ?string + { + return $this->morphTo; + } } diff --git a/tests/Feature/Generator/MigrationGeneratorTest.php b/tests/Feature/Generator/MigrationGeneratorTest.php index 51168a77..0c87c07b 100644 --- a/tests/Feature/Generator/MigrationGeneratorTest.php +++ b/tests/Feature/Generator/MigrationGeneratorTest.php @@ -305,8 +305,8 @@ public function output_also_creates_constraints_for_pivot_table_migration_larave } /** - * @test - */ + * @test + */ public function output_does_not_duplicate_pivot_table_migration() { $this->files->expects('stub') @@ -334,8 +334,8 @@ public function output_does_not_duplicate_pivot_table_migration() } /** - * @test - */ + * @test + */ public function output_does_not_duplicate_pivot_table_migration_laravel6() { $app = \Mockery::mock(); @@ -422,6 +422,70 @@ public function output_creates_foreign_keys_with_nullable_chained_correctly_lara $this->assertEquals(['created' => [$model_migration]], $this->subject->output($tree)); } + /** + * @test + */ + public function output_works_with_polymorphic_relationships() + { + $this->files->expects('stub') + ->with('migration.stub') + ->andReturn(file_get_contents('stubs/migration.stub')); + + $now = Carbon::now(); + Carbon::setTestNow($now); + + $post_migration = str_replace('timestamp', $now->copy()->subSeconds(2)->format('Y_m_d_His'), 'database/migrations/timestamp_create_posts_table.php'); + $user_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_users_table.php'); + $image_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_images_table.php'); + + $this->files->expects('put') + ->with($post_migration, $this->fixture('migrations/polymorphic_relationships_posts_table.php')); + $this->files->expects('put') + ->with($user_migration, $this->fixture('migrations/polymorphic_relationships_users_table.php')); + $this->files->expects('put') + ->with($image_migration, $this->fixture('migrations/polymorphic_relationships_images_table.php')); + + $tokens = $this->blueprint->parse($this->fixture('definitions/polymorphic-relationships.bp')); + $tree = $this->blueprint->analyze($tokens); + + $this->assertEquals(['created' => [$post_migration, $user_migration, $image_migration]], $this->subject->output($tree)); + } + + /** + * @test + */ + public function output_works_with_polymorphic_relationships_laravel6() + { + $app = \Mockery::mock(); + $app->shouldReceive('version') + ->withNoArgs() + ->andReturn('6.0.0'); + App::swap($app); + + $this->files->expects('stub') + ->with('migration.stub') + ->andReturn(file_get_contents('stubs/migration.stub')); + + $now = Carbon::now(); + Carbon::setTestNow($now); + + $post_migration = str_replace('timestamp', $now->copy()->subSeconds(2)->format('Y_m_d_His'), 'database/migrations/timestamp_create_posts_table.php'); + $user_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_users_table.php'); + $image_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_images_table.php'); + + $this->files->expects('put') + ->with($post_migration, $this->fixture('migrations/polymorphic_relationships_posts_table_laravel6.php')); + $this->files->expects('put') + ->with($user_migration, $this->fixture('migrations/polymorphic_relationships_users_table_laravel6.php')); + $this->files->expects('put') + ->with($image_migration, $this->fixture('migrations/polymorphic_relationships_images_table_laravel6.php')); + + $tokens = $this->blueprint->parse($this->fixture('definitions/polymorphic-relationships.bp')); + $tree = $this->blueprint->analyze($tokens); + + $this->assertEquals(['created' => [$post_migration, $user_migration, $image_migration]], $this->subject->output($tree)); + } + public function modelTreeDataProvider() { return [ diff --git a/tests/Feature/Generator/ModelGeneratorTest.php b/tests/Feature/Generator/ModelGeneratorTest.php index 98193cb2..8eabecb6 100644 --- a/tests/Feature/Generator/ModelGeneratorTest.php +++ b/tests/Feature/Generator/ModelGeneratorTest.php @@ -93,8 +93,8 @@ public function output_generates_models($definition, $path, $model) } /** - * @test - */ + * @test + */ public function output_works_for_pascal_case_definition() { $this->files->expects('stub') @@ -164,6 +164,51 @@ public function output_generates_relationships() $this->assertEquals(['created' => ['app/Subscription.php']], $this->subject->output($tree)); } + /** + * @test + */ + public function output_generates_polymorphic_relationships() + { + $this->files->expects('stub') + ->with('model/class.stub') + ->andReturn(file_get_contents('stubs/model/class.stub')); + $this->files->expects('stub') + ->times(3) + ->with('model/fillable.stub') + ->andReturn(file_get_contents('stubs/model/fillable.stub')); + $this->files->expects('stub') + ->times(3) + ->with('model/casts.stub') + ->andReturn(file_get_contents('stubs/model/casts.stub')); + $this->files->expects('stub') + ->times(3) + ->with('model/method.stub') + ->andReturn(file_get_contents('stubs/model/method.stub')); + + $this->files->expects('exists') + ->with('app') + ->andReturnTrue(); + $this->files->expects('put') + ->with('app/Post.php', $this->fixture('models/post-polymorphic-relationship.php')); + + $this->files->expects('exists') + ->with('app') + ->andReturnTrue(); + $this->files->expects('put') + ->with('app/User.php', $this->fixture('models/user-polymorphic-relationship.php')); + + $this->files->expects('exists') + ->with('app') + ->andReturnTrue(); + $this->files->expects('put') + ->with('app/Image.php', $this->fixture('models/image-polymorphic-relationship.php')); + + $tokens = $this->blueprint->parse($this->fixture('definitions/polymorphic-relationships.bp')); + $tree = $this->blueprint->analyze($tokens); + + $this->assertEquals(['created' => ['app/Post.php', 'app/User.php', 'app/Image.php']], $this->subject->output($tree)); + } + /** * @test */ @@ -326,8 +371,8 @@ public function output_generates_models_with_guarded_property_when_config_option } /** - * @test - */ + * @test + */ public function output_generates_models_with_custom_namespace_correctly() { $definition = 'definitions/custom-models-namespace.bp'; diff --git a/tests/Feature/Lexers/ModelLexerTest.php b/tests/Feature/Lexers/ModelLexerTest.php index cfd386f1..428fbc58 100644 --- a/tests/Feature/Lexers/ModelLexerTest.php +++ b/tests/Feature/Lexers/ModelLexerTest.php @@ -499,6 +499,42 @@ public function it_stores_relationships() $this->assertEquals(['Duration', 'Transaction:tid'], $relationships['hasOne']); } + /** + * @test + */ + public function it_enables_morphable_and_set_its_reference() + { + $tokens = [ + 'models' => [ + 'Model' => [ + 'relationships' => [ + 'morphTo' => 'Morphable', + ] + ], + ], + ]; + + $actual = $this->subject->analyze($tokens); + + $this->assertIsArray($actual['models']); + $this->assertCount(1, $actual['models']); + + $model = $actual['models']['Model']; + $this->assertEquals('Model', $model->name()); + $this->assertEquals('Morphable', $model->morphTo()); + $this->assertTrue($model->usesTimestamps()); + + $columns = $model->columns(); + $this->assertCount(1, $columns); + $this->assertEquals('id', $columns['id']->name()); + $this->assertEquals('id', $columns['id']->dataType()); + $this->assertEquals([], $columns['id']->modifiers()); + + $relationships = $model->relationships(); + $this->assertCount(1, $relationships); + $this->assertEquals(['Morphable'], $relationships['morphTo']); + } + public function dataTypeAttributesDataProvider() { return [ diff --git a/tests/fixtures/definitions/polymorphic-relationships.bp b/tests/fixtures/definitions/polymorphic-relationships.bp new file mode 100644 index 00000000..ac4b9143 --- /dev/null +++ b/tests/fixtures/definitions/polymorphic-relationships.bp @@ -0,0 +1,15 @@ +models: + Post: + name: string:400 + relationships: + morphMany: Image + + User: + name: string:400 + relationships: + morphMany: Image + + Image: + url: string:400 + relationships: + morphTo: Imageable diff --git a/tests/fixtures/migrations/polymorphic_relationships_images_table.php b/tests/fixtures/migrations/polymorphic_relationships_images_table.php new file mode 100644 index 00000000..22138a77 --- /dev/null +++ b/tests/fixtures/migrations/polymorphic_relationships_images_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('url', 400); + $table->unsignedBigInteger('imageable_id'); + $table->string('imageable_type'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('images'); + } +} diff --git a/tests/fixtures/migrations/polymorphic_relationships_images_table_laravel6.php b/tests/fixtures/migrations/polymorphic_relationships_images_table_laravel6.php new file mode 100644 index 00000000..ec0cf69e --- /dev/null +++ b/tests/fixtures/migrations/polymorphic_relationships_images_table_laravel6.php @@ -0,0 +1,34 @@ +bigIncrements('id'); + $table->string('url', 400); + $table->unsignedBigInteger('imageable_id'); + $table->string('imageable_type'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('images'); + } +} diff --git a/tests/fixtures/migrations/polymorphic_relationships_posts_table.php b/tests/fixtures/migrations/polymorphic_relationships_posts_table.php new file mode 100644 index 00000000..daa77b34 --- /dev/null +++ b/tests/fixtures/migrations/polymorphic_relationships_posts_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name', 400); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('posts'); + } +} diff --git a/tests/fixtures/migrations/polymorphic_relationships_posts_table_laravel6.php b/tests/fixtures/migrations/polymorphic_relationships_posts_table_laravel6.php new file mode 100644 index 00000000..9f45d2bb --- /dev/null +++ b/tests/fixtures/migrations/polymorphic_relationships_posts_table_laravel6.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->string('name', 400); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('posts'); + } +} diff --git a/tests/fixtures/migrations/polymorphic_relationships_users_table.php b/tests/fixtures/migrations/polymorphic_relationships_users_table.php new file mode 100644 index 00000000..b9c84d54 --- /dev/null +++ b/tests/fixtures/migrations/polymorphic_relationships_users_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name', 400); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('users'); + } +} diff --git a/tests/fixtures/migrations/polymorphic_relationships_users_table_laravel6.php b/tests/fixtures/migrations/polymorphic_relationships_users_table_laravel6.php new file mode 100644 index 00000000..bcd71a5c --- /dev/null +++ b/tests/fixtures/migrations/polymorphic_relationships_users_table_laravel6.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->string('name', 400); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('users'); + } +} diff --git a/tests/fixtures/models/image-polymorphic-relationship.php b/tests/fixtures/models/image-polymorphic-relationship.php new file mode 100644 index 00000000..8aa3b034 --- /dev/null +++ b/tests/fixtures/models/image-polymorphic-relationship.php @@ -0,0 +1,32 @@ + 'integer', + ]; + + + public function imageable() + { + return $this->morphTo(); + } +} diff --git a/tests/fixtures/models/post-polymorphic-relationship.php b/tests/fixtures/models/post-polymorphic-relationship.php new file mode 100644 index 00000000..a985d00d --- /dev/null +++ b/tests/fixtures/models/post-polymorphic-relationship.php @@ -0,0 +1,32 @@ + 'integer', + ]; + + + public function images() + { + return $this->morphMany(\App\Image::class, 'imageable'); + } +} diff --git a/tests/fixtures/models/user-polymorphic-relationship.php b/tests/fixtures/models/user-polymorphic-relationship.php new file mode 100644 index 00000000..dd5f75e4 --- /dev/null +++ b/tests/fixtures/models/user-polymorphic-relationship.php @@ -0,0 +1,32 @@ + 'integer', + ]; + + + public function images() + { + return $this->morphMany(\App\Image::class, 'imageable'); + } +}