Skip to content

Commit

Permalink
Populate migration file with actual model fields (#935)
Browse files Browse the repository at this point in the history
Documented by wintercms/docs#165
  • Loading branch information
mjauvin committed Nov 1, 2023
1 parent af305ad commit 2309b05
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 4 deletions.
147 changes: 145 additions & 2 deletions console/CreateMigration.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<?php namespace System\Console;

use File;
use InvalidArgumentException;
use System\Classes\VersionManager;
use System\Console\BaseScaffoldCommand;
use Winter\Storm\Database\Model;
use Winter\Storm\Support\Str;
use Yaml;

Expand Down Expand Up @@ -157,8 +159,7 @@ protected function prepareVars(): array
}

if ($this->option('create') && $this->option('update')) {
$this->error('The create & update options cannot both be set at the same time');
return false;
throw new InvalidArgumentException('The create & update options cannot both be set at the same time');
}

if ($this->option('create')) {
Expand All @@ -173,6 +174,10 @@ protected function prepareVars(): array
throw new InvalidArgumentException('The table or model options are required when using the create or update options');
}

if (($table || $model) && !in_array($scaffold, ['create', 'update'])) {
throw new InvalidArgumentException('One of create or update option is required when using the model or table options');
}

$this->stubs = $this->migrationScaffolds[$scaffold];

if (!empty($this->option('for-version'))) {
Expand All @@ -192,13 +197,100 @@ protected function prepareVars(): array
'version' => $version,
];

if (!empty($model)) {
$vars['model'] = $model;
}
if (!empty($table)) {
$vars['table'] = $table;
}

return $vars;
}

/**
* Create vars for model fields mappings so they can be used in update/create stubs
*/
protected function processVars(array $vars): array
{
$vars = parent::processVars($vars);

// --model option needed below
if (empty($vars['model'])) {
return $vars;
}

$vars['fields'] = [];

$fields_path = $vars['plugin_url'] . '/models/' . $vars['lower_model'] . '/fields.yaml';
$fields = Yaml::parseFile(plugins_path($fields_path));

$modelName = $vars['plugin_namespace'] . '\\Models\\' . $vars['model'];

$vars['model'] = $model = new $modelName();

foreach (['fields', 'tabs', 'secondaryTabs'] as $type) {
if (!isset($fields[$type])) {
continue;
}
if ($type === 'fields') {
$fieldList = $fields[$type];
} else {
$fieldList = $fields[$type]['fields'];
}

foreach ($fieldList as $field => $config) {
if (str_contains($field, '@')) {
list($field, $context) = explode('@', $field);
}

$type = $config['type'] ?? 'text';

if (str_starts_with($field, '_')
|| $field === $model->getKeyName()
|| str_contains($field, '[')
|| in_array($type, ['fileupload', 'relation', 'relationmanager', 'repeater', 'section', 'hint'])
|| in_array($field, $model->purgeable ?? [])
|| $model->getRelationType($field)
) {
continue;
}

$vars['fields'][$field] = $this->mapFieldType($field, $config, $model);
}
}

foreach ($model->getRelationDefinitions() as $relationType => $definitions) {
if (in_array($relationType, ['belongsTo', 'hasOne'])) {
foreach (array_keys($definitions) as $relation) {
$vars['fields'][$relation . '_id'] = [
'type' => 'foreignId',
'index' => true,
'required' => true,
];
}
}
}

if ($model->methodExists('getSortOrderColumn')) {
$field = $model->getSortOrderColumn();
$vars['fields'][$field] = [
'type' => 'unsignedinteger',
'required' => false,
'index' => true,
];
}

$vars['primaryKey'] = $model->getKeyName();
$vars['jsonable'] = $model->getJsonable();
$vars['timestamps'] = $model->timestamps;

if ($morphable = $model->morphTo) {
$vars['morphable'] = array_keys($morphable);
}

return $vars;
}

/**
* Get the next version number based on the current number.
*/
Expand All @@ -209,4 +301,55 @@ protected function getNextVersion($currentVersion): string
$parts[count($parts) - 1] = (int) $parts[count($parts) - 1] + 1;
return 'v' . implode('.', $parts);
}

/**
* Maps model fields config to DB Schema column types.
*/
protected function mapFieldType(string $fieldName, array $fieldConfig, ?Model $model = null) : array
{
switch ($fieldConfig['type'] ?? 'text') {
case 'checkbox':
case 'switch':
$dbType = 'boolean';
break;
case 'number':
$dbType = 'double';
if (isset($fieldConfig['step']) && is_int($fieldConfig['step'])) {
$dbType = 'integer';
}
if ($dbType === 'integer' && isset($fieldConfig['min']) && $fieldConfig['min'] >= 0) {
$dbType = 'unsignedInteger';
}
break;
case 'range':
$dbType = 'unsignedInteger';
break;
case 'datepicker':
$dbType = $fieldConfig['mode'] ?? 'datetime';
break;
case 'markdown':
$dbType = 'mediumText';
break;
case 'textarea':
$dbType = 'text';
break;
default:
$dbType = 'string';
}

if ($model) {
$rule = array_get($model->rules ?? [], $fieldName, '');
$rule = is_array($rule) ? implode(',', $rule) : $rule;

$required = str_contains($rule, 'required') ? true : $fieldConfig['required'] ?? false;
} else {
$required = $fieldConfig['required'] ?? false;
}

return [
'type' => $dbType,
'required' => $required,
'index' => in_array($fieldName, ["slug"]) or str_ends_with($fieldName, "_id"),
];
}
}
15 changes: 15 additions & 0 deletions console/scaffold/migration/migration.create.stub
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,23 @@ return new class extends Migration
public function up()
{
Schema::create('{{ table }}', function (Blueprint $table) {
{% if primaryKey %}
$table->increments('{{ primaryKey }}');
{% else %}
$table->id();
{% endif %}
{% for field,config in fields %}
$table->{{ config.type }}('{{ field }}'){{ config.required == false ? '->nullable()' }}{{ config.index ? '->index()' }};
{% endfor %}
{% for field in jsonable %}
$table->mediumText('{{ field }}')->nullable();
{% endfor %}
{% for field in morphable %}
$table->nullableMorphs('{{ field }}', 'morphable_index');
{% endfor %}
{% if not model or timestamps %}
$table->timestamps();
{% endif %}
});
}

