Skip to content

Commit

Permalink
Support for foreign data type shorthand (#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonmccreary authored May 21, 2020
1 parent b789904 commit 3c638a5
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 17 deletions.
35 changes: 26 additions & 9 deletions src/Generators/ModelGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
38 changes: 31 additions & 7 deletions src/Lexers/ModelLexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Blueprint\Contracts\Lexer;
use Blueprint\Models\Column;
use Blueprint\Models\Model;
use Illuminate\Support\Str;

class ModelLexer implements Lexer
{
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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];
Expand Down Expand Up @@ -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 ?? []);
}
}
1 change: 1 addition & 0 deletions tests/Feature/Generator/FactoryGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
];
}
}
24 changes: 23 additions & 1 deletion tests/Feature/Generator/MigrationGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand Down
1 change: 1 addition & 0 deletions tests/Feature/Generator/ModelGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
];
}
}
107 changes: 107 additions & 0 deletions tests/Feature/Lexers/ModelLexerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/definitions/foreign-key-shorthand.bp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
models:
Comment:
post_id: foreign
author_id: foreign:user
ccid: foreign:countries.code
16 changes: 16 additions & 0 deletions tests/fixtures/factories/foreign-key-shorthand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Comment;
use Faker\Generator as Faker;

$factory->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;
},
];
});
34 changes: 34 additions & 0 deletions tests/fixtures/migrations/foreign-key-shorthand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCommentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->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');
}
}
Loading

0 comments on commit 3c638a5

Please sign in to comment.