Skip to content

Commit 3c638a5

Browse files
Support for foreign data type shorthand (#224)
1 parent b789904 commit 3c638a5

File tree

10 files changed

+308
-17
lines changed

10 files changed

+308
-17
lines changed

src/Generators/ModelGenerator.php

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,29 +137,46 @@ private function buildRelationships(Model $model)
137137

138138
foreach ($model->relationships() as $type => $references) {
139139
foreach ($references as $reference) {
140+
$key = null;
141+
$class = null;
142+
143+
$column_name = $reference;
144+
$method_name = Str::beforeLast($reference, '_id');
145+
140146
if (Str::contains($reference, ':')) {
141-
[$class, $name] = explode(':', $reference);
142-
} else {
143-
$name = $reference;
144-
$class = null;
147+
[$foreign_reference, $column_name] = explode(':', $reference);
148+
$method_name = Str::beforeLast($column_name, '_id');
149+
150+
if (Str::contains($foreign_reference, '.')) {
151+
[$class, $key] = explode('.', $foreign_reference);
152+
153+
if ($key === 'id') {
154+
$key = null;
155+
} else {
156+
$method_name = Str::lower($class);
157+
}
158+
} else {
159+
$class = $foreign_reference;
160+
}
145161
}
146162

147-
$name = Str::beforeLast($name, '_id');
148-
$class = Str::studly($class ?? $name);
163+
$class = Str::studly($class ?? $method_name);
149164

150165
if ($type === 'morphTo') {
151166
$relationship = sprintf('$this->%s()', $type);
152167
} elseif ($type === 'morphMany' || $type === 'morphOne') {
153-
$relation = Str::of($name)->lower()->singular() . 'able';
168+
$relation = Str::lower(Str::singular($column_name)) . 'able';
154169
$relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class, $relation);
170+
} elseif (!is_null($key)) {
171+
$relationship = sprintf('$this->%s(%s::class, \'%s\', \'%s\')', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class, $column_name, $key);
155172
} else {
156173
$relationship = sprintf('$this->%s(%s::class)', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class);
157174
}
158175

159176
if ($type === 'morphTo') {
160177
$method_name = Str::lower($class);
161-
} else {
162-
$method_name = in_array($type, ['hasMany', 'belongsToMany', 'morphMany']) ? Str::plural($name) : $name;
178+
} elseif (in_array($type, ['hasMany', 'belongsToMany', 'morphMany'])) {
179+
$method_name = Str::plural($column_name);
163180
}
164181
$method = str_replace('DummyName', Str::camel($method_name), $template);
165182
$method = str_replace('null', $relationship, $method);

src/Lexers/ModelLexer.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Blueprint\Contracts\Lexer;
66
use Blueprint\Models\Column;
77
use Blueprint\Models\Model;
8+
use Illuminate\Support\Str;
89

910
class ModelLexer implements Lexer
1011
{
@@ -172,12 +173,27 @@ private function buildModel(string $name, array $columns)
172173
$column = $this->buildColumn($name, $definition);
173174
$model->addColumn($column);
174175

175-
if ($column->name() !== 'id' && in_array($column->dataType(), ['id', 'uuid'])) {
176-
if ($column->attributes()) {
177-
$model->addRelationship('belongsTo', $column->attributes()[0] . ':' . $column->name());
178-
} else {
179-
$model->addRelationship('belongsTo', $column->name());
176+
$foreign = collect($column->modifiers())->filter(function ($modifier) {
177+
return (is_array($modifier) && key($modifier) === 'foreign') || $modifier === 'foreign';
178+
})->flatten()->first();
179+
180+
if ($column->name() !== 'id' && (in_array($column->dataType(), ['id', 'uuid']) || $foreign)) {
181+
$reference = $column->name();
182+
183+
if ($foreign && $foreign !== 'foreign') {
184+
$table = $foreign;
185+
$key = 'id';
186+
187+
if (Str::contains($foreign, '.')) {
188+
[$table, $key] = explode('.', $foreign);
189+
}
190+
191+
$reference = Str::singular($table) . ($key === 'id' ? '' : '.' . $key) . ':' . $column->name();
192+
} elseif ($column->attributes()) {
193+
$reference = $column->attributes()[0] . ':' . $column->name();
180194
}
195+
196+
$model->addRelationship('belongsTo', $reference);
181197
}
182198
}
183199

@@ -186,10 +202,10 @@ private function buildModel(string $name, array $columns)
186202

187203
private function buildColumn(string $name, string $definition)
188204
{
189-
$data_type = 'string';
205+
$data_type = null;
190206
$modifiers = [];
191207

192-
$tokens = preg_split('#".*?"(*SKIP)(*F)|\s+#', $definition);
208+
$tokens = preg_split('#".*?"(*SKIP)(*FAIL)|\s+#', $definition);
193209
foreach ($tokens as $token) {
194210
$parts = explode(':', $token);
195211
$value = $parts[0];
@@ -217,6 +233,14 @@ private function buildColumn(string $name, string $definition)
217233
}
218234
}
219235

236+
if (is_null($data_type)) {
237+
$is_foreign_key = collect($modifiers)->contains(function ($modifier) {
238+
return (is_array($modifier) && key($modifier) === 'foreign') || $modifier === 'foreign';
239+
});
240+
241+
$data_type = $is_foreign_key ? 'id' : 'string';
242+
}
243+
220244
return new Column($name, $data_type, $modifiers, $attributes ?? []);
221245
}
222246
}

tests/Feature/Generator/FactoryGeneratorTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ public function modelTreeDataProvider()
150150
['definitions/model-modifiers.bp', 'database/factories/ModifierFactory.php', 'factories/model-modifiers.php'],
151151
['definitions/model-key-constraints.bp', 'database/factories/OrderFactory.php', 'factories/model-key-constraints.php'],
152152
['definitions/unconventional-foreign-key.bp', 'database/factories/StateFactory.php', 'factories/unconventional-foreign-key.php'],
153+
['definitions/foreign-key-shorthand.bp', 'database/factories/CommentFactory.php', 'factories/foreign-key-shorthand.php'],
153154
];
154155
}
155156
}

