Skip to content

Commit

Permalink
Allow to provide extra values to EnumCase attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
ogizanagi committed Jan 26, 2022
1 parent c447d57 commit 2b4b0bc
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 22 deletions.
1 change: 1 addition & 0 deletions .php-cs-fixer.php
Expand Up @@ -18,6 +18,7 @@
'tests/Fixtures/Enum/Suit.php',
'tests/Fixtures/Enum/SuitWithAttributesMissingAttribute.php',
'tests/Fixtures/Enum/SuitWithAttributes.php',
'tests/Fixtures/Enum/SuitWithExtras.php',
'tests/Fixtures/Enum/UnitSuit.php',
'tests/Fixtures/Integration/Symfony/src/Enum/Suit.php',
])
Expand Down
80 changes: 80 additions & 0 deletions README.md
Expand Up @@ -65,6 +65,7 @@ the [`EnumCase`](src/Attribute/EnumCase.php) attribute:
namespace App\Enum;

use Elao\Enum\ReadableEnumInterface;
use Elao\Enum\ReadableEnumTrait;
use Elao\Enum\Attribute\EnumCase;

enum Suit: string implements ReadableEnumInterface
Expand Down Expand Up @@ -113,6 +114,85 @@ $enum = Suit::Hearts;
$translator->trans($enum->getReadable(), locale: 'fr'); // returns 'Coeurs'
```

## Extra values

The `EnumCase` attributes also provides you a way to configure some extra attributes on your cases and access these easily with the `ExtrasTrait`:

```php
namespace App\Enum;

use Elao\Enum\ReadableEnumInterface;
use Elao\Enum\ExtrasTrait;
use Elao\Enum\Attribute\EnumCase;

enum Suit implements ReadableEnumInterface
{
use ExtrasTrait;

#[EnumCase(extras: ['icon' => 'fa-heart', 'color' => 'red'])]
case Hearts;

#[EnumCase(extras: ['icon' => 'fa-diamond', 'color' => 'red'])]
case Diamonds;

#[EnumCase(extras: ['icon' => 'fa-club', 'color' => 'black'])]
case Clubs;

#[EnumCase(extras: ['icon' => 'fa-spade', 'color' => 'black'])]
case Spades;
}
```

Access these infos using `ExtrasTrait::getExtra(string $key, bool $throwOnMissingExtra = false): mixed`:

```php
Suit::Hearts->getExtra('color'); // 'red'
Suit::Spades->getExtra('icon'); // 'fa-spade'
Suit::Spades->getExtra('missing-key'); // null
Suit::Spades->getExtra('missing-key', true); // throws
```

or create your own interfaces/traits:

```php
interface RenderableEnumInterface
{
public function getColor(): string;
public function getIcon(): string;
}

use Elao\Enum\ExtrasTrait;

trait RenderableEnumTrait
{
use ExtrasTrait;

public function getColor(): string
{
$this->getExtra('color', true);
}

public function getIcon(): string
{
$this->getExtra('icon', true);
}
}

use Elao\Enum\Attribute\EnumCase;

enum Suit implements RenderableEnumInterface
{
use RenderableEnumTrait;

#[EnumCase(extras: ['icon' => 'fa-heart', 'color' => 'red'])]
case Hearts;

// […]
}

