Skip to content

Commit d15b5de

Browse files
Generate foreign key constraints (#154)
1 parent 799612c commit d15b5de

25 files changed

+517
-43
lines changed

config/blueprint.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,17 @@
5656
*/
5757
'generate_phpdocs' => false,
5858

59+
/*
60+
|--------------------------------------------------------------------------
61+
| Foreign Key Constraints
62+
|--------------------------------------------------------------------------
63+
|
64+
| Here you may enable foreign key constraints for the migrations. This
65+
| will link records in the defined tables together and help the database
66+
| to be more structured and readable.
67+
|
68+
*/
69+
'use_constraints' => false,
70+
5971
'fake_nullables' => true,
6072
];

src/Generators/FactoryGenerator.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,14 @@ protected function buildDefinition(Model $model)
7373
}
7474

7575
if (in_array($column->dataType(), ['id', 'uuid'])) {
76-
$name = Str::beforeLast($column->name(), '_id');
77-
$class = Str::studly($column->attributes()[0] ?? $name);
76+
$foreign = $column->isForeignKey();
77+
78+
if ($foreign && $foreign !== 'foreign') {
79+
$class = Str::studly(Str::singular($foreign));
80+
} else {
81+
$name = Str::beforeLast($column->name(), '_id');
82+
$class = Str::studly($column->attributes()[0] ?? $name);
83+
}
7884

7985
$definition .= self::INDENT . "'{$column->name()}' => ";
8086
$definition .= sprintf('factory(%s::class)', '\\' . $model->fullyQualifiedNamespace() . '\\' . $class);

src/Generators/MigrationGenerator.php

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -108,41 +108,59 @@ protected function buildDefinition(Model $model)
108108
$dataType = 'nullable' . ucfirst($dataType);
109109
}
110110

111+
$column_definition = self::INDENT;
111112
if ($dataType === 'bigIncrements' && $this->isLaravel7orNewer()) {
112-
$definition .= self::INDENT . '$table->id(';
113+
$column_definition .= '$table->id(';
113114
} else {
114-
$definition .= self::INDENT . '$table->' . $dataType . "('{$column->name()}'";
115+
$column_definition .= '$table->' . $dataType . "('{$column->name()}'";
115116
}
116117

117118
if (!empty($column->attributes()) && !in_array($column->dataType(), ['id', 'uuid'])) {
118-
$definition .= ', ';
119+
$column_definition .= ', ';
119120
if (in_array($column->dataType(), ['set', 'enum'])) {
120-
$definition .= json_encode($column->attributes());
121+
$column_definition .= json_encode($column->attributes());
121122
} else {
122-
$definition .= implode(', ', $column->attributes());
123+
$column_definition .= implode(', ', $column->attributes());
123124
}
124125
}
125-
$definition .= ')';
126+
$column_definition .= ')';
127+
128+
$modifiers = $column->modifiers();
126129

127130
$foreign = '';
131+
$foreign_modifier = $column->isForeignKey();
132+
133+
if ($this->shouldAddForeignKeyConstraint($column)) {
134+
$foreign = $this->buildForeignKey($column->name(), $foreign_modifier === 'foreign' ? null : $foreign_modifier, $column->dataType());
135+
if ($column->dataType() === 'id' && $this->isLaravel7orNewer()) {
136+
$column_definition = $foreign;
137+
$foreign = '';
138+
}
139+
140+
// TODO: unset the proper modifier
141+
$modifiers = collect($modifiers)->reject(function ($modifier) {
142+
return (is_array($modifier) && key($modifier) === 'foreign') || $modifier === 'foreign';
143+
});
144+
}
128145

129-
foreach ($column->modifiers() as $modifier) {
146+
foreach ($modifiers as $modifier) {
130147
if (is_array($modifier)) {
131-
if (key($modifier) === 'foreign') {
132-
$foreign = self::INDENT . '$table->foreign(' . "'{$column->name()}')->references('id')->on('" . Str::lower(Str::plural(current($modifier))) . "')->onDelete('cascade');" . PHP_EOL;
133-
} else {
134-
$definition .= '->' . key($modifier) . '(' . current($modifier) . ')';
135-
}
148+
$column_definition .= '->' . key($modifier) . '(' . current($modifier) . ')';
136149
} elseif ($modifier === 'unsigned' && Str::startsWith($dataType, 'unsigned')) {
137150
continue;
138151
} elseif ($modifier === 'nullable' && Str::startsWith($dataType, 'nullable')) {
139152
continue;
140153
} else {
141-
$definition .= '->' . $modifier . '()';
154+
$column_definition .= '->' . $modifier . '()';
142155
}
143156
}
144157

145-
$definition .= ';' . PHP_EOL . $foreign;
158+
$column_definition .= ';' . PHP_EOL;
159+
if (!empty($foreign)) {
160+
$column_definition .= $foreign . ';' . PHP_EOL;
161+
}
162+
163+
$definition .= $column_definition;
146164
}
147165