tests/Feature/Generator/MigrationGeneratorTest.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,29 @@ public function output_writes_migration_for_model_tree($definition, $path, $migr
7070
$this->assertEquals(['created' => [$timestamp_path]], $this->subject->output($tree));
7171
}
7272

73+
/**
74+
* @test
75+
*/
76+
public function output_writes_migration_for_foreign_shorthand()
77+
{
78+
$this->files->expects('stub')
79+
->with('migration.stub')
80+
->andReturn(file_get_contents('stubs/migration.stub'));
81+
82+
$now = Carbon::now();
83+
Carbon::setTestNow($now);
84+
85+
$timestamp_path = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_comments_table.php');
86+
87+
$this->files->expects('put')
88+
->with($timestamp_path, $this->fixture('migrations/foreign-key-shorthand.php'));
89+
90+
$tokens = $this->blueprint->parse($this->fixture('definitions/foreign-key-shorthand.bp'));
91+
$tree = $this->blueprint->analyze($tokens);
92+
93+
$this->assertEquals(['created' => [$timestamp_path]], $this->subject->output($tree));
94+
}
95+
7396
/**
7497
* @test
7598
*/
@@ -269,7 +292,6 @@ public function output_also_creates_constraints_for_pivot_table_migration()
269292
$this->assertEquals(['created' => [$model_migration, $pivot_migration]], $this->subject->output($tree));
270293
}
271294

272-
273295
/**
274296
* @test
275297
*/

tests/Feature/Generator/ModelGeneratorTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ public function docBlockModelsDataProvider()
425425
['definitions/soft-deletes.bp', 'app/Comment.php', 'models/soft-deletes-phpdoc.php'],
426426
['definitions/relationships.bp', 'app/Comment.php', 'models/relationships-phpdoc.php'],
427427
['definitions/disable-auto-columns.bp', 'app/State.php', 'models/disable-auto-columns-phpdoc.php'],
428+
['definitions/foreign-key-shorthand.bp', 'app/Comment.php', 'models/foreign-key-shorthand-phpdoc.php'],
428429
];
429430
}
430431
}

tests/Feature/Lexers/ModelLexerTest.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,113 @@ public function it_enables_soft_deletes()
381381
$this->assertEquals([], $columns['id']->modifiers());
382382
}
383383

