From 9936f4c0597b60ec1bfa933aed41cb1ca203187d Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Tue, 7 May 2024 13:46:30 +0100 Subject: [PATCH 1/3] Added Command Attributes --- src/Commands/Attributes/Argument.php | 24 ++++ src/Commands/Attributes/FlagOption.php | 26 +++++ src/Commands/Attributes/Option.php | 26 +++++ src/Commands/Attributes/OptionalArgument.php | 29 +++++ src/Commands/Attributes/RequiredArgument.php | 26 +++++ src/Commands/Attributes/ValueOption.php | 31 +++++ src/Commands/Concerns/Virtue.php | 117 +++++++++++++++++++ src/Models/Concerns/Virtue.php | 32 +---- src/Support/HasAttributesReflection.php | 44 +++++++ 9 files changed, 325 insertions(+), 30 deletions(-) create mode 100644 src/Commands/Attributes/Argument.php create mode 100644 src/Commands/Attributes/FlagOption.php create mode 100644 src/Commands/Attributes/Option.php create mode 100644 src/Commands/Attributes/OptionalArgument.php create mode 100644 src/Commands/Attributes/RequiredArgument.php create mode 100644 src/Commands/Attributes/ValueOption.php create mode 100644 src/Commands/Concerns/Virtue.php create mode 100644 src/Support/HasAttributesReflection.php diff --git a/src/Commands/Attributes/Argument.php b/src/Commands/Attributes/Argument.php new file mode 100644 index 0000000..ba878a4 --- /dev/null +++ b/src/Commands/Attributes/Argument.php @@ -0,0 +1,24 @@ +|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 = []; + $requiredArguments = $this->buildArgumentsList(RequiredArgument::class); + [$arrayArguments, $nonArrayArguments] = $requiredArguments->partition(fn (array $argument) => $argument['mode'] > InputArgument::REQUIRED); + $optionalArguments = $this->buildArgumentsList(OptionalArgument::class); + + 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]; + } +} From a9f999cebefced723c3133507d0f88a496f848d3 Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Tue, 7 May 2024 14:00:45 +0100 Subject: [PATCH 2/3] Added tests for Commands Attributes --- composer.json | 1 + tests/Datasets/TestCommand.php | 44 +++++++++++++++++++++++++ tests/Feature/CommandAttributesTest.php | 24 ++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 tests/Datasets/TestCommand.php create mode 100644 tests/Feature/CommandAttributesTest.php 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/tests/Datasets/TestCommand.php b/tests/Datasets/TestCommand.php new file mode 100644 index 0000000..cb8c261 --- /dev/null +++ b/tests/Datasets/TestCommand.php @@ -0,0 +1,44 @@ +getArguments())->keyBy('name'); + $options = Collection::make($command->getOptions())->keyBy('name'); + + expect($arguments)->toHaveCount(2) + ->and($arguments->get('name'))->toBeArray() + ->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]); +}); From f908a562ec51f1a6ce441fe6c48b565e70ccd2ba Mon Sep 17 00:00:00 2001 From: Wendell Adriel Date: Tue, 7 May 2024 15:10:33 +0100 Subject: [PATCH 3/3] Add generic Argument attribute --- src/Commands/Attributes/Argument.php | 4 +++- src/Commands/Concerns/Virtue.php | 10 ++++++++-- tests/Datasets/TestCommand.php | 3 +++ tests/Feature/CommandAttributesTest.php | 5 ++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Commands/Attributes/Argument.php b/src/Commands/Attributes/Argument.php index ba878a4..909abe1 100644 --- a/src/Commands/Attributes/Argument.php +++ b/src/Commands/Attributes/Argument.php @@ -4,9 +4,11 @@ namespace WendellAdriel\Virtue\Commands\Attributes; +use Attribute; use Closure; -abstract class Argument +#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +class Argument { /** * @param string|int|bool|array|float|null $default diff --git a/src/Commands/Concerns/Virtue.php b/src/Commands/Concerns/Virtue.php index b42b541..5bbb144 100644 --- a/src/Commands/Concerns/Virtue.php +++ b/src/Commands/Concerns/Virtue.php @@ -26,9 +26,15 @@ trait Virtue public function getArguments(): array { $arguments = []; - $requiredArguments = $this->buildArgumentsList(RequiredArgument::class); + $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); + + $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; diff --git a/tests/Datasets/TestCommand.php b/tests/Datasets/TestCommand.php index cb8c261..5c72c03 100644 --- a/tests/Datasets/TestCommand.php +++ b/tests/Datasets/TestCommand.php @@ -5,6 +5,7 @@ namespace Tests\Datasets; use Illuminate\Console\Command; +use WendellAdriel\Virtue\Commands\Attributes\Argument; use WendellAdriel\Virtue\Commands\Attributes\FlagOption; use WendellAdriel\Virtue\Commands\Attributes\OptionalArgument; use WendellAdriel\Virtue\Commands\Attributes\RequiredArgument; @@ -13,6 +14,8 @@ #[RequiredArgument(name: 'name')] #[OptionalArgument(name: 'age', default: 18)] +#[Argument(name: 'optional', required: false, description: 'Optional parameter')] +#[Argument(name: 'required')] #[FlagOption(name: 'negative', shortcut: 'm', negatable: true)] #[ValueOption(name: 'year', description: 'The year')] #[ValueOption(name: 'scores', array: true, default: [1, 2, 3])] diff --git a/tests/Feature/CommandAttributesTest.php b/tests/Feature/CommandAttributesTest.php index be0a784..cd342c6 100644 --- a/tests/Feature/CommandAttributesTest.php +++ b/tests/Feature/CommandAttributesTest.php @@ -10,8 +10,11 @@ $arguments = Collection::make($command->getArguments())->keyBy('name'); $options = Collection::make($command->getOptions())->keyBy('name'); - expect($arguments)->toHaveCount(2) + 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)