Skip to content

Commit 9865ae8

Browse files
Generate correct reference for nested models (#447)
1 parent ac7a022 commit 9865ae8

File tree

8 files changed

+228
-11
lines changed

8 files changed

+228
-11
lines changed

src/Generators/FactoryGenerator.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class FactoryGenerator implements Generator
1717
/** @var \Illuminate\Contracts\Filesystem\Filesystem */
1818
private $files;
1919

20+
/** @var Tree */
21+
private $tree;
22+
2023
private $imports = [];
2124

2225
public function __construct($files)
@@ -26,6 +29,8 @@ public function __construct($files)
2629

2730
public function output(Tree $tree): array
2831
{
32+
$this->tree = $tree;
33+
2934
$output = [];
3035

3136
if (Blueprint::isLaravel8OrHigher()) {
@@ -127,9 +132,10 @@ protected function buildDefinition(Model $model)
127132
}
128133

129134
$class = Str::studly(Str::singular($table));
135+
$reference = $this->fullyQualifyModelReference($class) ?? $model;
130136

131137
if (Blueprint::isLaravel8OrHigher()) {
132-
$this->addImport($model, $model->fullyQualifiedNamespace() . '\\' . $class);
138+
$this->addImport($model, $reference->fullyQualifiedNamespace() . '\\' . $class);
133139
}
134140
if ($key === 'id') {
135141
if (Blueprint::isLaravel8OrHigher()) {
@@ -138,7 +144,7 @@ protected function buildDefinition(Model $model)
138144
$definition .= ',' . PHP_EOL;
139145
} else {
140146
$definition .= str_repeat(self::INDENT, 2) . "'{$column->name()}' => ";
141-
$definition .= sprintf('factory(%s::class)', '\\' . $model->fullyQualifiedNamespace() . '\\' . $class);
147+
$definition .= sprintf('factory(%s::class)', '\\' . $reference->fullyQualifiedNamespace() . '\\' . $class);
142148
$definition .= ',' . PHP_EOL;
143149
}
144150
} else {
@@ -157,14 +163,15 @@ protected function buildDefinition(Model $model)
157163
} elseif ($column->dataType() === 'id' || ($column->dataType() === 'uuid' && Str::endsWith($column->name(), '_id'))) {
158164
$name = Str::beforeLast($column->name(), '_id');
159165
$class = Str::studly($column->attributes()[0] ?? $name);
166+
$reference = $this->fullyQualifyModelReference($class) ?? $model;
160167

161168
if (Blueprint::isLaravel8OrHigher()) {
162-
$this->addImport($model, $model->fullyQualifiedNamespace() . '\\' . $class);
169+
$this->addImport($model, $reference->fullyQualifiedNamespace() . '\\' . $class);
163170
$definition .= str_repeat(self::INDENT, 3) . "'{$column->name()}' => ";
164171
$definition .= sprintf('%s::factory()', $class);
165172
} else {
166173
$definition .= str_repeat(self::INDENT, 2) . "'{$column->name()}' => ";
167-
$definition .= sprintf('factory(%s::class)', '\\' . $model->fullyQualifiedNamespace() . '\\' . $class);
174+
$definition .= sprintf('factory(%s::class)', '\\' . $reference->fullyQualifiedNamespace() . '\\' . $class);
168175
}
169176
$definition .= ',' . PHP_EOL;
170177
} elseif (in_array($column->dataType(), ['enum', 'set']) && !empty($column->attributes())) {
@@ -286,4 +293,9 @@ private function fillableColumns(array $columns): array
286293
return !in_array('nullable', $column->modifiers());
287294
});
288295
}
296+
297+
private function fullyQualifyModelReference(string $model_name)
298+
{
299+
return $this->tree->modelForContext($model_name);
300+
}
289301
}

src/Generators/ModelGenerator.php

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@ class ModelGenerator implements Generator
1414
/** @var \Illuminate\Contracts\Filesystem\Filesystem */
1515
private $files;
1616

17+
/** @var Tree */
18+
private $tree;
19+
1720
public function __construct($files)
1821
{
1922
$this->files = $files;
2023
}
2124

