Skip to content
This repository was archived by the owner on Mar 4, 2025. It is now read-only.

Commit 8c12cfb

Browse files
Merge pull request #2 from WendellAdriel/feat/command-attributes
Add Command Attributes
2 parents fe13e4a + f908a56 commit 8c12cfb

File tree

12 files changed

+408
-30
lines changed

12 files changed

+408
-30
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
],
3232
"require": {
3333
"php": "^8.2",
34+
"illuminate/console": "^11.0",
3435
"illuminate/database": "^11.0",
3536
"illuminate/support": "^11.0"
3637
},
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\Virtue\Commands\Attributes;
6+
7+
use Attribute;
8+
use Closure;
9+
10+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
11+
class Argument
12+
{
13+
/**
14+
* @param string|int|bool|array<mixed>|float|null $default
15+
* @param array<mixed>|Closure $suggestedValues
16+
*/
17+
public function __construct(
18+
public string $name,
19+
public bool $required = true,
20+
public bool $array = false,
21+
public string $description = '',
22+
public string|int|bool|array|float|null $default = null,
23+
public array|Closure $suggestedValues = [],
24+
) {
25+
}
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\Virtue\Commands\Attributes;
6+
7+
use Attribute;
8+
use Symfony\Component\Console\Input\InputOption;
9+
10+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
11+
final class FlagOption extends Option
12+
{
13+
public function __construct(
14+
public string $name,
15+
public string|array|null $shortcut = null,
16+
public string $description = '',
17+
public bool $negatable = false,
18+
) {
19+
parent::__construct(
20+
name: $name,
21+
shortcut: $shortcut,
22+
mode: $negatable ? InputOption::VALUE_NEGATABLE | InputOption::VALUE_NONE : InputOption::VALUE_NONE,
23+
description: $description,
24+
);
25+
}
26+
}

src/Commands/Attributes/Option.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\Virtue\Commands\Attributes;
6+
7+
use Closure;
8+
use Symfony\Component\Console\Input\InputOption;
9+
10+
abstract class Option
11+
{
12+
/**
13+
* @param string|array<mixed>|null $shortcut
14+
* @param string|int|bool|array<mixed>|float|null $default
15+
* @param array<mixed>|Closure $suggestedValues
16+
*/
17+
public function __construct(
18+
public string $name,
19+
public string|array|null $shortcut = null,
20+
public int $mode = InputOption::VALUE_NONE,
21+
public string $description = '',
22+
public string|bool|int|float|array|null $default = null,
23+
public array|Closure $suggestedValues = [],
24+
) {
25+
}
26+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\Virtue\Commands\Attributes;
6+
7+
use Attribute;
8+
use Closure;
9+
10+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
11+
final class OptionalArgument extends Argument
12+
{
13+
public function __construct(
14+
public string $name,
15+
public bool $array = false,
16+
public string $description = '',
17+
public string|int|bool|array|float|null $default = null,
18+
public array|Closure $suggestedValues = [],
19+
) {
20+
parent::__construct(
21+
name: $name,
22+
required: false,
23+
array: $array,
24+
description: $description,
25+
default: $default,
26+
suggestedValues: $suggestedValues,
27+
);
28+
}
29+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\Virtue\Commands\Attributes;
6+
7+
use Attribute;
8+
use Closure;
9+
10+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
11+
final class RequiredArgument extends Argument
12+
{
13+
public function __construct(
14+
public string $name,
15+
public bool $array = false,
16+
public string $description = '',
17+
public array|Closure $suggestedValues = [],
18+
) {
19+
parent::__construct(
20+
name: $name,
21+
array: $array,
22+
description: $description,
23+
suggestedValues: $suggestedValues,
24+
);
25+
}
26+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\Virtue\Commands\Attributes;
6+
7+
use Attribute;
8+
use Closure;
9+
use Symfony\Component\Console\Input\InputOption;
10+
11+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
12+
final class ValueOption extends Option
13+
{
14+
public function __construct(
15+
public string $name,
16+
public bool $array = false,
17+
public string|array|null $shortcut = null,
18+
public string $description = '',
19+
public string|bool|int|float|array|null $default = null,
20+
public array|Closure $suggestedValues = [],
21+
) {
22+
parent::__construct(
23+
name: $name,
24+
shortcut: $shortcut,
25+
mode: $array ? InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL : InputOption::VALUE_OPTIONAL,
26+
description: $description,
27+
default: $default,
28+
suggestedValues: $suggestedValues,
29+
);
30+
}
31+
}

src/Commands/Concerns/Virtue.php

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\Virtue\Commands\Concerns;
6+
7+
use Illuminate\Support\Collection;
8+
use ReflectionAttribute;
9+
use Symfony\Component\Console\Input\InputArgument;
10+
use Symfony\Component\Console\Input\InputOption;
11+
use WendellAdriel\Virtue\Commands\Attributes\Argument;
12+
use WendellAdriel\Virtue\Commands\Attributes\FlagOption;
13+
use WendellAdriel\Virtue\Commands\Attributes\Option;
14+
use WendellAdriel\Virtue\Commands\Attributes\OptionalArgument;
15+
use WendellAdriel\Virtue\Commands\Attributes\RequiredArgument;
16+
use WendellAdriel\Virtue\Commands\Attributes\ValueOption;
17+
use WendellAdriel\Virtue\Support\HasAttributesReflection;
18+
19+
trait Virtue
20+
{
21+
use HasAttributesReflection;
22+
23+
/**
24+
* @return array<InputArgument>
25+
*/
26+
public function getArguments(): array
27+
{
28+
$arguments = [];
29+
$generalArguments = $this->buildArgumentsList(Argument::class);
30+
31+
$requiredArguments = $this->buildArgumentsList(RequiredArgument::class)
32+
->merge($generalArguments->where('mode', InputArgument::REQUIRED));
33+
34+
[$arrayArguments, $nonArrayArguments] = $requiredArguments->partition(fn (array $argument) => $argument['mode'] > InputArgument::REQUIRED);
35+
36+
$optionalArguments = $this->buildArgumentsList(OptionalArgument::class)
37+
->merge($generalArguments->filter(fn (array $argument) => $argument['mode'] === InputArgument::OPTIONAL || $argument['mode'] === InputArgument::IS_ARRAY));
38+
39+
foreach ($nonArrayArguments as $argument) {
40+
$arguments[] = $argument;
41+
}
42+
43+
foreach ($optionalArguments as $argument) {
44+
$arguments[] = $argument;
45+
}
46+
47+
foreach ($arrayArguments as $argument) {
48+
$arguments[] = $argument;
49+
}
50+
51+
return $arguments;
52+
}
53+
54+
/**
55+
* @return array<InputOption>
56+
*/
57+
public function getOptions(): array
58+
{
59+
$options = [];
60+
$valueOptions = $this->buildOptionsList(ValueOption::class);
61+
$flagOptions = $this->buildOptionsList(FlagOption::class);
62+
63+
foreach ($valueOptions as $option) {
64+
$options[] = $option;
65+
}
66+
67+
foreach ($flagOptions as $option) {
68+
$options[] = $option;
69+
}
70+
71+
return $options;
72+
}
73+
74+
/**
75+
* @param class-string $attribute
76+
*/
77+
private function buildArgumentsList(string $attribute): Collection
78+
{
79+
return $this->resolveMultipleAttributes($attribute)->map(function (ReflectionAttribute $argumentAttribute) {
80+
/** @var Argument $attribute */
81+
$attribute = $argumentAttribute->newInstance();
82+
83+
return [
84+
'name' => $attribute->name,
85+
'mode' => $this->resolveMode($attribute),
86+
'description' => $attribute->description,
87+
'default' => $attribute->default,
88+
'suggestedValues' => $attribute->suggestedValues,
89+
];
90+
});
91+
}
92+
93+
private function resolveMode(Argument $attribute): int
94+
{
95+
return match (true) {
96+
$attribute->required && $attribute->array => InputArgument::IS_ARRAY | InputArgument::REQUIRED,
97+
$attribute->required && ! $attribute->array => InputArgument::REQUIRED,
98+
! $attribute->required && $attribute->array => InputArgument::IS_ARRAY,
99+
! $attribute->required && ! $attribute->array => InputArgument::OPTIONAL,
100+
default => InputArgument::REQUIRED,
101+
};
102+
}
103+
104+
/**
105+
* @param class-string $attribute
106+
*/
107+
private function buildOptionsList(string $attribute): Collection
108+
{
109+
return $this->resolveMultipleAttributes($attribute)->map(function (ReflectionAttribute $optionAttribute) {
110+
/** @var Option $attribute */
111+
$attribute = $optionAttribute->newInstance();
112+
113+
return [
114+
'name' => $attribute->name,
115+
'shortcut' => $attribute->shortcut,
116+
'mode' => $attribute->mode,
117+
'description' => $attribute->description,
118+
'default' => $attribute->default,
119+
'suggestedValues' => $attribute->suggestedValues,
120+
];
121+
});
122+
}
123+
}

src/Models/Concerns/Virtue.php

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@
66

77
use Illuminate\Support\Collection;
88
use ReflectionAttribute;
9-
use ReflectionClass;
109
use WendellAdriel\Virtue\Models\Attributes\Cast;
1110
use WendellAdriel\Virtue\Models\Attributes\Database;
1211
use WendellAdriel\Virtue\Models\Attributes\DispatchesOn;
1312
use WendellAdriel\Virtue\Models\Attributes\Fillable;
1413
use WendellAdriel\Virtue\Models\Attributes\Hidden;
1514
use WendellAdriel\Virtue\Models\Attributes\PrimaryKey;
15+
use WendellAdriel\Virtue\Support\HasAttributesReflection;
1616

1717
trait Virtue
1818
{
19+
use HasAttributesReflection;
1920
use HasRelations;
2021

2122
/** @var array<class-string,Collection>|null */
@@ -144,33 +145,4 @@ private function handleEvents(): void
144145
$this->dispatchesEvents = $eventsArray;
145146
}
146147
}
147-
148-
/**
149-
* @param class-string $attributeClass
150-
*/
151-
private function resolveSingleAttribute(string $attributeClass): ?ReflectionAttribute
152-
{
153-
$classAttributes = $this->classAttributes();
154-
155-
return $classAttributes->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeClass)
156-
->first();
157-
}
158-
159-
private function resolveMultipleAttributes(string $attributeClass): Collection
160-
{
161-
$classAttributes = $this->classAttributes();
162-
163-
return $classAttributes->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeClass);
164-
}
165-
166-
private function classAttributes(): Collection
167-
{
168-
$class = static::class;
169-
if (! array_key_exists($class, self::$classAttributes) || is_null(self::$classAttributes[$class])) {
170-
$reflectionClass = new ReflectionClass(static::class);
171-
self::$classAttributes[$class] = Collection::make($reflectionClass->getAttributes());
172-
}
173-
174-
return self::$classAttributes[$class];
175-
}
176148
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WendellAdriel\Virtue\Support;
6+
7+
use Illuminate\Support\Collection;
8+
use ReflectionAttribute;
9+
use ReflectionClass;
10+
11+
trait HasAttributesReflection
12+
{
13+
/** @var array<class-string,Collection>|null */
14+
private static ?array $classAttributes = [];
15+
16+
/**
17+
* @param class-string $attributeClass
18+
*/
19+
private function resolveSingleAttribute(string $attributeClass): ?ReflectionAttribute
20+
{
21+
$classAttributes = $this->classAttributes();
22+
23+
return $classAttributes->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeClass)
24+
->first();
25+
}
26+
27+
private function resolveMultipleAttributes(string $attributeClass): Collection
28+
{
29+
$classAttributes = $this->classAttributes();
30+
31+
return $classAttributes->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeClass);
32+
}
33+
34+
private function classAttributes(): Collection
35+
{
36+
$class = static::class;
37+
if (! array_key_exists($class, self::$classAttributes) || is_null(self::$classAttributes[$class])) {
38+
$reflectionClass = new ReflectionClass(static::class);
39+
self::$classAttributes[$class] = Collection::make($reflectionClass->getAttributes());
40+
}
41+
42+
return self::$classAttributes[$class];
43+
}
44+
}

0 commit comments

Comments
 (0)