Skip to content

Commit

Permalink
Improve CLI module generator
Browse files Browse the repository at this point in the history
- Generate model and migration content dynamically depending on provided options, removing comments, providing a greatly improved developer experience (this is at the price of ugly stubs – we should revisit our approach by using a proper PHP code generator in the future)
- Add `--all` option to enable all traits without any prompt
- When providing no option, prompt defaults to yes for all options
- When providing one or multiple options, prompt defaults to no for all other options
- It is possible to use artisan's `--no-interaction` option to skip the prompt
  • Loading branch information
ifox committed Nov 19, 2019
1 parent c73154f commit a9de815
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 102 deletions.
150 changes: 114 additions & 36 deletions src/Commands/ModuleMake.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Config\Repository as Config;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use Illuminate\Support\Composer;
use Illuminate\Support\Str;

Expand All @@ -22,14 +23,15 @@ class ModuleMake extends Command
{--M|hasMedias}
{--F|hasFiles}
{--P|hasPosition}
{--R|hasRevisions}';
{--R|hasRevisions}
{--all}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new CMS Module';
protected $description = 'Create a new Twill Module';

/**
* @var Filesystem
Expand Down Expand Up @@ -69,6 +71,16 @@ public function __construct(Filesystem $files, Composer $composer, Config $confi
$this->composer = $composer;
$this->config = $config;

$this->blockable = false;
$this->translatable = false;
$this->sluggable = false;
$this->mediable = false;
$this->fileable = false;
$this->sortable = false;
$this->revisionable = false;

$this->defaultsAnswserToNo = false;

$this->modelTraits = ['HasBlocks', 'HasTranslation', 'HasSlug', 'HasMedias', 'HasFiles', 'HasRevisions', 'HasPosition'];
$this->repositoryTraits = ['HandleBlocks', 'HandleTranslations', 'HandleSlugs', 'HandleMedias', 'HandleFiles', 'HandleRevisions'];
}
Expand All @@ -82,24 +94,48 @@ public function handle()
{
$moduleName = Str::plural(lcfirst($this->argument('moduleName')));

$blockable = $this->checkOption('hasBlocks');
$translatable = $this->checkOption('hasTranslation');
$sluggable = $this->checkOption('hasSlug');
$mediable = $this->checkOption('hasMedias');
$fileable = $this->checkOption('hasFiles');
$sortable = $this->checkOption('hasPosition');
$revisionable = $this->checkOption('hasRevisions');
$enabledOptions = Collection::make($this->options())->only([
'hasBlocks',
'hasTranslation',
'hasSlug',
'hasMedias',
'hasFiles',
'hasPosition',
'hasRevisions',
])->filter(function ($enabled) {
return $enabled;
});

if (count($enabledOptions) > 0) {
$this->defaultsAnswserToNo = true;
}

$activeTraits = [$blockable, $translatable, $sluggable, $mediable, $fileable, $revisionable, $sortable];
$this->blockable = $this->checkOption('hasBlocks');
$this->translatable = $this->checkOption('hasTranslation');
$this->sluggable = $this->checkOption('hasSlug');
$this->mediable = $this->checkOption('hasMedias');
$this->fileable = $this->checkOption('hasFiles');
$this->sortable = $this->checkOption('hasPosition');
$this->revisionable = $this->checkOption('hasRevisions');

$activeTraits = [
$this->blockable,
$this->translatable,
$this->sluggable,
$this->mediable,
$this->fileable,
$this->revisionable,
$this->sortable,
];

$modelName = Str::studly(Str::singular($moduleName));

$this->createMigration($moduleName);
$this->createModels($modelName, $translatable, $sluggable, $sortable, $revisionable, $activeTraits);
$this->createModels($modelName, $activeTraits);
$this->createRepository($modelName, $activeTraits);
$this->createController($moduleName, $modelName);
$this->createRequest($modelName);
$this->createViews($moduleName, $translatable);
$this->createViews($moduleName);

$this->info("Add Route::module('{$moduleName}'); to your admin routes file.");
$this->info("Setup a new CMS menu item in config/twill-navigation.php:");
Expand Down Expand Up @@ -129,7 +165,6 @@ public function handle()
private function createMigration($moduleName = 'items')
{
$table = Str::snake($moduleName);

$tableClassName = Str::studly($table);

$className = "Create{$tableClassName}Tables";
Expand All @@ -147,6 +182,22 @@ private function createMigration($moduleName = 'items')
$this->files->get(__DIR__ . '/stubs/migration.stub')
);

if ($this->translatable) {
$stub = preg_replace('/{{!hasTranslation}}[\s\S]+?{{\/!hasTranslation}}/', '', $stub);
} else {
$stub = str_replace([
'{{!hasTranslation}}',
'{{/!hasTranslation}}',
], '', $stub);
}

$stub = $this->renderStubForOption($stub, 'hasTranslation', $this->translatable);
$stub = $this->renderStubForOption($stub, 'hasSlug', $this->sluggable);
$stub = $this->renderStubForOption($stub, 'hasRevisions', $this->revisionable);
$stub = $this->renderStubForOption($stub, 'hasPosition', $this->sortable);

$stub = preg_replace('/\}\);[\s\S]+?Schema::create/', "});\n\n Schema::create", $stub);

$this->files->put($fullPath, $stub);

$this->info("Migration created successfully! Add some fields!");
Expand All @@ -157,20 +208,16 @@ private function createMigration($moduleName = 'items')
* Creates new model class files for the given model name and traits.
*
* @param string $modelName
* @param bool $translatable
* @param bool $sluggable
* @param bool $sortable
* @param bool $revisionable
* @param array $activeTraits
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
private function createModels($modelName = 'Item', $translatable = false, $sluggable = false, $sortable = false, $revisionable = false, $activeTraits = [])
private function createModels($modelName = 'Item', $activeTraits = [])
{
if (!$this->files->isDirectory(twill_path('Models'))) {
$this->files->makeDirectory(twill_path('Models'));
}

if ($translatable) {
if ($this->translatable) {
if (!$this->files->isDirectory(twill_path('Models/Translations'))) {
$this->files->makeDirectory(twill_path('Models/Translations'));
}
Expand All @@ -182,7 +229,7 @@ private function createModels($modelName = 'Item', $translatable = false, $slugg
$this->files->put(twill_path('Models/Translations/' . $modelTranslationClassName . '.php'), $stub);
}

if ($sluggable) {
if ($this->sluggable) {
if (!$this->files->isDirectory(twill_path('Models/Slugs'))) {
$this->files->makeDirectory(twill_path('Models/Slugs'));
}
Expand All @@ -194,7 +241,7 @@ private function createModels($modelName = 'Item', $translatable = false, $slugg
$this->files->put(twill_path('Models/Slugs/' . $modelSlugClassName . '.php'), $stub);
}

if ($revisionable) {
if ($this->revisionable) {
if (!$this->files->isDirectory(twill_path('Models/Revisions'))) {
$this->files->makeDirectory(twill_path('Models/Revisions'));
}
Expand All @@ -220,19 +267,48 @@ private function createModels($modelName = 'Item', $translatable = false, $slugg

$activeModelTraitsImports = empty($activeModelTraits) ? '' : "use A17\Twill\Models\Behaviors\\" . implode(";\nuse A17\Twill\Models\Behaviors\\", $activeModelTraits) . ";";

$activeModelImplements = $sortable ? 'implements Sortable' : '';
$activeModelImplements = $this->sortable ? 'implements Sortable' : '';

if ($sortable) {
if ($this->sortable) {
$activeModelTraitsImports .= "\nuse A17\Twill\Models\Behaviors\Sortable;";
}

$stub = str_replace(['{{modelClassName}}', '{{modelTraits}}', '{{modelImports}}', '{{modelImplements}}'], [$modelClassName, $activeModelTraitsString, $activeModelTraitsImports, $activeModelImplements], $this->files->get(__DIR__ . '/stubs/model.stub'));
$stub = str_replace([
'{{modelClassName}}',
'{{modelTraits}}',
'{{modelImports}}',
'{{modelImplements}}',
], [
$modelClassName,
$activeModelTraitsString,
$activeModelTraitsImports,
$activeModelImplements,
], $this->files->get(__DIR__ . '/stubs/model.stub'));

$stub = $this->renderStubForOption($stub, 'hasTranslation', $this->translatable);
$stub = $this->renderStubForOption($stub, 'hasSlug', $this->sluggable);
$stub = $this->renderStubForOption($stub, 'hasMedias', $this->mediable);
$stub = $this->renderStubForOption($stub, 'hasPosition', $this->sortable);

$this->files->put(twill_path('Models/' . $modelClassName . '.php'), $stub);

$this->info("Models created successfully! Fill your fillables!");
}

private function renderStubForOption($stub, $option, $enabled)
{
if ($enabled) {
$stub = str_replace([
'{{' . $option . '}}',
'{{/' . $option . '}}',
], '', $stub);
} else {
$stub = preg_replace('/{{' . $option . '}}[\s\S]+?{{\/' . $option . '}}/', '', $stub);
}

return $stub;
}

/**
* Creates new repository class file for the given model name.
*
Expand Down Expand Up @@ -321,19 +397,18 @@ private function createRequest($modelName = 'Item')
* Creates appropriate module Blade view files.
*
* @param string $moduleName
* @param bool $translatable
* @return void
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
private function createViews($moduleName = 'items', $translatable = false)
private function createViews($moduleName = 'items')
{
$viewsPath = $this->config->get('view.paths')[0] . '/admin/' . $moduleName;

if (!$this->files->isDirectory($viewsPath)) {
$this->files->makeDirectory($viewsPath, 0755, true);
}

$formView = $translatable ? 'form_translatable' : 'form';
$formView = $this->translatable ? 'form_translatable' : 'form';

$this->files->put($viewsPath . '/form.blade.php', $this->files->get(__DIR__ . '/stubs/' . $formView . '.blade.stub'));

Expand All @@ -342,17 +417,20 @@ private function createViews($moduleName = 'items', $translatable = false)

private function checkOption($option)
{
if ($this->option($option) || $this->option('all')) {
return true;
}

$questions = [
'hasBlocks' => 'Does this module require blocks?',
'hasTranslation' => 'Does this module require translation?',
'hasSlug' => 'Does this module require slug?',
'hasMedias' => 'Does this module has medias?',
'hasFiles' => 'Does this module has files?',
'hasPosition' => 'Does this module require position?',
'hasRevisions' => 'Does this module require revisions?'
'hasBlocks' => 'Do you need to use the block editor on this module?',
'hasTranslation' => 'Do you need to translate content on this module?',
'hasSlug' => 'Do you need to generate slugs on this module?',
'hasMedias' => 'Do you need to attach images on this module?',
'hasFiles' => 'Do you need to attach files on this module?',
'hasPosition' => 'Do you need to manage the position of records on this module?',
'hasRevisions' => 'Do you need to enable revisions on this module?',
];

// If option is not provided, ask choice question.
return $this->option($option) ? true : 'yes' === $this->choice($questions[$option], ['yes', 'no'], 1);
return 'yes' === $this->choice($questions[$option], ['no', 'yes'], $this->defaultsAnswserToNo ? 0 : 1);
}
}
39 changes: 16 additions & 23 deletions src/Commands/stubs/migration.stub
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,42 @@ class Create{{tableClassName}}Tables extends Migration
public function up()
{
Schema::create('{{table}}', function (Blueprint $table) {

// this will create an id, a "published" column, and soft delete and timestamps columns
createDefaultTableFields($table);

{{!hasTranslation}}
// feel free to modify the name of this column, but title is supported by default (you would need to specify the name of the column Twill should consider as your "title" column in your module controller if you change it)
$table->string('title', 200)->nullable();

// your generated model and form include a description field, to get you started, but feel free to get rid of it if you don't need it
$table->text('description')->nullable();

{{/!hasTranslation}}{{hasPosition}}
$table->integer('position')->unsigned()->nullable();
{{/hasPosition}}
// add those 2 colums to enable publication timeframe fields (you can use publish_start_date only if you don't need to provide the ability to specify an end date)
// $table->timestamp('publish_start_date')->nullable();
// $table->timestamp('publish_end_date')->nullable();


// use this column with the HasPosition trait
// $table->integer('position')->unsigned()->nullable();
});

// remove this if you're not going to use any translated field, ie. using the HasTranslation trait. If you do use it, create fields you want translatable in this table instead of the main table above. You do not need to create fields in both tables.
Schema::create('{{singularTableName}}_translations', function (Blueprint $table) {
{{hasTranslation}}Schema::create('{{singularTableName}}_translations', function (Blueprint $table) {
createDefaultTranslationsTableFields($table, '{{singularTableName}}');
// add some translated fields
// $table->string('title', 200)->nullable();
// $table->text('description')->nullable();
});
$table->string('title', 200)->nullable();
$table->text('description')->nullable();
});{{/hasTranslation}}

// remove this if you're not going to use slugs, ie. using the HasSlug trait
Schema::create('{{singularTableName}}_slugs', function (Blueprint $table) {
{{hasSlug}}Schema::create('{{singularTableName}}_slugs', function (Blueprint $table) {
createDefaultSlugsTableFields($table, '{{singularTableName}}');
});
});{{/hasSlug}}

// remove this if you're not going to use revisions, ie. using the HasRevisions trait
Schema::create('{{singularTableName}}_revisions', function (Blueprint $table) {
{{hasRevisions}}Schema::create('{{singularTableName}}_revisions', function (Blueprint $table) {
createDefaultRevisionsTableFields($table, '{{singularTableName}}');
});
});{{/hasRevisions}}
}

public function down()
{
Schema::dropIfExists('{{singularTableName}}_revisions');
Schema::dropIfExists('{{singularTableName}}_translations');
Schema::dropIfExists('{{singularTableName}}_slugs');
{{hasRevisions}}Schema::dropIfExists('{{singularTableName}}_revisions');{{/hasRevisions}}{{hasTranslation}}
Schema::dropIfExists('{{singularTableName}}_translations');{{/hasTranslation}}{{hasSlug}}
Schema::dropIfExists('{{singularTableName}}_slugs');{{/hasSlug}}
Schema::dropIfExists('{{table}}');
}
}
Loading

0 comments on commit a9de815

Please sign in to comment.