From 3c638a518b36139e5bb812347978bfeb527d15b6 Mon Sep 17 00:00:00 2001 From: Jason McCreary Date: Thu, 21 May 2020 14:53:40 -0400 Subject: [PATCH] Support for foreign data type shorthand (#224) --- src/Generators/ModelGenerator.php | 35 ++++-- src/Lexers/ModelLexer.php | 38 +++++-- .../Generator/FactoryGeneratorTest.php | 1 + .../Generator/MigrationGeneratorTest.php | 24 +++- .../Feature/Generator/ModelGeneratorTest.php | 1 + tests/Feature/Lexers/ModelLexerTest.php | 107 ++++++++++++++++++ .../definitions/foreign-key-shorthand.bp | 5 + .../factories/foreign-key-shorthand.php | 16 +++ .../migrations/foreign-key-shorthand.php | 34 ++++++ .../models/foreign-key-shorthand-phpdoc.php | 64 +++++++++++ 10 files changed, 308 insertions(+), 17 deletions(-) create mode 100644 tests/fixtures/definitions/foreign-key-shorthand.bp create mode 100644 tests/fixtures/factories/foreign-key-shorthand.php create mode 100644 tests/fixtures/migrations/foreign-key-shorthand.php create mode 100644 tests/fixtures/models/foreign-key-shorthand-phpdoc.php diff --git a/src/Generators/ModelGenerator.php b/src/Generators/ModelGenerator.php index 68e79b0b..99b00f1e 100644 --- a/src/Generators/ModelGenerator.php +++ b/src/Generators/ModelGenerator.php @@ -137,29 +137,46 @@ private function buildRelationships(Model $model) foreach ($model->relationships() as $type => $references) { foreach ($references as $reference) { + $key = null; + $class = null; + + $column_name = $reference; + $method_name = Str::beforeLast($reference, '_id'); + if (Str::contains($reference, ':')) { - [$class, $name] = explode(':', $reference); - } else { - $name = $reference; - $class = null; + [$foreign_reference, $column_name] = explode(':', $reference); + $method_name = Str::beforeLast($column_name, '_id'); + + if (Str::contains($foreign_reference, '.')) { + [$class, $key] = explode('.', $foreign_reference); + + if ($key === 'id') { + $key = null; + } else { + $method_name = Str::lower($class); + } + } else { + $class = $foreign_reference; + } } - $name = Str::beforeLast($name, '_id'); - $class = Str::studly($class ?? $name); + $class = Str::studly($class ?? $method_name); if ($type === 'morphTo') { $relationship = sprintf('$this->%s()', $type); } elseif ($type === 'morphMany' || $type === 'morphOne') { - $relation = Str::of($name)->lower()->singular() . 'able'; + $relation = Str::lower(Str::singular($column_name)) . 'able'; $relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class, $relation); + } elseif (!is_null($key)) { + $relationship = sprintf('$this->%s(%s::class, \'%s\', \'%s\')', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class, $column_name, $key); } 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; + } elseif (in_array($type, ['hasMany', 'belongsToMany', 'morphMany'])) { + $method_name = Str::plural($column_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 0a7d217d..6393e702 100644 --- a/src/Lexers/ModelLexer.php +++ b/src/Lexers/ModelLexer.php @@ -5,6 +5,7 @@ use Blueprint\Contracts\Lexer; use Blueprint\Models\Column; use Blueprint\Models\Model; +use Illuminate\Support\Str; class ModelLexer implements Lexer { @@ -172,12 +173,27 @@ private function buildModel(string $name, array $columns) $column = $this->buildColumn($name, $definition); $model->addColumn($column); - if ($column->name() !== 'id' && in_array($column->dataType(), ['id', 'uuid'])) { - if ($column->attributes()) { - $model->addRelationship('belongsTo', $column->attributes()[0] . ':' . $column->name()); - } else { - $model->addRelationship('belongsTo', $column->name()); + $foreign = collect($column->modifiers())->filter(function ($modifier) { + return (is_array($modifier) && key($modifier) === 'foreign') || $modifier === 'foreign'; + })->flatten()->first(); + + if ($column->name() !== 'id' && (in_array($column->dataType(), ['id', 'uuid']) || $foreign)) { + $reference = $column->name(); + + if ($foreign && $foreign !== 'foreign') { + $table = $foreign; + $key = 'id'; + + if (Str::contains($foreign, '.')) { + [$table, $key] = explode('.', $foreign); + } + + $reference = Str::singular($table) . ($key === 'id' ? '' : '.' . $key) . ':' . $column->name(); + } elseif ($column->attributes()) { + $reference = $column->attributes()[0] . ':' . $column->name(); } + + $model->addRelationship('belongsTo', $reference); } } @@ -186,10 +202,10 @@ private function buildModel(string $name, array $columns) private function buildColumn(string $name, string $definition) { - $data_type = 'string'; + $data_type = null; $modifiers = []; - $tokens = preg_split('#".*?"(*SKIP)(*F)|\s+#', $definition); + $tokens = preg_split('#".*?"(*SKIP)(*FAIL)|\s+#', $definition); foreach ($tokens as $token) { $parts = explode(':', $token); $value = $parts[0]; @@ -217,6 +233,14 @@ private function buildColumn(string $name, string $definition) } } + if (is_null($data_type)) { + $is_foreign_key = collect($modifiers)->contains(function ($modifier) { + return (is_array($modifier) && key($modifier) === 'foreign') || $modifier === 'foreign'; + }); + + $data_type = $is_foreign_key ? 'id' : 'string'; + } + return new Column($name, $data_type, $modifiers, $attributes ?? []); } } diff --git a/tests/Feature/Generator/FactoryGeneratorTest.php b/tests/Feature/Generator/FactoryGeneratorTest.php index 7c0cc3e0..d4c13f53 100644 --- a/tests/Feature/Generator/FactoryGeneratorTest.php +++ b/tests/Feature/Generator/FactoryGeneratorTest.php @@ -150,6 +150,7 @@ public function modelTreeDataProvider() ['definitions/model-modifiers.bp', 'database/factories/ModifierFactory.php', 'factories/model-modifiers.php'], ['definitions/model-key-constraints.bp', 'database/factories/OrderFactory.php', 'factories/model-key-constraints.php'], ['definitions/unconventional-foreign-key.bp', 'database/factories/StateFactory.php', 'factories/unconventional-foreign-key.php'], + ['definitions/foreign-key-shorthand.bp', 'database/factories/CommentFactory.php', 'factories/foreign-key-shorthand.php'], ]; } } diff --git a/tests/Feature/Generator/MigrationGeneratorTest.php b/tests/Feature/Generator/MigrationGeneratorTest.php index 0c87c07b..615ac6c5 100644 --- a/tests/Feature/Generator/MigrationGeneratorTest.php +++ b/tests/Feature/Generator/MigrationGeneratorTest.php @@ -70,6 +70,29 @@ public function output_writes_migration_for_model_tree($definition, $path, $migr $this->assertEquals(['created' => [$timestamp_path]], $this->subject->output($tree)); } + /** + * @test + */ + public function output_writes_migration_for_foreign_shorthand() + { + $this->files->expects('stub') + ->with('migration.stub') + ->andReturn(file_get_contents('stubs/migration.stub')); + + $now = Carbon::now(); + Carbon::setTestNow($now); + + $timestamp_path = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_comments_table.php'); + + $this->files->expects('put') + ->with($timestamp_path, $this->fixture('migrations/foreign-key-shorthand.php')); + + $tokens = $this->blueprint->parse($this->fixture('definitions/foreign-key-shorthand.bp')); + $tree = $this->blueprint->analyze($tokens); + + $this->assertEquals(['created' => [$timestamp_path]], $this->subject->output($tree)); + } + /** * @test */ @@ -269,7 +292,6 @@ public function output_also_creates_constraints_for_pivot_table_migration() $this->assertEquals(['created' => [$model_migration, $pivot_migration]], $this->subject->output($tree)); } - /** * @test */ diff --git a/tests/Feature/Generator/ModelGeneratorTest.php b/tests/Feature/Generator/ModelGeneratorTest.php index 8eabecb6..390c7e27 100644 --- a/tests/Feature/Generator/ModelGeneratorTest.php +++ b/tests/Feature/Generator/ModelGeneratorTest.php @@ -425,6 +425,7 @@ public function docBlockModelsDataProvider() ['definitions/soft-deletes.bp', 'app/Comment.php', 'models/soft-deletes-phpdoc.php'], ['definitions/relationships.bp', 'app/Comment.php', 'models/relationships-phpdoc.php'], ['definitions/disable-auto-columns.bp', 'app/State.php', 'models/disable-auto-columns-phpdoc.php'], + ['definitions/foreign-key-shorthand.bp', 'app/Comment.php', 'models/foreign-key-shorthand-phpdoc.php'], ]; } } diff --git a/tests/Feature/Lexers/ModelLexerTest.php b/tests/Feature/Lexers/ModelLexerTest.php index 428fbc58..02dd018c 100644 --- a/tests/Feature/Lexers/ModelLexerTest.php +++ b/tests/Feature/Lexers/ModelLexerTest.php @@ -381,6 +381,113 @@ public function it_enables_soft_deletes() $this->assertEquals([], $columns['id']->modifiers()); } + /** + * @test + */ + public function it_converts_foreign_shorthand_to_id() + { + $tokens = [ + 'models' => [ + 'Model' => [ + 'post_id' => 'foreign', + 'author_id' => 'foreign:user', + ], + ], + ]; + + $actual = $this->subject->analyze($tokens); + + $this->assertIsArray($actual['models']); + $this->assertCount(1, $actual['models']); + + $model = $actual['models']['Model']; + $this->assertEquals('Model', $model->name()); + $this->assertTrue($model->usesTimestamps()); + $this->assertFalse($model->usesSoftDeletes()); + + $columns = $model->columns(); + $this->assertCount(3, $columns); + $this->assertEquals('id', $columns['id']->name()); + $this->assertEquals('id', $columns['id']->dataType()); + $this->assertEquals([], $columns['id']->modifiers()); + $this->assertEquals('post_id', $columns['post_id']->name()); + $this->assertEquals('id', $columns['post_id']->dataType()); + $this->assertEquals(['foreign'], $columns['post_id']->modifiers()); + $this->assertEquals('author_id', $columns['author_id']->name()); + $this->assertEquals('id', $columns['author_id']->dataType()); + $this->assertEquals([['foreign' => 'user']], $columns['author_id']->modifiers()); + } + + /** + * @test + */ + public function it_sets_belongs_to_with_foreign_attributes() + { + $tokens = [ + 'models' => [ + 'Model' => [ + 'post_id' => 'id foreign', + 'author_id' => 'id foreign:users', + 'uid' => 'id:user foreign:users.id', + 'cntry_id' => 'foreign:countries', + 'ccid' => 'foreign:countries.code', + ], + ], + ]; + + $actual = $this->subject->analyze($tokens); + + $this->assertIsArray($actual['models']); + $this->assertCount(1, $actual['models']); + + $model = $actual['models']['Model']; + $this->assertEquals('Model', $model->name()); + $this->assertTrue($model->usesTimestamps()); + $this->assertFalse($model->usesSoftDeletes()); + + $columns = $model->columns(); + $this->assertCount(6, $columns); + $this->assertEquals('id', $columns['id']->name()); + $this->assertEquals('id', $columns['id']->dataType()); + $this->assertEquals([], $columns['id']->attributes()); + $this->assertEquals([], $columns['id']->modifiers()); + + $this->assertEquals('post_id', $columns['post_id']->name()); + $this->assertEquals('id', $columns['post_id']->dataType()); + $this->assertEquals([], $columns['post_id']->attributes()); + $this->assertEquals(['foreign'], $columns['post_id']->modifiers()); + + $this->assertEquals('author_id', $columns['author_id']->name()); + $this->assertEquals('id', $columns['author_id']->dataType()); + $this->assertEquals([], $columns['author_id']->attributes()); + $this->assertEquals([['foreign' => 'users']], $columns['author_id']->modifiers()); + + $this->assertEquals('uid', $columns['uid']->name()); + $this->assertEquals('id', $columns['uid']->dataType()); + $this->assertEquals(['user'], $columns['uid']->attributes()); + $this->assertEquals([['foreign' => 'users.id']], $columns['uid']->modifiers()); + + $this->assertEquals('cntry_id', $columns['cntry_id']->name()); + $this->assertEquals('id', $columns['cntry_id']->dataType()); + $this->assertEquals([], $columns['cntry_id']->attributes()); + $this->assertEquals([['foreign' => 'countries']], $columns['cntry_id']->modifiers()); + + $this->assertEquals('ccid', $columns['ccid']->name()); + $this->assertEquals('id', $columns['ccid']->dataType()); + $this->assertEquals([], $columns['ccid']->attributes()); + $this->assertEquals([['foreign' => 'countries.code']], $columns['ccid']->modifiers()); + + $relationships = $model->relationships(); + $this->assertCount(1, $relationships); + $this->assertEquals([ + 'post_id', + 'user:author_id', + 'user:uid', + 'country:cntry_id', + 'country.code:ccid', + ], $relationships['belongsTo']); + } + /** * @test */ diff --git a/tests/fixtures/definitions/foreign-key-shorthand.bp b/tests/fixtures/definitions/foreign-key-shorthand.bp new file mode 100644 index 00000000..e588c4e4 --- /dev/null +++ b/tests/fixtures/definitions/foreign-key-shorthand.bp @@ -0,0 +1,5 @@ +models: + Comment: + post_id: foreign + author_id: foreign:user + ccid: foreign:countries.code diff --git a/tests/fixtures/factories/foreign-key-shorthand.php b/tests/fixtures/factories/foreign-key-shorthand.php new file mode 100644 index 00000000..978b000d --- /dev/null +++ b/tests/fixtures/factories/foreign-key-shorthand.php @@ -0,0 +1,16 @@ +define(Comment::class, function (Faker $faker) { + return [ + 'post_id' => factory(\App\Post::class), + 'author_id' => factory(\App\User::class), + 'ccid' => function () { + return factory(\App\Country::class)->create()->code; + }, + ]; +}); diff --git a/tests/fixtures/migrations/foreign-key-shorthand.php b/tests/fixtures/migrations/foreign-key-shorthand.php new file mode 100644 index 00000000..f4d0586a --- /dev/null +++ b/tests/fixtures/migrations/foreign-key-shorthand.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('post_id')->constrained()->cascadeOnDelete(); + $table->foreignId('author_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('ccid')->constrained('countries', 'code')->cascadeOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('comments'); + } +} diff --git a/tests/fixtures/models/foreign-key-shorthand-phpdoc.php b/tests/fixtures/models/foreign-key-shorthand-phpdoc.php new file mode 100644 index 00000000..be2874c7 --- /dev/null +++ b/tests/fixtures/models/foreign-key-shorthand-phpdoc.php @@ -0,0 +1,64 @@ + 'integer', + 'post_id' => 'integer', + 'author_id' => 'integer', + 'ccid' => 'integer', + ]; + + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function post() + { + return $this->belongsTo(\App\Post::class); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function author() + { + return $this->belongsTo(\App\User::class); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function country() + { + return $this->belongsTo(\App\Country::class, 'ccid', 'code'); + } +}