148166
if ($model->usesSoftDeletes()) {
@@ -156,18 +174,57 @@ protected function buildDefinition(Model $model)
156174
return trim($definition);
157175
}
158176

159-
protected function buildPivotTableDefinition(array $segments, $dataType = 'unsignedBigInteger')
177+
protected function buildPivotTableDefinition(array $segments)
160178
{
161179
$definition = '';
162180

163181
foreach ($segments as $segment) {
164-
$column = strtolower($segment) . '_id';
165-
$definition .= self::INDENT . '$table->' . $dataType . "('{$column}');" . PHP_EOL;
182+
$column = Str::lower($segment);
183+
$references = 'id';
184+
$on = Str::plural($column);
185+
$foreign = Str::singular($column) . '_' . $references;
186+
187+
if (!$this->isLaravel7orNewer()) {
188+
$definition .= self::INDENT . '$table->unsignedBigInteger(\'' . $foreign . '\');' . PHP_EOL;
189+
}
190+
191+
if (config('blueprint.use_constraints')) {
192+
$definition .= $this->buildForeignKey($foreign, $on, 'id') . ';' . PHP_EOL;
193+
} elseif ($this->isLaravel7orNewer()) {
194+
$definition .= self::INDENT . '$table->foreignId(\'' . $foreign . '\');' . PHP_EOL;
195+
}
166196
}
167197

168198
return trim($definition);
169199
}
170200

201+
protected function buildForeignKey(string $column_name, ?string $on, string $type)
202+
{
203+
if (is_null($on)) {
204+
$table = Str::plural(Str::beforeLast($column_name, '_'));
205+
$column = Str::afterLast($column_name, '_');
206+
} elseif (Str::contains($on, '.')) {
207+
[$table, $column] = explode('.', $on);
208+
$table = Str::snake($table);
209+
} else {
210+
$table = Str::plural($on);
211+
$column = Str::afterLast($column_name, '_');
212+
}
213+
214+
if ($this->isLaravel7orNewer() && $type === 'id') {
215+
if ($column_name === Str::singular($table) . '_' . $column) {
216+
return self::INDENT . '$table->foreignId' . "('{$column_name}')->constrained()->cascadeOnDelete()";
217+
}
218+
if ($column === 'id') {
219+
return self::INDENT . '$table->foreignId' . "('{$column_name}')->constrained('{$table}')->cascadeOnDelete()";
220+
}
221+
222+
return self::INDENT . '$table->foreignId' . "('{$column_name}')->constrained('{$table}', '{$column}')->cascadeOnDelete())";
223+
}
224+
225+
return self::INDENT . '$table->foreign' . "('{$column_name}')->references('{$column}')->on('{$table}')->onDelete('cascade')";
226+
}
227+
171228
protected function getClassName(Model $model)
172229
{
173230
return 'Create' . Str::studly($model->tableName()) . 'Table';
@@ -201,4 +258,17 @@ protected function getPivotTableName(array $segments)
201258
sort($segments);
202259
return strtolower(implode('_', $segments));
203260
}
261+
262+
private function shouldAddForeignKeyConstraint(\Blueprint\Models\Column $column)
263+
{
264+
if ($column->name() === 'id') {
265+
return false;
266+
}
267+
268+
if ($column->isForeignKey()) {
269+
return true;
270+
}
271+
272+
return in_array($column->dataType(), ['id', 'uuid']) && config('blueprint.use_constraints');
273+
}
204274
}

src/Models/Column.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ public function modifiers()
3737
return $this->modifiers;
3838
}
3939