Expand Down
20 changes: 18 additions & 2 deletions console/scaffold/migration/migration.update.stub
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ return new class extends Migration
public function up()
{
Schema::table('{{ table }}', function (Blueprint $table) {
//
{% for field,config in fields %}
$table->{{ config.type }}('{{ field }}'){{ config.required == false ? '->nullable()' }}{{ config.index ? '->index()' }};
{% endfor %}
{% for field in jsonable %}
$table->mediumText('{{ field }}')->nullable();
{% endfor %}
{% for field in morphable %}
$table->nullableMorphs('{{ field }}', 'morphable_index');
{% endfor %}
});
}

Expand All @@ -26,7 +34,15 @@ return new class extends Migration
public function down()
{
Schema::table('{{ table }}', function (Blueprint $table) {
//
{% for field,config in fields %}
$table->dropColumn('{{ field }}');
{% endfor %}
{% for field in jsonable %}
$table->dropColumn('{{ field }}');
{% endfor %}
{% for field in morphable %}
$table->dropColumn('{{ field }}');
{% endfor %}
});
}
};
90 changes: 90 additions & 0 deletions tests/console/CreateMigrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

namespace System\Tests\Console;

use File;
use Illuminate\Database\Schema\Blueprint;
use Schema;
use System\Tests\Bootstrap\PluginTestCase;

class CreateMigrationTest extends PluginTestCase
{
public function setUp(): void
{
parent::setUp();

$this->app->setPluginsPath(base_path() . '/modules/system/tests/fixtures/plugins/');

$this->table = 'winter_tester_test_model';
$this->versionFile = plugins_path('winter/tester/updates/version.yaml');
$this->versionFolder = plugins_path('winter/tester/updates/v0.0.1');

File::copy($this->versionFile, $this->versionFile . '.bak');
}

public function testCreateMigration()
{
$this->artisan('create:migration Winter.Tester -c --force --for-version v0.0.1 --model TestModel --name create_table');
$this->assertFileExists($this->versionFolder . '/create_table.php');

$migration = require_once $this->versionFolder . '/create_table.php';
$migration->up();

$this->assertTrue(Schema::hasTable($this->table));

$columns = [
'id' => ['type'=>'integer', 'index'=>'primary', 'required'=>true],
'cb' => ['type'=>'boolean'],
'switch' => ['type'=>'boolean'],
'int' => ['type'=>'integer'],
'uint' => ['type'=>'integer', 'required'=>true],
'double' => ['type'=>'float'],
'range' => ['type'=>'integer', 'required'=>true],
'datetime' => ['type'=>'datetime'],
'date' => ['type'=>'date', 'required'=>true],
'time' => ['type'=>'time'],
'md' => ['type'=>'text'],
'textarea' => ['type'=>'text'],
'text' => ['type'=>'string', 'required'=>true],
'phone_id' => ['type'=>'integer', 'index'=>true, 'required'=>true],
'user_id' => ['type'=>'integer', 'index'=>true, 'required'=>true],
'data' => ['type'=>'text'],
'sort_order' => ['type'=>'integer', 'index'=>true],
'taggable_id' => ['type'=>'integer', 'index'=>'morphable_index'],
'taggable_type' => ['type'=>'string', 'index'=>'morphable_index'],
'created_at' => ['type'=>'datetime'],
'updated_at' => ['type'=>'datetime'],
];

$table = Schema::getConnection()->getDoctrineSchemaManager()->listTableDetails($this->table);

foreach ($columns as $name => $definition) {
$this->assertEquals(array_get($definition, 'type'), Schema::getColumnType($this->table, $name));

// assert an index has been created for the primary, morph and foreign keys
if ($indexName = array_get($definition, 'index')) {
if ($indexName === true) {
$indexName = sprintf("%s_%s_index", $this->table, $name);
}
$this->assertTrue($table->hasIndex($indexName));

if ($indexName === 'morphable_index') {
$index = $table->getIndex($indexName);
$this->assertTrue(in_array($name, $index->getColumns()));
}
}
$this->assertEquals(array_get($definition, 'required', false), $table->getColumn($name)->getNotnull());
}

$migration->down();
$this->assertFalse(Schema::hasTable($this->table));
}

public function tearDown(): void
{
File::move($this->versionFile . '.bak', $this->versionFile);
File::deleteDirectory($this->versionFolder);

parent::tearDown();
}
}
39 changes: 39 additions & 0 deletions tests/fixtures/plugins/winter/tester/models/TestModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php namespace Winter\Tester\Models;

use Model;

class TestModel extends Model
{
use \Winter\Storm\Database\Traits\Sortable;

public $table = 'winter_tester_test_model';

public $jsonable = [
'data',
];

public $belongsTo = [
'user' => TestUser::class
];

public $hasOne = [
'phone' => TestPhone::class
];

public $morphTo = [
'taggable' => []
];

public $rules = [
'uint' => 'required',
'range' => 'required|integer|between:1,10',
];
}

class TestUser extends Model
{
}

class TestPhone extends Model
{
}
Loading

0 comments on commit 2309b05

Please sign in to comment.