diff --git a/composer.json b/composer.json index 871f9d8..6afd7cb 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ ], "require": { "php": "^8.2", + "illuminate/console": "^11.0", "illuminate/database": "^11.0", "illuminate/support": "^11.0" }, diff --git a/src/Commands/Attributes/Argument.php b/src/Commands/Attributes/Argument.php new file mode 100644 index 0000000..909abe1 --- /dev/null +++ b/src/Commands/Attributes/Argument.php @@ -0,0 +1,26 @@ +|float|null $default + * @param array|Closure $suggestedValues + */ + public function __construct( + public string $name, + public bool $required = true, + public bool $array = false, + public string $description = '', + public string|int|bool|array|float|null $default = null, + public array|Closure $suggestedValues = [], + ) { + } +} diff --git a/src/Commands/Attributes/FlagOption.php b/src/Commands/Attributes/FlagOption.php new file mode 100644 index 0000000..e3273ea --- /dev/null +++ b/src/Commands/Attributes/FlagOption.php @@ -0,0 +1,26 @@ +|null $shortcut + * @param string|int|bool|array|float|null $default + * @param array|Closure $suggestedValues + */ + public function __construct( + public string $name, + public string|array|null $shortcut = null, + public int $mode = InputOption::VALUE_NONE, + public string $description = '', + public string|bool|int|float|array|null $default = null, + public array|Closure $suggestedValues = [], + ) { + } +} diff --git a/src/Commands/Attributes/OptionalArgument.php b/src/Commands/Attributes/OptionalArgument.php new file mode 100644 index 0000000..2d522ce --- /dev/null +++ b/src/Commands/Attributes/OptionalArgument.php @@ -0,0 +1,29 @@ + + */ + public function getArguments(): array + { + $arguments = []; + $generalArguments = $this->buildArgumentsList(Argument::class); + + $requiredArguments = $this->buildArgumentsList(RequiredArgument::class) + ->merge($generalArguments->where('mode', InputArgument::REQUIRED)); + + [$arrayArguments, $nonArrayArguments] = $requiredArguments->partition(fn (array $argument) => $argument['mode'] > InputArgument::REQUIRED); + + $optionalArguments = $this->buildArgumentsList(OptionalArgument::class) + ->merge($generalArguments->filter(fn (array $argument) => $argument['mode'] === InputArgument::OPTIONAL || $argument['mode'] === InputArgument::IS_ARRAY)); + + foreach ($nonArrayArguments as $argument) { + $arguments[] = $argument; + } + + foreach ($optionalArguments as $argument) { + $arguments[] = $argument; + } + + foreach ($arrayArguments as $argument) { + $arguments[] = $argument; + } + + return $arguments; + } + + /** + * @return array + */ + public function getOptions(): array + { + $options = []; + $valueOptions = $this->buildOptionsList(ValueOption::class); + $flagOptions = $this->buildOptionsList(FlagOption::class); + + foreach ($valueOptions as $option) { + $options[] = $option; + } + + foreach ($flagOptions as $option) { + $options[] = $option; + } + + return $options; + } + + /** + * @param class-string $attribute + */ + private function buildArgumentsList(string $attribute): Collection + { + return $this->resolveMultipleAttributes($attribute)->map(function (ReflectionAttribute $argumentAttribute) { + /** @var Argument $attribute */ + $attribute = $argumentAttribute->newInstance(); + + return [ + 'name' => $attribute->name, + 'mode' => $this->resolveMode($attribute), + 'description' => $attribute->description, + 'default' => $attribute->default, + 'suggestedValues' => $attribute->suggestedValues, + ]; + }); + } + + private function resolveMode(Argument $attribute): int + { + return match (true) { + $attribute->required && $attribute->array => InputArgument::IS_ARRAY | InputArgument::REQUIRED, + $attribute->required && ! $attribute->array => InputArgument::REQUIRED, + ! $attribute->required && $attribute->array => InputArgument::IS_ARRAY, + ! $attribute->required && ! $attribute->array => InputArgument::OPTIONAL, + default => InputArgument::REQUIRED, + }; + } + + /** + * @param class-string $attribute + */ + private function buildOptionsList(string $attribute): Collection + { + return $this->resolveMultipleAttributes($attribute)->map(function (ReflectionAttribute $optionAttribute) { + /** @var Option $attribute */ + $attribute = $optionAttribute->newInstance(); + + return [ + 'name' => $attribute->name, + 'shortcut' => $attribute->shortcut, + 'mode' => $attribute->mode, + 'description' => $attribute->description, + 'default' => $attribute->default, + 'suggestedValues' => $attribute->suggestedValues, + ]; + }); + } +} diff --git a/src/Models/Concerns/Virtue.php b/src/Models/Concerns/Virtue.php index 549d825..69c055b 100644 --- a/src/Models/Concerns/Virtue.php +++ b/src/Models/Concerns/Virtue.php @@ -6,16 +6,17 @@ use Illuminate\Support\Collection; use ReflectionAttribute; -use ReflectionClass; use WendellAdriel\Virtue\Models\Attributes\Cast; use WendellAdriel\Virtue\Models\Attributes\Database; use WendellAdriel\Virtue\Models\Attributes\DispatchesOn; use WendellAdriel\Virtue\Models\Attributes\Fillable; use WendellAdriel\Virtue\Models\Attributes\Hidden; use WendellAdriel\Virtue\Models\Attributes\PrimaryKey; +use WendellAdriel\Virtue\Support\HasAttributesReflection; trait Virtue { + use HasAttributesReflection; use HasRelations; /** @var array|null */ @@ -144,33 +145,4 @@ private function handleEvents(): void $this->dispatchesEvents = $eventsArray; } } - - /** - * @param class-string $attributeClass - */ - private function resolveSingleAttribute(string $attributeClass): ?ReflectionAttribute - { - $classAttributes = $this->classAttributes(); - - return $classAttributes->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeClass) - ->first(); - } - - private function resolveMultipleAttributes(string $attributeClass): Collection - { - $classAttributes = $this->classAttributes(); - - return $classAttributes->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeClass); - } - - private function classAttributes(): Collection - { - $class = static::class; - if (! array_key_exists($class, self::$classAttributes) || is_null(self::$classAttributes[$class])) { - $reflectionClass = new ReflectionClass(static::class); - self::$classAttributes[$class] = Collection::make($reflectionClass->getAttributes()); - } - - return self::$classAttributes[$class]; - } } diff --git a/src/Support/HasAttributesReflection.php b/src/Support/HasAttributesReflection.php new file mode 100644 index 0000000..8273fae --- /dev/null +++ b/src/Support/HasAttributesReflection.php @@ -0,0 +1,44 @@ +|null */ + private static ?array $classAttributes = []; + + /** + * @param class-string $attributeClass + */ + private function resolveSingleAttribute(string $attributeClass): ?ReflectionAttribute + { + $classAttributes = $this->classAttributes(); + + return $classAttributes->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeClass) + ->first(); + } + + private function resolveMultipleAttributes(string $attributeClass): Collection + { + $classAttributes = $this->classAttributes(); + + return $classAttributes->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeClass); + } + + private function classAttributes(): Collection + { + $class = static::class; + if (! array_key_exists($class, self::$classAttributes) || is_null(self::$classAttributes[$class])) { + $reflectionClass = new ReflectionClass(static::class); + self::$classAttributes[$class] = Collection::make($reflectionClass->getAttributes()); + } + + return self::$classAttributes[$class]; + } +} diff --git a/tests/Datasets/TestCommand.php b/tests/Datasets/TestCommand.php new file mode 100644 index 0000000..5c72c03 --- /dev/null +++ b/tests/Datasets/TestCommand.php @@ -0,0 +1,47 @@ +getArguments())->keyBy('name'); + $options = Collection::make($command->getOptions())->keyBy('name'); + + expect($arguments)->toHaveCount(4) + ->and($arguments->get('name'))->toBeArray() + ->and($arguments->get('required'))->toBeArray() + ->and($arguments->get('optional'))->toBeArray() + ->and($arguments->get('optional')['description'])->toBe('Optional parameter') + ->and($arguments->get('age'))->toBeArray() + ->and($arguments->get('age')['default'])->toBe(18) + ->and($options)->toHaveCount(3) + ->and($options->get('year'))->toBeArray() + ->and($options->get('year')['description'])->toBe('The year') + ->and($options->get('negative'))->toBeArray() + ->and($options->get('negative')['shortcut'])->toBe('m') + ->and($options->get('scores'))->toBeArray() + ->and($options->get('scores')['default'])->toBe([1, 2, 3]); +});