Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
aerni committed Aug 15, 2024
1 parent 4b56882 commit c78a08c
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 3 deletions.
5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
"require": {
"php": "^8.1",
"fakerphp/faker": "^1.23.0",
"laravel/prompts": "^0.1.17",
"statamic/cms": "^5.0",
"stillat/primitives": "^1.4.1"
"laravel/prompts": "^0.1.24",
"statamic/cms": "^5.0"
},
"require-dev": {
"orchestra/testbench": "^8.0|^9.0",
Expand Down
143 changes: 143 additions & 0 deletions src/Commands/Factories/FactoryMakeCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

namespace Illuminate\Database\Console\Factories;

use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;

#[AsCommand(name: 'make:factory')]
class FactoryMakeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'make:factory';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new model factory';

/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Factory';

/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/factory.stub');
}

/**
* Resolve the fully-qualified path to the stub.
*
* @param string $stub
* @return string
*/
protected function resolveStubPath($stub)
{
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
: __DIR__.$stub;
}

/**
* Build the class with the given name.
*
* @param string $name
* @return string
*/
protected function buildClass($name)
{
$factory = class_basename(Str::ucfirst(str_replace('Factory', '', $name)));

$namespaceModel = $this->option('model')
? $this->qualifyModel($this->option('model'))
: $this->qualifyModel($this->guessModelName($name));

$model = class_basename($namespaceModel);

$namespace = $this->getNamespace(
Str::replaceFirst($this->rootNamespace(), 'Database\\Factories\\', $this->qualifyClass($this->getNameInput()))
);

$replace = [
'{{ factoryNamespace }}' => $namespace,
'NamespacedDummyModel' => $namespaceModel,
'{{ namespacedModel }}' => $namespaceModel,
'{{namespacedModel}}' => $namespaceModel,
'DummyModel' => $model,
'{{ model }}' => $model,
'{{model}}' => $model,
'{{ factory }}' => $factory,
'{{factory}}' => $factory,
];

return str_replace(
array_keys($replace), array_values($replace), parent::buildClass($name)
);
}

/**
* Get the destination class path.
*
* @param string $name
* @return string
*/
protected function getPath($name)
{
$name = (string) Str::of($name)->replaceFirst($this->rootNamespace(), '')->finish('Factory');

return $this->laravel->databasePath().'/factories/'.str_replace('\\', '/', $name).'.php';
}

/**
* Guess the model name from the Factory name or return a default model name.
*
* @param string $name
* @return string
*/
protected function guessModelName($name)
{
if (str_ends_with($name, 'Factory')) {
$name = substr($name, 0, -7);
}

$modelName = $this->qualifyModel(Str::after($name, $this->rootNamespace()));

if (class_exists($modelName)) {
return $modelName;
}

if (is_dir(app_path('Models/'))) {
return $this->rootNamespace().'Models\Model';
}

return $this->rootNamespace().'Model';
}

/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['model', 'm', InputOption::VALUE_OPTIONAL, 'The name of the model'],
];
}
}
132 changes: 132 additions & 0 deletions src/Commands/MakeFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace Aerni\Factory\Commands;

use PhpParser\BuilderFactory;
use Archetype\Facades\PHPFile;
use Archetype\Support\Snippet;
use Statamic\Facades\Taxonomy;
use Statamic\Fields\Blueprint;
use Illuminate\Console\Command;

use Statamic\Facades\Collection;

use function Laravel\Prompts\alert;
use function Laravel\Prompts\info;
use Statamic\Console\RunsInPlease;
use function Laravel\Prompts\select;
use function Laravel\Prompts\error;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\note;

use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use Aerni\Factory\Factories\DefinitionGenerator;

class MakeFactory extends Command
{
use RunsInPlease;

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'statamic:make:factory';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate a factory from a blueprint';

public function handle()
{
$type = select(
label: 'Select the type of factory you want to create.',
options: [
'entry' => 'Entry',
'term' => 'Term',
],
validate: fn (string $value) => match ($value) {
'entry' => Collection::all()->isEmpty()
? 'You need to create at least one collection to use the factory.'
: null,
'term' => Taxonomy::all()->isEmpty()
? 'You need to create at least one taxonomy to use the factory.'
: null,
},
);

$model = $this->getModelData($type);

$classNamespace = 'Database\\Factories\\Statamic\\' . collect([$model['repository'], $model['type']])->map(ucfirst(...))->implode('\\');
$className = ucfirst($model['blueprint']);
$definition = new DefinitionGenerator($model['blueprint']);

$stub = preg_replace(
['/\{{ classNamespace \}}/', '/\{{ className \}}/', '/\{{ definition \}}/'],
[$classNamespace, $className, $definition],
File::get(__DIR__.'/stubs/factory.stub')
);

$classPath = $this->generatePathFromNamespace($classNamespace)."{$className}Factory.php";

if (File::exists($classPath) && ! confirm(label: 'This factory already exists. Do you want to override the class?', default: false)) {
return;
}

// TODO: Make it possible to update the definition of an existing factory class.

File::ensureDirectoryExists(dirname($classPath));
File::put($classPath, $stub);
Process::run("./vendor/bin/pint $classPath");

info("The factory was successfully created: <comment>{$this->getRelativePath($classPath)}</comment>");
}

protected function getModelData(string $type): array
{
$models = match ($type) {
'entry' => Collection::all(),
'term' => Taxonomy::all(),
};

$selectedModel = select(
label: 'Select the collection of the factory.',
options: $models->mapWithKeys(fn ($model) => [$model->handle() => $model->title()]),
);

$model = $models->firstWhere('handle', $selectedModel);

$blueprints = match (true) {
($type === 'entry') => $model->entryBlueprints(),
($type === 'term') => $model->termBlueprints(),
};

$selectedBlueprint = select(
label: 'Select the blueprint of the factory.',
options: $blueprints->mapWithKeys(fn ($blueprint) => [$blueprint->handle() => $blueprint->title()]),
);

$blueprint = $blueprints->firstWhere('handle', $selectedBlueprint);

return [
'repository' => ($type === 'entry') ? 'collections' : 'taxonomies',
'type' => $selectedModel,
'blueprint' => $blueprint,
];
}

protected function generatePathFromNamespace(string $namespace): string
{
$name = str($namespace)->finish('\\')->replaceFirst(app()->getNamespace(), '')->lower();
return base_path(str_replace('\\', '/', $name));
}

protected function getRelativePath(string $path): string
{
return str_replace(base_path().'/', '', $path);
}
}
18 changes: 18 additions & 0 deletions src/Commands/stubs/factory.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace {{ classNamespace }};

use Aerni\Factory\Factories\Factory;

class {{ className }}Factory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
{{ definition }}
}
}
Loading

0 comments on commit c78a08c

Please sign in to comment.