diff --git a/composer.json b/composer.json index e831bb8..2e1dbf3 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/src/Commands/Factories/FactoryMakeCommand.php b/src/Commands/Factories/FactoryMakeCommand.php new file mode 100644 index 0000000..ba8df30 --- /dev/null +++ b/src/Commands/Factories/FactoryMakeCommand.php @@ -0,0 +1,143 @@ +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'], + ]; + } +} diff --git a/src/Commands/MakeFactory.php b/src/Commands/MakeFactory.php new file mode 100644 index 0000000..605efde --- /dev/null +++ b/src/Commands/MakeFactory.php @@ -0,0 +1,132 @@ + '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: {$this->getRelativePath($classPath)}"); + } + + 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); + } +} diff --git a/src/Commands/stubs/factory.stub b/src/Commands/stubs/factory.stub new file mode 100644 index 0000000..af969fb --- /dev/null +++ b/src/Commands/stubs/factory.stub @@ -0,0 +1,18 @@ + + */ + public function definition(): array + { + {{ definition }} + } +} diff --git a/src/Factories/DefinitionGenerator.php b/src/Factories/DefinitionGenerator.php new file mode 100644 index 0000000..1f52389 --- /dev/null +++ b/src/Factories/DefinitionGenerator.php @@ -0,0 +1,129 @@ +mapItems($this->blueprint->fields()->items()->all()); + } + + public function __toString(): string + { + return "return {$this->arrayToString($this->toArray())};"; + } + + public function mapItems(array $items): array + { + return collect($items) + ->flatMap($this->mapFieldtypes(...)) + ->all(); + } + + protected function mapFieldtypes(array $item): array + { + return match (true) { + $item['field']['type'] === 'bard' => $this->mapBardAndReplicator($item), + $item['field']['type'] === 'replicator' => $this->mapBardAndReplicator($item), + $item['field']['type'] === 'grid' => $this->mapGrid($item), + $item['field']['type'] === 'table' => $this->mapTable($item), + default => $this->mapSimple($item), + }; + } + + protected function mapBardAndReplicator(array $item): array + { + $sets = collect($item['field']['sets'])->flatMap(function ($group) { + return collect($group['sets'])->map(function ($set, $key) { + return array_merge($this->mapItems($set['fields']), [ + 'type' => $key, + 'enabled' => true, + ]); + }); + })->values()->all(); + + return [ + $item['handle'] => $sets, + ]; + } + + protected function mapGrid(array $item): array + { + $fields = collect($item['field']['fields']) + ->flatMap(fn ($item) => $this->mapItems([$item])) + ->toArray(); + + return [ + $item['handle'] => $fields, + ]; + } + + protected function mapTable(array $item): array + { + $handle = $item['handle']; + + $minRows = $item['field']['factory']['min_rows']; + $maxRows = $item['field']['factory']['max_rows']; + $minCells = $item['field']['factory']['min_cells']; + $maxCells = $item['field']['factory']['max_cells']; + $rowCount = random_int($minRows, $maxRows); + $cellCount = random_int($minCells, $maxCells); + + $formatter = $this->formatter($item); + + $table = [ + $handle => [], + ]; + + for ($i = 0; $i < $rowCount; $i++) { + array_push($table[$handle], [ + 'cells' => [], + ]); + } + + $table[$handle] = array_map(function ($item) use ($cellCount, $formatter) { + for ($i = 0; $i < $cellCount; $i++) { + array_push($item['cells'], $formatter); + } + + return $item; + }, $table[$handle]); + + return $table; + } + + protected function mapSimple(array $item): array + { + return [ + $item['handle'] => null, + ]; + } + + protected function arrayToString($array, $indentLevel = 0): string + { + $output = "[\n"; + $indentation = str_repeat(' ', $indentLevel + 1); // 4 spaces per indent level + + foreach ($array as $key => $value) { + $formattedKey = is_int($key) ? '' : "'$key' => "; + + if (is_array($value)) { + $formattedValue = $this->arrayToString($value, $indentLevel + 1); + } else { + $formattedValue = var_export($value, true); + } + + $output .= "{$indentation}{$formattedKey}{$formattedValue},\n"; + } + + return $output .= str_repeat(' ', $indentLevel) . "]"; + } +} diff --git a/src/Factories/DefinitionHelpers.php b/src/Factories/DefinitionHelpers.php new file mode 100644 index 0000000..e01f3b6 --- /dev/null +++ b/src/Factories/DefinitionHelpers.php @@ -0,0 +1,13 @@ + $callback(), range(1, $count)); + } +} diff --git a/src/Factories/Factory.php b/src/Factories/Factory.php index 93c92e9..3ced0e9 100644 --- a/src/Factories/Factory.php +++ b/src/Factories/Factory.php @@ -6,11 +6,14 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\Support\Collection; +use Aerni\Factory\Factories\Sequence; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Taxonomies\Term; abstract class Factory { + use DefinitionHelpers; + public static string $namespace = 'Database\\Factories\\Statamic\\'; public function __construct( @@ -112,6 +115,16 @@ public function state(mixed $state): self ]); } + public function set($key, $value) + { + return $this->state([$key => $value]); + } + + public function sequence(...$sequence) + { + return $this->state(new Sequence(...$sequence)); + } + public function count(?int $count): self { return $this->newInstance(['count' => $count]); diff --git a/src/Factories/Sequence.php b/src/Factories/Sequence.php new file mode 100644 index 0000000..f890e3c --- /dev/null +++ b/src/Factories/Sequence.php @@ -0,0 +1,9 @@ +