Skip to content

Commit

Permalink
Merge pull request #2 from WendellAdriel/feat/command-attributes
Browse files Browse the repository at this point in the history
Add Command Attributes
  • Loading branch information
WendellAdriel authored May 7, 2024
2 parents fe13e4a + f908a56 commit 8c12cfb
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 30 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
],
"require": {
"php": "^8.2",
"illuminate/console": "^11.0",
"illuminate/database": "^11.0",
"illuminate/support": "^11.0"
},
Expand Down
26 changes: 26 additions & 0 deletions src/Commands/Attributes/Argument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace WendellAdriel\Virtue\Commands\Attributes;

use Attribute;
use Closure;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Argument
{
/**
* @param string|int|bool|array<mixed>|float|null $default
* @param array<mixed>|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 = [],
) {
}
}
26 changes: 26 additions & 0 deletions src/Commands/Attributes/FlagOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace WendellAdriel\Virtue\Commands\Attributes;

use Attribute;
use Symfony\Component\Console\Input\InputOption;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class FlagOption extends Option
{
public function __construct(
public string $name,
public string|array|null $shortcut = null,
public string $description = '',
public bool $negatable = false,
) {
parent::__construct(
name: $name,
shortcut: $shortcut,
mode: $negatable ? InputOption::VALUE_NEGATABLE | InputOption::VALUE_NONE : InputOption::VALUE_NONE,
description: $description,
);
}
}
26 changes: 26 additions & 0 deletions src/Commands/Attributes/Option.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace WendellAdriel\Virtue\Commands\Attributes;

use Closure;
use Symfony\Component\Console\Input\InputOption;

abstract class Option
{
/**
* @param string|array<mixed>|null $shortcut
* @param string|int|bool|array<mixed>|float|null $default
* @param array<mixed>|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 = [],
) {
}
}
29 changes: 29 additions & 0 deletions src/Commands/Attributes/OptionalArgument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace WendellAdriel\Virtue\Commands\Attributes;

use Attribute;
use Closure;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class OptionalArgument extends Argument
{
public function __construct(
public string $name,
public bool $array = false,
public string $description = '',
public string|int|bool|array|float|null $default = null,
public array|Closure $suggestedValues = [],
) {
parent::__construct(
name: $name,
required: false,
array: $array,
description: $description,
default: $default,
suggestedValues: $suggestedValues,
);
}
}
26 changes: 26 additions & 0 deletions src/Commands/Attributes/RequiredArgument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace WendellAdriel\Virtue\Commands\Attributes;

use Attribute;
use Closure;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class RequiredArgument extends Argument
{
public function __construct(
public string $name,
public bool $array = false,
public string $description = '',
public array|Closure $suggestedValues = [],
) {
parent::__construct(
name: $name,
array: $array,
description: $description,
suggestedValues: $suggestedValues,
);
}
}
31 changes: 31 additions & 0 deletions src/Commands/Attributes/ValueOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace WendellAdriel\Virtue\Commands\Attributes;

use Attribute;
use Closure;
use Symfony\Component\Console\Input\InputOption;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class ValueOption extends Option
{
public function __construct(
public string $name,
public bool $array = false,
public string|array|null $shortcut = null,
public string $description = '',
public string|bool|int|float|array|null $default = null,
public array|Closure $suggestedValues = [],
) {
parent::__construct(
name: $name,
shortcut: $shortcut,
mode: $array ? InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL : InputOption::VALUE_OPTIONAL,
description: $description,
default: $default,
suggestedValues: $suggestedValues,
);
}
}
123 changes: 123 additions & 0 deletions src/Commands/Concerns/Virtue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

namespace WendellAdriel\Virtue\Commands\Concerns;

use Illuminate\Support\Collection;
use ReflectionAttribute;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use WendellAdriel\Virtue\Commands\Attributes\Argument;
use WendellAdriel\Virtue\Commands\Attributes\FlagOption;
use WendellAdriel\Virtue\Commands\Attributes\Option;
use WendellAdriel\Virtue\Commands\Attributes\OptionalArgument;
use WendellAdriel\Virtue\Commands\Attributes\RequiredArgument;
use WendellAdriel\Virtue\Commands\Attributes\ValueOption;
use WendellAdriel\Virtue\Support\HasAttributesReflection;

trait Virtue
{
use HasAttributesReflection;

/**
* @return array<InputArgument>
*/
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<InputOption>
*/
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,
];
});
}
}
32 changes: 2 additions & 30 deletions src/Models/Concerns/Virtue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<class-string,Collection>|null */
Expand Down Expand Up @@ -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];
}
}
44 changes: 44 additions & 0 deletions src/Support/HasAttributesReflection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace WendellAdriel\Virtue\Support;

use Illuminate\Support\Collection;
use ReflectionAttribute;
use ReflectionClass;

trait HasAttributesReflection
{
/** @var array<class-string,Collection>|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];
}
}
Loading

0 comments on commit 8c12cfb

Please sign in to comment.