Suit::Hearts->getColor(); // 'red'
```

## Flag enums

Flagged enumerations are used for bitwise operations.
Expand Down
5 changes: 1 addition & 4 deletions src/Attribute/EnumCase.php
Expand Up @@ -12,13 +12,10 @@

namespace Elao\Enum\Attribute;

/**
* @final
*/
#[\Attribute(\Attribute::TARGET_CLASS_CONSTANT)]
class EnumCase
{
public function __construct(public readonly ?string $label = null)
public function __construct(public readonly ?string $label = null, public readonly array $extras = [])
{
}
}
48 changes: 48 additions & 0 deletions src/EnumCaseAttributesTrait.php
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace Elao\Enum;

use Elao\Enum\Attribute\EnumCase;

trait EnumCaseAttributesTrait
{
private function getEnumCaseAttribute(): ?EnumCase
{
return static::enumCaseAttributes()[$this] ?? null;
}

/**
* @return \SplObjectStorage<\UnitEnum,EnumCase>
*/
private static function enumCaseAttributes(): \SplObjectStorage
{
static $attributes;

if (!isset($attributes)) {
$attributes = new \SplObjectStorage();

foreach ((new \ReflectionEnum(static::class))->getCases() as $rCase) {
if (null === $rAttr = $rCase->getAttributes(EnumCase::class)[0] ?? null) {
continue;
}

/** @var EnumCase $attr */
$attr = $rAttr->newInstance();

$attributes[$rCase->getValue()] = $attr;
}
}

return $attributes;
}
}
62 changes: 62 additions & 0 deletions src/ExtrasTrait.php
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace Elao\Enum;

trait ExtrasTrait
{
use EnumCaseAttributesTrait;

public function getExtra(string $key, bool $throwOnMissingExtra = false): mixed
{
if ($throwOnMissingExtra && !isset(static::arrayAccessibleExtras()[$this][$key])) {
throw new \InvalidArgumentException(sprintf(
'No value for extra "%s" for enum case %s::%s',
$key,
__CLASS__,
$this->name,
));
}

return static::arrayAccessibleExtras()[$this][$key] ?? null;
}

/**
* @return iterable<static, mixed>
*/
public static function extras(string $key, bool $throwOnMissingExtra = false): iterable
{
/** @var static $case */
foreach (static::cases() as $case) {
yield $case => $case->getExtra($key, $throwOnMissingExtra);
}
}

/**
* @internal
*/
private static function arrayAccessibleExtras(): \SplObjectStorage
{
static $extras;

if (!isset($extras)) {
$extras = new \SplObjectStorage();

/** @var static $case */
foreach (static::cases() as $case) {
$extras[$case] = $case->getEnumCaseAttribute()?->extras;
}
}

return $extras;
}
}
32 changes: 16 additions & 16 deletions src/ReadableEnumTrait.php
Expand Up @@ -18,6 +18,8 @@

trait ReadableEnumTrait
{
use EnumCaseAttributesTrait;

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -61,7 +63,7 @@ public static function readableForName(string $name): string
*/
public function getReadable(): string
{
return static::cachedReadables()[$this];
return static::arrayAccessReadables()[$this];
}

/**
Expand All @@ -75,11 +77,12 @@ public static function readables(): iterable

if (!isset($readables)) {
$readables = new \SplObjectStorage();
$r = new \ReflectionEnum(static::class);

/** @var static $case */
foreach (static::cases() as $case) {
$rCase = $r->getCase($case->name);
if (null === $rAttr = $rCase->getAttributes(EnumCase::class)[0] ?? null) {
$attribute = $case->getEnumCaseAttribute();

if (null === $attribute) {
throw new LogicException(sprintf(
'enum "%s" using the "%s" trait must define a "%s" attribute on every cases. Case "%s" is missing one. Alternatively, override the "%s()" method',
static::class,
Expand All @@ -90,10 +93,7 @@ public static function readables(): iterable
));
}

/** @var EnumCase $attr */
$attr = $rAttr->newInstance();

if (null === $attr->label) {
if (null === $attribute->label) {
throw new LogicException(sprintf(
'enum "%s" using the "%s" trait must define a label using the "%s" attribute on every cases. Case "%s" is missing a label. Alternatively, override the "%s()" method',
static::class,
Expand All @@ -104,11 +104,14 @@ public static function readables(): iterable
));
}

$readables[$case] = $attr->label;
$readables[$case] = $attribute->label;
}
}

return $readables;
/** @var static $case */
foreach (static::cases() as $case) {
yield $case => $readables[$case];
}
}

/**
Expand All @@ -118,17 +121,14 @@ public static function readables(): iterable
* making these two storage mechanisms effectively equivalent.
*
* However, there is a {@link https://wiki.php.net/rfc/object_keys_in_arrays pending RFC} regarding object as array keys.
*
* @internal
*/
private static function cachedReadables(): \ArrayAccess
private static function arrayAccessReadables(): \SplObjectStorage
{
static $readables;

if (!isset($readables)) {
$readables = static::readables();
if ($readables instanceof \ArrayAccess) {
return $readables;
}

$readables = new \SplObjectStorage();
foreach (static::readables() as $case => $label) {
$readables[$case] = $label;
Expand Down
33 changes: 33 additions & 0 deletions tests/Fixtures/Enum/SuitWithExtras.php
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace Elao\Enum\Tests\Fixtures\Enum;

use Elao\Enum\Attribute\EnumCase;
use Elao\Enum\ExtrasTrait;

enum SuitWithExtras
{
use ExtrasTrait;

#[EnumCase(extras: ['icon' => 'fa-heart', 'color' => 'red', 'only-for-hearts' => 'value'])]
case Hearts;

#[EnumCase(extras: ['icon' => 'fa-diamond', 'color' => 'red'])]
case Diamonds;

#[EnumCase(extras: ['icon' => 'fa-club', 'color' => 'black'])]
case Clubs;

#[EnumCase(extras: ['icon' => 'fa-spade', 'color' => 'black'])]
case Spades;
}
44 changes: 44 additions & 0 deletions tests/IterableAssertionsTrait.php
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace Elao\Enum\Tests;

use PHPUnit\Framework\Assert;

trait IterableAssertionsTrait
{
private static function iterates(iterable $iterable): void
{
foreach ($iterable as $_) {
}
}

private static function assertIterablesMatch(iterable $expected, iterable $iterable)
{
$keys = [];
$values = [];
foreach ($iterable as $key => $value) {
$keys[] = $key;
$values[] = $value;
}

$expectedKeys = [];
$expectedValues = [];
foreach ($expected as $key => $value) {
$expectedKeys[] = $key;
$expectedValues[] = $value;
}

Assert::assertSame($keys, $expectedKeys, 'iterator keys are identical');
Assert::assertSame($values, $expectedValues, 'iterator values are identical');
}
}

0 comments on commit 2b4b0bc

Please sign in to comment.