Skip to content

Commit 55478c4

Browse files
stanclPHP CS Fixer
andauthored
Metadata (#8)
* Metadata * Fix code style (php-cs-fixer) * Code style Co-authored-by: PHP CS Fixer <[email protected]>
1 parent cc5bba1 commit 55478c4

File tree

8 files changed

+399
-45
lines changed

8 files changed

+399
-45
lines changed

README.md

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ A collection of enum helpers for PHP.
77
- [`Values`](#values)
88
- [`Options`](#options)
99
- [`From`](#from)
10+
- [`Metadata`](#metadata)
1011

1112
You can read more about the idea on [Twitter](https://twitter.com/archtechx/status/1495158228757270528). I originally wanted to include the `InvokableCases` helper in [`archtechx/helpers`](https://github.com/archtechx/helpers), but it makes more sense to make it a separate dependency and use it *inside* the other package.
1213

@@ -211,7 +212,7 @@ enum TaskStatus: int
211212
enum Role
212213
{
213214
use From;
214-
215+
215216
case ADMINISTRATOR;
216217
case SUBSCRIBER;
217218
case GUEST;
@@ -246,6 +247,125 @@ Role::tryFromName('GUEST'); // Role::GUEST
246247
Role::tryFromName('TESTER'); // null
247248
```
248249

250+
### Metadata
251+
252+
This trait lets you add metadata to enum cases.
253+
254+
#### Apply the trait on your enum
255+
```php
256+
use ArchTech\Enums\Metadata;
257+
use ArchTech\Enums\Meta\Meta;
258+
use App\Enums\MetaProperties\{Description, Color};
259+
260+
#[Meta(Description::class, Color::class)]
261+
enum TaskStatus: int
262+
{
263+
use Metadata;
264+
265+
#[Description('Incomplete Task')] #[Color('red')]
266+
case INCOMPLETE = 0;
267+
268+
#[Description('Completed Task')] #[Color('green')]
269+
case COMPLETED = 1;
270+
271+
#[Description('Canceled Task')] #[Color('gray')]
272+
case CANCELED = 2;
273+
}
274+
```
275+
276+
Explanation:
277+
- `Description` and `Color` are userland class attributes — meta properties
278+
- The `#[Meta]` call enables those two meta properties on the enum
279+
- Each case must have a defined description & color (in this example)
280+
281+
#### Access the metadata
282+
283+
```php
284+
TaskStatus::INCOMPLETE->description(); // 'Incomplete Task'
285+
TaskStatus::COMPLETED->color(); // 'green'
286+
```
287+
288+
#### Creating meta properties
289+
290+
Each meta property (= attribute used on a case) needs to exist as a class.
291+
```php
292+
#[Attribute]
293+
class Color extends MetaProperty {}
294+
295+
#[Attribute]
296+
class Description extends MetaProperty {}
297+
```
298+
299+
Inside the class, you can customize a few things. For instance, you may want to use a different method name than the one derived from the class name (`Description` becomes `description()` by default). To do that, override the `method()` method on the meta property:
300+
```php
301+
#[Attribute]
302+
class Description extends MetaProperty
303+
{
304+
public static function method(): string
305+
{
306+
return 'note';
307+
}
308+
}
309+
```
310+
311+
With the code above, the description of a case will be accessible as `TaskStatus::INCOMPLETE->note()`.
312+
313+
Another thing you can customize is the passed value. For instance, to wrap a color name like `text-{$color}-500`, you'd add the following `transform()` method:
314+
```php
315+
#[Attribute]
316+
class Color extends MetaProperty
317+
{
318+
protected function transform(mixed $value): mixed
319+
{
320+
return "text-{$color}-500";
321+
}
322+
}
323+
```
324+
325+
And now the returned color will be correctly transformed:
326+
```php
327+
TaskStatus::COMPLETED->color(); // 'text-green-500'
328+
```
329+
330+
#### Use the `fromMeta()` method
331+
```php
332+
TaskStatus::fromMeta(Color::make('green')); // TaskStatus::COMPLETED
333+
TaskStatus::fromMeta(Color::make('blue')); // Error: ValueError
334+
```
335+
336+
#### Use the `tryFromMeta()` method
337+
```php
338+
TaskStatus::tryFromMeta(Color::make('green')); // TaskStatus::COMPLETED
339+
TaskStatus::tryFromMeta(Color::make('blue')); // null
340+
```
341+
342+
#### Recommendation: use annotations and traits
343+
344+
If you'd like to add better IDE support for the metadata getter methods, you can use `@method` annotations:
345+
346+
```php
347+
/**
348+
* @method string description()
349+
* @method string color()
350+
*/
351+
#[Meta(Description::class, Color::class)]
352+
enum TaskStatus: int
353+
{
354+
use Metadata;
355+
356+
#[Description('Incomplete Task')] #[Color('red')]
357+
case INCOMPLETE = 0;
358+
359+
#[Description('Completed Task')] #[Color('green')]
360+
case COMPLETED = 1;
361+
362+
#[Description('Canceled Task')] #[Color('gray')]
363+
case CANCELED = 2;
364+
}
365+
```
366+
367+
And if you're using the same meta property in multiple enums, you can create a dedicated trait that includes this `@method` annotation.
368+
249369
## Development
250370

251371
Run all checks locally:

phpstan.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ parameters:
1111
- Illuminate\Routing\Route
1212

1313
ignoreErrors:
14+
- '#Access to an undefined static property static\(ArchTech\\Enums\\Meta\\MetaProperty\)\:\:\$method#'
15+
- '#invalid typehint type ArchTech\\Enums\\Metadata#'
16+
- '#invalid typehint type Enum#'
17+
- '#on an unknown class Enum#'
1418
# -
1519
# message: '#Offset (.*?) does not exist on array\|null#'
1620
# paths:

src/Meta/Meta.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ArchTech\Enums\Meta;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_CLASS)]
10+
class Meta
11+
{
12+
/** @var MetaProperty[] */
13+
public array $metaProperties;
14+
15+
public function __construct(array|string ...$metaProperties)
16+
{
17+
// When an array is passed, it'll be wrapped in an outer array due to the ...variadic parameter
18+
if (isset($metaProperties[0]) && is_array($metaProperties[0])) {
19+
// Extract the inner array
20+
$metaProperties = $metaProperties[0];
21+
}
22+
23+
$this->metaProperties = $metaProperties;
24+
}
25+
}

src/Meta/MetaProperty.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ArchTech\Enums\Meta;
6+
7+
abstract class MetaProperty
8+
{
9+
final public function __construct(
10+
public mixed $value,
11+
) {
12+
$this->value = $this->transform($value);
13+
}
14+
15+
public static function make(mixed $value): static
16+
{
17+
return new static($value);
18+
}
19+
20+
protected function transform(mixed $value): mixed
21+
{
22+
// Feel free to override this to transform the value during instantiation
23+
24+
return $value;
25+
}
26+
27+
/** Get the name of the accessor method */
28+
public static function method(): string
29+
{
30+
if (property_exists(static::class, 'method')) {
31+
return static::${'method'};
32+
}
33+
34+
$parts = explode('\\', static::class);
35+
36+
return lcfirst(end($parts));
37+
}
38+
}

src/Meta/Reflection.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ArchTech\Enums\Meta;
6+
7+
use ReflectionAttribute;
8+
use ReflectionEnumUnitCase;
9+
use ReflectionObject;
10+
11+
class Reflection
12+
{
13+
/**
14+
* Get the meta properties enabled on an Enum.
15+
*
16+
* @param \Enum&\ArchTech\Enums\Metadata $enum
17+
* @return string[]|array<\class-string<MetaProperty>>
18+
*/
19+
public static function metaProperties(mixed $enum): array
20+
{
21+
$reflection = new ReflectionObject($enum);
22+
23+
// Attributes of the `Meta` type
24+
$attributes = array_values(array_filter(
25+
$reflection->getAttributes(),
26+
fn (ReflectionAttribute $attr) => $attr->getName() === Meta::class,
27+
));
28+
29+
if ($attributes) {
30+
return $attributes[0]->newInstance()->metaProperties;
31+
}
32+
33+
return [];
34+
}
35+
36+
/**
37+
* Get the value of a meta property on the provided enum.
38+
*
39+
* @param \Enum $enum
40+
*/
41+
public static function metaValue(string $metaProperty, mixed $enum): mixed
42+
{
43+
// Find the case used by $enum
44+
$reflection = new ReflectionEnumUnitCase($enum::class, $enum->name);
45+
$attributes = $reflection->getAttributes();
46+
47+
// Instantiate each ReflectionAttribute
48+
/** @var MetaProperty[] $properties */
49+
$properties = array_map(fn (ReflectionAttribute $attr) => $attr->newInstance(), $attributes);
50+
51+
// Find the property that matches the $metaProperty class
52+
$properties = array_filter($properties, fn (MetaProperty $property) => $property::class === $metaProperty);
53+
54+
// Reset array index
55+
$properties = array_values($properties);
56+
57+
if ($properties) {
58+
return $properties[0]->value;
59+
}
60+
61+
return null;
62+
}
63+
}

src/Metadata.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ArchTech\Enums;
6+
7+
use ArchTech\Enums\Meta\MetaProperty;
8+
use ValueError;
9+
10+
trait Metadata
11+
{
12+
/** Try to get the first case with this meta property value. */
13+
public static function tryFromMeta(MetaProperty $metaProperty): static|null
14+
{
15+
foreach (static::cases() as $case) {
16+
if (Meta\Reflection::metaValue($metaProperty::class, $case) === $metaProperty->value) {
17+
return $case;
18+
}
19+
}
20+
21+
return null;
22+
}
23+
24+
/** Get the first case with this meta property value. */
25+
public static function fromMeta(MetaProperty $metaProperty): static
26+
{
27+
return static::tryFromMeta($metaProperty) ?? throw new ValueError(
28+
'Enum ' . static::class . ' does not have a case with a meta property "' .
29+
$metaProperty::class . '" of value "' . $metaProperty->value . '"'
30+
);
31+
}
32+
33+
public function __call(string $property, $arguments): mixed
34+
{
35+
$metaProperties = Meta\Reflection::metaProperties($this);
36+
37+
foreach ($metaProperties as $metaProperty) {
38+
if ($metaProperty::method() === $property) {
39+
return Meta\Reflection::metaValue($metaProperty, $this);
40+
}
41+
}
42+
43+
return null;
44+
}
45+
}

0 commit comments

Comments
 (0)