2225
public function output(Tree $tree): array
2326
{
27+
$this->tree = $tree;
28+
2429
$output = [];
2530

2631
if (Blueprint::isLaravel8OrHigher()) {
@@ -210,19 +215,20 @@ protected function buildRelationships(Model $model)
210215
}
211216

212217
$class_name = Str::studly($class ?? $method_name);
218+
$fqcn = $this->fullyQualifyModelReference($class_name) ?? $model->fullyQualifiedNamespace() . '\\' . $class_name;
213219

214220
if ($type === 'morphTo') {
215221
$relationship = sprintf('$this->%s()', $type);
216222
} elseif ($type === 'morphMany' || $type === 'morphOne') {
217223
$relation = Str::lower(Str::singular($column_name)) . 'able';
218-
$relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class_name, $relation);
219-
} elseif (! is_null($key)) {
220-
$relationship = sprintf('$this->%s(%s::class, \'%s\', \'%s\')', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class_name, $column_name, $key);
221-
} elseif (! is_null($class) && $type === 'belongsToMany') {
222-
$relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class_name, $column_name);
224+
$relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, '\\' . $fqcn, $relation);
225+
} elseif (!is_null($key)) {
226+
$relationship = sprintf('$this->%s(%s::class, \'%s\', \'%s\')', $type, '\\' . $fqcn, $column_name, $key);
227+
} elseif (!is_null($class) && $type === 'belongsToMany') {
228+
$relationship = sprintf('$this->%s(%s::class, \'%s\')', $type, '\\' . $fqcn, $column_name);
223229
$column_name = $class;
224230
} else {
225-
$relationship = sprintf('$this->%s(%s::class)', $type, '\\' . $model->fullyQualifiedNamespace() . '\\' . $class_name);
231+
$relationship = sprintf('$this->%s(%s::class)', $type, '\\' . $fqcn);
226232
}
227233

228234
if ($type === 'morphTo') {
@@ -408,4 +414,20 @@ private function phpDataType(string $dataType)
408414

409415
return $php_data_types[strtolower($dataType)] ?? 'string';
410416
}
417+
418+
private function fullyQualifyModelReference(string $model_name)
419+
{
420+
// TODO: get model_name from tree.
421+
// If not found, assume parallel namespace as controller.
422+
// Use respond-statement.php as test case.
423+
424+
/** @var \Blueprint\Models\Model $model */
425+
$model = $this->tree->modelForContext($model_name);
426+
427+
if (isset($model)) {
428+
return $model->fullyQualifiedClassName();
429+
}
430+
431+
return null;
432+
}
411433
}

src/Tree.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function modelForContext(string $context)
4646
});
4747

4848
if (count($matches) === 1) {
49-
return $this->models[$matches[0]];
49+
return $this->models[current($matches)];
5050
}
5151
}
5252

tests/Feature/Generators/FactoryGeneratorTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,42 @@ public function output_using_return_types()
169169
$this->assertEquals(['created' => ['database/factories/PostFactory.php']], $this->subject->output($tree));
170170
}
171171