384+
/**
385+
* @test
386+
*/
387+
public function it_converts_foreign_shorthand_to_id()
388+
{
389+
$tokens = [
390+
'models' => [
391+
'Model' => [
392+
'post_id' => 'foreign',
393+
'author_id' => 'foreign:user',
394+
],
395+
],
396+
];
397+
398+
$actual = $this->subject->analyze($tokens);
399+
400+
$this->assertIsArray($actual['models']);
401+
$this->assertCount(1, $actual['models']);
402+
403+
$model = $actual['models']['Model'];
404+
$this->assertEquals('Model', $model->name());
405+
$this->assertTrue($model->usesTimestamps());
406+
$this->assertFalse($model->usesSoftDeletes());
407+
408+
$columns = $model->columns();
409+
$this->assertCount(3, $columns);
410+
$this->assertEquals('id', $columns['id']->name());
411+
$this->assertEquals('id', $columns['id']->dataType());
412+
$this->assertEquals([], $columns['id']->modifiers());
413+
$this->assertEquals('post_id', $columns['post_id']->name());
414+
$this->assertEquals('id', $columns['post_id']->dataType());
415+
$this->assertEquals(['foreign'], $columns['post_id']->modifiers());
416+
$this->assertEquals('author_id', $columns['author_id']->name());
417+
$this->assertEquals('id', $columns['author_id']->dataType());
418+
$this->assertEquals([['foreign' => 'user']], $columns['author_id']->modifiers());
419+
}
420+
421+
/**
422+
* @test
423+
*/
424+
public function it_sets_belongs_to_with_foreign_attributes()
425+
{
426+
$tokens = [
427+
'models' => [
428+
'Model' => [
429+
'post_id' => 'id foreign',
430+
'author_id' => 'id foreign:users',
431+
'uid' => 'id:user foreign:users.id',
432+
'cntry_id' => 'foreign:countries',
433+
'ccid' => 'foreign:countries.code',
434+
],
435+
],
436+
];
437+
438+
$actual = $this->subject->analyze($tokens);
439+
440+
$this->assertIsArray($actual['models']);
441+
$this->assertCount(1, $actual['models']);
442+
443+
$model = $actual['models']['Model'];
444+
$this->assertEquals('Model', $model->name());
445+
$this->assertTrue($model->usesTimestamps());
446+
$this->assertFalse($model->usesSoftDeletes());
447+
448+
$columns = $model->columns();
449+
$this->assertCount(6, $columns);
450+
$this->assertEquals('id', $columns['id']->name());
451+
$this->assertEquals('id', $columns['id']->dataType());
452+
$this->assertEquals([], $columns['id']->attributes());
453+
$this->assertEquals([], $columns['id']->modifiers());
454+
455+
$this->assertEquals('post_id', $columns['post_id']->name());
456+
$this->assertEquals('id', $columns['post_id']->dataType());
457+
$this->assertEquals([], $columns['post_id']->attributes());
458+
$this->assertEquals(['foreign'], $columns['post_id']->modifiers());
459+
460+
$this->assertEquals('author_id', $columns['author_id']->name());
461+
$this->assertEquals('id', $columns['author_id']->dataType());
462+
$this->assertEquals([], $columns['author_id']->attributes());
463+
$this->assertEquals([['foreign' => 'users']], $columns['author_id']->modifiers());
464+
465+
$this->assertEquals('uid', $columns['uid']->name());
466+
$this->assertEquals('id', $columns['uid']->dataType());
467+
$this->assertEquals(['user'], $columns['uid']->attributes());
468+
$this->assertEquals([['foreign' => 'users.id']], $columns['uid']->modifiers());
469+
470+
$this->assertEquals('cntry_id', $columns['cntry_id']->name());
471+
$this->assertEquals('id', $columns['cntry_id']->dataType());
472+
$this->assertEquals([], $columns['cntry_id']->attributes());
473+
$this->assertEquals([['foreign' => 'countries']], $columns['cntry_id']->modifiers());
474+
475+
$this->assertEquals('ccid', $columns['ccid']->name());
476+
$this->assertEquals('id', $columns['ccid']->dataType());
477+
$this->assertEquals([], $columns['ccid']->attributes());
478+
$this->assertEquals([['foreign' => 'countries.code']], $columns['ccid']->modifiers());
479+
480+
$relationships = $model->relationships();
481+
$this->assertCount(1, $relationships);
482+
$this->assertEquals([
483+
'post_id',
484+
'user:author_id',
485+
'user:uid',
486+
'country:cntry_id',
487+
'country.code:ccid',
488+
], $relationships['belongsTo']);
489+
}
490+
384491
/**
385492
* @test
386493
*/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
models:
2+
Comment:
3+
post_id: foreign
4+
author_id: foreign:user
5+
ccid: foreign:countries.code
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/** @var \Illuminate\Database\Eloquent\Factory $factory */
4+
5+
use App\Comment;
6+
use Faker\Generator as Faker;
7+
8+
$factory->define(Comment::class, function (Faker $faker) {
9+
return [
10+
'post_id' => factory(\App\Post::class),
11+
'author_id' => factory(\App\User::class),
12+
'ccid' => function () {
13+
return factory(\App\Country::class)->create()->code;
14+
},
15+
];
16+
});
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 CreateCommentsTable extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('comments', function (Blueprint $table) {
17+
$table->id();
18+
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
19+
$table->foreignId('author_id')->constrained('users')->cascadeOnDelete();
20+
$table->foreignId('ccid')->constrained('countries', 'code')->cascadeOnDelete();
21+
$table->timestamps();
22+
});
23+
}
24+
25+
/**
26+
* Reverse the migrations.
27+
*
28+
* @return void
29+
*/
30+
public function down()
31+
{
32+
Schema::dropIfExists('comments');
33+
}
34+
}

0 commit comments

Comments
 (0)