Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
359 lines
12 KiB
PHP
359 lines
12 KiB
PHP
<?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;
|
|
|
|
/**
|
|
* Scaffolds a new migration file
|
|
*
|
|
* @TODO:
|
|
* - Add flag to either create a new version automatically in version.yaml or
|
|
* add the migration to a specific version, would also put the migration in
|
|
* a version specific folder
|
|
*/
|
|
class CreateMigration extends BaseScaffoldCommand
|
|
{
|
|
/**
|
|
* @var string|null The default command name for lazy loading.
|
|
*/
|
|
protected static $defaultName = 'create:migration';
|
|
|
|
/**
|
|
* @var string The name and signature of this command.
|
|
*/
|
|
protected $signature = 'create:migration
|
|
{plugin : The name of the plugin. <info>(eg: Winter.Blog)</info>}
|
|
{--name= : The name of the migration}
|
|
{--model= : The model to create a migration for. <info>(eg: Post)</info>}
|
|
{--table= : The table to migrate, defaults to autogenerated from the provided model. <info>(eg: winter_blog_posts)</info>}
|
|
{--for-version= : Generate a migration for a specific version}
|
|
{--c|create : Generate a migration that creates the specified table}
|
|
{--u|update : Generate a migration that updates the specified table}
|
|
{--f|force : Overwrite existing files with generated files.}
|
|
{--uninspiring : Disable inspirational quotes}
|
|
';
|
|
|
|
/**
|
|
* @var string The console command description.
|
|
*/
|
|
protected $description = 'Creates a new migration.';
|
|
|
|
/**
|
|
* @var array List of commands that this command replaces (aliases)
|
|
*/
|
|
protected $replaces = [
|
|
'make:migration',
|
|
];
|
|
|
|
/**
|
|
* @var string The type of class being generated.
|
|
*/
|
|
protected $type = 'Migration';
|
|
|
|
/**
|
|
* @var bool Validate the provided plugin input against the PluginManager.
|
|
*/
|
|
protected $validatePluginInput = true;
|
|
|
|
/**
|
|
* @var array Available migration scaffolds and their types
|
|
*/
|
|
protected $migrationScaffolds = [
|
|
'create' => [
|
|
'scaffold/migration/migration.create.stub' => 'updates/{{version}}/{{snake_name}}.php',
|
|
],
|
|
'update' => [
|
|
'scaffold/migration/migration.update.stub' => 'updates/{{version}}/{{snake_name}}.php',
|
|
],
|
|
'migrate' => [
|
|
'scaffold/migration/migration.stub' => 'updates/{{version}}/{{snake_name}}.php',
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Make all stubs.
|
|
*/
|
|
public function makeStubs(): void
|
|
{
|
|
parent::makeStubs();
|
|
|
|
$plugin = $this->getPlugin();
|
|
|
|
// Identify the changes to insert into the version.yaml file
|
|
$changes = [$this->vars['name']];
|
|
$stubs = array_keys($this->stubs);
|
|
foreach ($stubs as $stub) {
|
|
$changes[] = Str::after($this->getDestinationForStub($stub), $plugin->getPluginPath() . '/updates/');
|
|
}
|
|
|
|
// Identify the version to insert these changes into
|
|
$versions = $plugin->getPluginVersions();
|
|
$firstVersion = array_keys($versions)[0];
|
|
$usesV = Str::startsWith($firstVersion, 'v');
|
|
$version = $usesV ? $this->vars['version'] : Str::after($this->vars['version'], 'v');
|
|
|
|
// Insert these changes into the identified version
|
|
$changes = array_merge($versions[$version] ?? [], $changes);
|
|
$versions[$version] = $changes;
|
|
|
|
// Render and save the updated version.yaml file
|
|
$destinationFile = $plugin->getPluginPath() . '/updates/version.yaml';
|
|
$this->files->put($destinationFile, Yaml::render($versions));
|
|
$this->comment('File updated: ' . str_replace(base_path(), '', $destinationFile));
|
|
}
|
|
|
|
/**
|
|
* Get the desired class name from the input.
|
|
*/
|
|
protected function getNameInput(): string
|
|
{
|
|
$name = trim($this->option($this->nameFrom));
|
|
if (empty($name)) {
|
|
if ($this->option('create')) {
|
|
$template = 'Create {ResourceName} Table';
|
|
} elseif ($this->option('update')) {
|
|
$template = 'Update {ResourceName} Table';
|
|
} else {
|
|
$template = now()->format('Y_m_d_His');
|
|
}
|
|
|
|
$resourceName = (
|
|
$this->option('model')
|
|
? Str::plural($this->option('model'))
|
|
: null
|
|
)
|
|
?? (
|
|
$this->option('table')
|
|
? Str::replace('_', ' ', $this->option('table'))
|
|
: ''
|
|
);
|
|
$name = Str::replace('{ResourceName}', $resourceName, $template);
|
|
}
|
|
return $name;
|
|
}
|
|
|
|
/**
|
|
* Prepare variables for stubs.
|
|
*/
|
|
protected function prepareVars(): array
|
|
{
|
|
$parts = explode('.', $this->getPluginIdentifier());
|
|
$plugin = array_pop($parts);
|
|
$author = array_pop($parts);
|
|
$name = $this->getNameInput();
|
|
$table = $this->option('table');
|
|
$model = $this->option('model');
|
|
|
|
if (empty($table) && !empty($model)) {
|
|
$modelClass = "\\{$author}\\{$plugin}\Models\\{$model}";
|
|
if (class_exists($modelClass)) {
|
|
$table = (new $modelClass)->getTable();
|
|
} else {
|
|
throw new InvalidArgumentException("The model [{$modelClass}] does not exist.");
|
|
}
|
|
}
|
|
|
|
if ($this->option('create') && $this->option('update')) {
|
|
throw new InvalidArgumentException('The create & update options cannot both be set at the same time');
|
|
}
|
|
|
|
if ($this->option('create')) {
|
|
$scaffold = 'create';
|
|
} elseif ($this->option('update')) {
|
|
$scaffold = 'update';
|
|
} else {
|
|
$scaffold = 'migrate';
|
|
}
|
|
|
|
if (in_array($scaffold, ['create', 'update']) && empty($table)) {
|
|
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'))) {
|
|
$version = $this->option('for-version');
|
|
} else {
|
|
$currentVersion = $this->getPlugin()->getPluginVersion();
|
|
if ($currentVersion === VersionManager::NO_VERSION_VALUE) {
|
|
throw new InvalidArgumentException('The plugin [' . $this->getPluginIdentifier() . '] does not have a version set and no --version option was provided. Please set a version in the plugin\'s updates/version.yaml file.');
|
|
}
|
|
$version = $this->getNextVersion($currentVersion);
|
|
}
|
|
|
|
$vars = [
|
|
'name' => $name,
|
|
'author' => $author,
|
|
'plugin' => $plugin,
|
|
'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 = plugins_path($vars['plugin_url'] . '/models/' . $vars['lower_model'] . '/fields.yaml');
|
|
$fields = [];
|
|
if (file_exists($fields_path)) {
|
|
$fields = Yaml::parseFile(($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.
|
|
*/
|
|
protected function getNextVersion($currentVersion): string
|
|
{
|
|
$currentVersion = ltrim($currentVersion, 'v');
|
|
$parts = explode('.', $currentVersion);
|
|
$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"),
|
|
];
|
|
}
|
|
}
|