172+
/**
173+
* @test
174+
* @environment-setup useLaravel8
175+
*/
176+
public function output_generates_references_for_nested_models()
177+
{
178+
$this->files->expects('stub')
179+
->with($this->factoryStub)
180+
->andReturn($this->stub($this->factoryStub));
181+
182+
$this->files->expects('exists')
183+
->times(4)
184+
->andReturnTrue();
185+
186+
$this->files->expects('put')
187+
->with('database/factories/QuestionTypeFactory.php', \Mockery::type('string'));
188+
$this->files->expects('put')
189+
->with('database/factories/Appointment/AppointmentTypeFactory.php', \Mockery::type('string'));
190+
$this->files->expects('put')
191+
->with('database/factories/Screening/ReportFactory.php', \Mockery::type('string'));
192+
$this->files->expects('put')
193+
->with('database/factories/Screening/ScreeningQuestionFactory.php', $this->fixture('factories/nested-models-laravel8.php'));
194+
195+
$tokens = $this->blueprint->parse($this->fixture('drafts/nested-models.yaml'));
196+
$tree = $this->blueprint->analyze($tokens);
197+
198+
$this->assertEquals([
199+
'created' => [
200+
'database/factories/QuestionTypeFactory.php',
201+
'database/factories/Appointment/AppointmentTypeFactory.php',
202+
'database/factories/Screening/ReportFactory.php',
203+
'database/factories/Screening/ScreeningQuestionFactory.php',
204+
],
205+
], $this->subject->output($tree));
206+
}
207+
172208
/**
173209
* @test
174210
* @environment-setup useLaravel8

tests/Feature/Generators/ModelGeneratorTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,55 @@ public function output_generates_models_with_guarded_property_when_config_option
581581
$this->assertEquals(['created' => ['app/Comment.php']], $this->subject->output($tree));
582582
}
583583

584+
/**
585+
* @test
586+
* @environment-setup useLaravel8
587+
*/
588+
public function output_generates_models_with_namespaces_correctly()
589+
{
590+
$this->app['config']->set('blueprint.models_namespace', 'Models');
591+
592+
$this->files->expects('stub')
593+
->with($this->modelStub)
594+
->andReturn($this->stub($this->modelStub));
595+
$this->files->expects('stub')
596+
->times(4)
597+
->with('model.fillable.stub')
598+
->andReturn($this->stub('model.fillable.stub'));
599+
$this->files->expects('stub')
600+
->times(4)
601+
->with('model.casts.stub')
602+
->andReturn($this->stub('model.casts.stub'));
603+
$this->files->expects('stub')
604+
->times(4)
605+
->with('model.method.stub')
606+
->andReturn($this->stub('model.method.stub'));
607+
608+
$this->files->expects('exists')
609+
->times(4)
610+
->andReturnTrue();
611+
$this->files->expects('put')
612+
->with('app/Models/QuestionType.php', \Mockery::type('string'));
613+
$this->files->expects('put')
614+
->with('app/Models/Appointment/AppointmentType.php', \Mockery::type('string'));
615+
$this->files->expects('put')
616+
->with('app/Models/Screening/Report.php', \Mockery::type('string'));
617+
$this->files->expects('put')
618+
->with('app/Models/Screening/ScreeningQuestion.php', $this->fixture('models/nested-models-laravel8.php'));
619+
620+
$tokens = $this->blueprint->parse($this->fixture('drafts/nested-models.yaml'));
621+
$tree = $this->blueprint->analyze($tokens);
622+
623+
$this->assertEquals([
624+
'created' => [
625+
'app/Models/QuestionType.php',
626+
'app/Models/Appointment/AppointmentType.php',
627+
'app/Models/Screening/Report.php',
628+
'app/Models/Screening/ScreeningQuestion.php',
629+
],
630+
], $this->subject->output($tree));
631+
}
632+
584633
/**
585634
* @test
586635
* @environment-setup useLaravel8
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
models:
2+
QuestionType:
3+
description: string
4+
5+
Appointment/AppointmentType:
6+
name: string
7+
8+
Screening/Report:
9+
name: string
10+
11+
Screening/ScreeningQuestion:
12+
report_id: id
13+
appointment_type_id: id
14+
question_type_id: id
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Database\Factories;
4+
5+
use Illuminate\Database\Eloquent\Factories\Factory;
6+
use Illuminate\Support\Str;
7+
use App\Appointment\AppointmentType;
8+
use App\QuestionType;
9+
use App\Screening\Report;
10+
use App\Screening\ScreeningQuestion;
11+
12+
class ScreeningQuestionFactory extends Factory
13+
{
14+
/**
15+
* The name of the factory's corresponding model.
16+
*
17+
* @var string
18+
*/
19+
protected $model = ScreeningQuestion::class;
20+
21+
/**
22+
* Define the model's default state.
23+
*
24+
* @return array
25+
*/
26+
public function definition()
27+
{
28+
return [
29+
'report_id' => Report::factory(),
30+
'appointment_type_id' => AppointmentType::factory(),
31+
'question_type_id' => QuestionType::factory(),
32+
];
33+
}
34+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace App\Models\Screening;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
class ScreeningQuestion extends Model
9+
{
10+
use HasFactory;
11+
12+
/**
13+
* The attributes that are mass assignable.
14+
*
15+
* @var array
16+
*/
17+
protected $fillable = [
18+
'report_id',
19+
'appointment_type_id',
20+
'question_type_id',
21+
];
22+
23+
/**
24+
* The attributes that should be cast to native types.
25+
*
26+
* @var array
27+
*/
28+
protected $casts = [
29+
'id' => 'integer',
30+
'report_id' => 'integer',
31+
'appointment_type_id' => 'integer',
32+
'question_type_id' => 'integer',
33+
];
34+
35+
36+
public function report()
37+
{
38+
return $this->belongsTo(\App\Models\Screening\Report::class);
39+
}
40+
41+
public function appointmentType()
42+
{
43+
return $this->belongsTo(\App\Models\Appointment\AppointmentType::class);
44+
}
45+
46+
public function questionType()
47+
{
48+
return $this->belongsTo(\App\Models\QuestionType::class);
49+
}
50+
}

0 commit comments

Comments
 (0)