40+
public function isForeignKey()
41+
{
42+
return collect($this->modifiers())->filter(function ($modifier) {
43+
return (is_array($modifier) && key($modifier) === 'foreign') || $modifier === 'foreign';
44+
})->flatten()->first();
45+
}
46+
4047
public function defaultValue()
4148
{
4249
return collect($this->modifiers())

src/Models/Model.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ public function addRelationship(string $type, string $reference)
153153

154154
public function addPivotTable(string $reference)
155155
{
156-
$segments = [$this->name(), strtolower($reference)];
156+
$segments = [$this->name(), $reference];
157157
sort($segments);
158158
$this->pivotTables[] = $segments;
159159
}

tests/Feature/Generator/FactoryGeneratorTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,12 @@ public function output_ignores_nullables_if_fake_nullables_configuration_is_set_
8383
->andReturnTrue();
8484

8585
$this->files->expects('put')
86-
->with('database/factories/OrderFactory.php', $this->fixture('factories/fake-nullables.php'));
86+
->with('database/factories/PostFactory.php', $this->fixture('factories/fake-nullables.php'));
8787

88-
$tokens = $this->blueprint->parse($this->fixture('definitions/model-key-constraints.bp'));
88+
$tokens = $this->blueprint->parse($this->fixture('definitions/readme-example.bp'));
8989
$tree = $this->blueprint->analyze($tokens);
9090

91-
$this->assertEquals(['created' => ['database/factories/OrderFactory.php']], $this->subject->output($tree));
91+
$this->assertEquals(['created' => ['database/factories/PostFactory.php']], $this->subject->output($tree));
9292
}
9393

9494
/**
@@ -149,6 +149,7 @@ public function modelTreeDataProvider()
149149
['definitions/unconventional.bp', 'database/factories/TeamFactory.php', 'factories/unconventional.php'],
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'],
152+
// ['definitions/unconventional-foreign-key.bp', 'database/factories/StateFactory.php', 'factories/unconventional-foreign-key.php'],
152153
];
153154
}
154155
}

tests/Feature/Generator/MigrationGeneratorTest.php

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ public function output_uses_past_timestamp_for_multiple_migrations()
9999
/**
100100
* @test
101101
*/
102-
public function output_uses_proper_data_type_for_id_column()
102+
public function output_uses_proper_data_type_for_id_columns_in_laravel6()
103103
{
104104
$app = \Mockery::mock();
105-
$app->expects('version')
105+
$app->shouldReceive('version')
106106
->withNoArgs()
107107
->andReturn('6.0.0');
108108
App::swap($app);
@@ -129,9 +129,35 @@ public function output_uses_proper_data_type_for_id_column()
129129
* @test
130130
*/
131131
public function output_also_creates_pivot_table_migration()
132+
{
133+
$this->files->expects('stub')
134+
->with('migration.stub')
135+
->andReturn(file_get_contents('stubs/migration.stub'));
136+
137+
$now = Carbon::now();
138+
Carbon::setTestNow($now);
139+
140+
$model_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
141+
$pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_diary_journey_table.php');
142+
143+
$this->files->expects('put')
144+
->with($model_migration, $this->fixture('migrations/belongs-to-many.php'));
145+
$this->files->expects('put')
146+
->with($pivot_migration, $this->fixture('migrations/belongs-to-many-pivot.php'));
147+
148+
$tokens = $this->blueprint->parse($this->fixture('definitions/belongs-to-many.bp'));
149+
$tree = $this->blueprint->analyze($tokens);
150+
151+
$this->assertEquals(['created' => [$model_migration, $pivot_migration]], $this->subject->output($tree));
152+
}
153+
154+
/**
155+
* @test
156+
*/
157+
public function output_also_creates_pivot_table_migration_laravel6()
132158
{
133159
$app = \Mockery::mock();
134-
$app->expects('version')
160+
$app->shouldReceive('version')
135161
->withNoArgs()
136162
->andReturn('6.0.0');
137163
App::swap($app);
@@ -147,10 +173,74 @@ public function output_also_creates_pivot_table_migration()
147173
$pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_diary_journey_table.php');
148174

149175
$this->files->expects('put')
150-
->with($model_migration, $this->fixture('migrations/belongs-to-many.php'));
176+
->with($model_migration, $this->fixture('migrations/belongs-to-many-laravel6.php'));
151177

152178
$this->files->expects('put')
153-
->with($pivot_migration, $this->fixture('migrations/belongs-to-many-pivot.php'));
179+
->with($pivot_migration, $this->fixture('migrations/belongs-to-many-pivot-laravel6.php'));
180+
181+
$tokens = $this->blueprint->parse($this->fixture('definitions/belongs-to-many.bp'));
182+
$tree = $this->blueprint->analyze($tokens);
183+
184+
$this->assertEquals(['created' => [$model_migration, $pivot_migration]], $this->subject->output($tree));
185+
}
186+
187+
/**
188+
* @test
189+
*/
190+
public function output_also_creates_constraints_for_pivot_table_migration()
191+
{
192+
$this->app->config->set('blueprint.use_constraints', true);
193+
194+
$this->files->expects('stub')
195+
->with('migration.stub')
196+
->andReturn(file_get_contents('stubs/migration.stub'));
197+
198+
$now = Carbon::now();
199+
Carbon::setTestNow($now);
200+
201+
$model_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
202+
$pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_diary_journey_table.php');
203+
204+
$this->files->expects('put')
205+
->with($model_migration, $this->fixture('migrations/belongs-to-many-key-constraints.php'));
206+
207+
$this->files->expects('put')
208+
->with($pivot_migration, $this->fixture('migrations/belongs-to-many-pivot-key-constraints.php'));
209+
210+
$tokens = $this->blueprint->parse($this->fixture('definitions/belongs-to-many.bp'));
211+
$tree = $this->blueprint->analyze($tokens);
212+
213+
$this->assertEquals(['created' => [$model_migration, $pivot_migration]], $this->subject->output($tree));
214+
}
215+
216+
217+
/**
218+
* @test
219+
*/
220+
public function output_also_creates_constraints_for_pivot_table_migration_laravel6()
221+
{
222+
$this->app->config->set('blueprint.use_constraints', true);
223+
224+
$app = \Mockery::mock();
225+
$app->shouldReceive('version')
226+
->withNoArgs()
227+
->andReturn('6.0.0');
228+
App::swap($app);
229+
230+
$this->files->expects('stub')
231+
->with('migration.stub')
232+
->andReturn(file_get_contents('stubs/migration.stub'));
233+
234+
$now = Carbon::now();
235+
Carbon::setTestNow($now);
236+
237+
$model_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
238+
$pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_diary_journey_table.php');
239+
240+
$this->files->expects('put')
241+
->with($model_migration, $this->fixture('migrations/belongs-to-many-key-constraints-laravel6.php'));
242+
$this->files->expects('put')
243+
->with($pivot_migration, $this->fixture('migrations/belongs-to-many-pivot-key-constraints-laravel6.php'));
154244

155245
$tokens = $this->blueprint->parse($this->fixture('definitions/belongs-to-many.bp'));
156246
$tree = $this->blueprint->analyze($tokens);
@@ -172,6 +262,7 @@ public function modelTreeDataProvider()
172262
['definitions/model-key-constraints.bp', 'database/migrations/timestamp_create_orders_table.php', 'migrations/model-key-constraints.php'],
173263
['definitions/disable-auto-columns.bp', 'database/migrations/timestamp_create_states_table.php', 'migrations/disable-auto-columns.php'],
174264
['definitions/uuid-shorthand.bp', 'database/migrations/timestamp_create_people_table.php', 'migrations/uuid-shorthand.php'],
265+
['definitions/unconventional-foreign-key.bp', 'database/migrations/timestamp_create_states_table.php', 'migrations/unconventional-foreign-key.php'],
175266
];
176267
}
177268
}

tests/TestCase.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ protected function getEnvironmentSetUp($app)
1111
$app['config']->set('blueprint.models_namespace', '');
1212
$app['config']->set('blueprint.app_path', 'app');
1313
$app['config']->set('blueprint.generate_phpdocs', false);
14+
$app['config']->set('blueprint.use_constraints', false);
1415
$app['config']->set('blueprint.fake_nullables', true);
1516
}
1617

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
models:
22
Journey:
33
name: string
4+
user_id: id
45
relationships:
56
belongstoMany: Diary
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
models:
22
Order:
33
id: uuid primary
4-
user_id: id foreign:user
4+
user_id: id foreign
55
external_id: string nullable index
6-
subscription_id: uuid foreign:subscription
6+
sub_id: uuid foreign:subscription
77
expires_at: timestamp nullable index
88
meta: json default:'[]'

0 commit comments

Comments
 (0)