Skip to content

Commit

Permalink
feature #201 [Readable] Allow to configure a common suffix/prefix & u…
Browse files Browse the repository at this point in the history
…se the enum case value/name as default label (ogizanagi)

This PR was merged into the 2.x-dev branch.

Discussion
----------

[Readable] Allow to configure a common suffix/prefix & use the enum case value/name as default label

Eases the declaration of readable enum translations keys with common suffix/prefix:

```php
#[ReadableEnum(prefix: 'suit.')]
enum Suit: string implements ReadableEnumInterface
{
    use ReadableEnumTrait;

    case Hearts = '♥︎';
    case Diamonds = '♦︎';
    case Clubs = '♣︎';
    case Spades = '︎♠︎';
}

Suit::Hearts->getReadable(); // returns 'suit.Hearts'
Suit::Clubs->getReadable(); // returns 'suit.Clubs'
```

[Docs](https://github.com/Elao/PhpEnums/blob/readable-auto/README.md#configure-suffixprefix--default-value)

## Questions

- Should we actually default on the `name` rather than the value by default when using this attribute?
- Name this attribute `ConfigureReadableEnum`? `AutoReadableEnum`?
- …

Commits
-------

deccd5a [Readable] Allow to configure a common suffix & prefix & use the enum case value/name as default label
  • Loading branch information
ogizanagi committed Nov 14, 2022
2 parents 74ba8a2 + deccd5a commit baae644
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 22 deletions.
66 changes: 50 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,15 @@ _Provides additional, opinionated features to the [PHP 8.1+ native enums](https:
as specific integrations with frameworks and libraries._

```php
#[ReadableEnum(prefix: 'suit.')]
enum Suit: string implements ReadableEnumInterface
{
use ReadableEnumTrait;

#[EnumCase('suit.hearts')]
case Hearts = 'H';

#[EnumCase('suit.diamonds')]
case Diamonds = 'D';

#[EnumCase('suit.clubs')]
case Clubs = 'C';

#[EnumCase('suit.spades')]
case Spades = 'S';
case Hearts = '♥︎';
case Diamonds = '♦︎';
case Clubs = '♣︎';
case Spades = '︎♠︎';
}
```

Expand Down Expand Up @@ -85,20 +79,20 @@ enum Suit: string implements ReadableEnumInterface
use ReadableEnumTrait;

#[EnumCase('suit.hearts')]
case Hearts = 'H';
case Hearts = '♥︎';

#[EnumCase('suit.diamonds')]
case Diamonds = 'D';
case Diamonds = '♦︎';

#[EnumCase('suit.clubs')]
case Clubs = 'C';
case Clubs = '♣︎';

#[EnumCase('suit.spades')]
case Spades = 'S';
case Spades = '︎♠︎';
}
```

The following snippet shows how to get the human readable value of an enum:
The following snippet shows how to get the human-readable value of an enum:

```php
Suit::Hearts->getReadable(); // returns 'suit.hearts'
Expand Down Expand Up @@ -126,6 +120,46 @@ $enum = Suit::Hearts;
$translator->trans($enum->getReadable(), locale: 'fr'); // returns 'Coeurs'
```
### Configure suffix/prefix & default value
As a shorcut, you can also use the [`ReadableEnum`](src/Attribute/ReadableEnum.php) attribute to define the
common `suffix` and `prefix` to use, as well as defaulting on the enum case name or value, if not provided explicitly:

```php
#[ReadableEnum(prefix: 'suit.')]
enum Suit: string implements ReadableEnumInterface
{
use ReadableEnumTrait;
#[EnumCase('hearts︎')]
case Hearts = '♥︎';
case Diamonds = '♦︎';
case Clubs = '♣︎';
case Spades = '︎♠︎';
}
Suit::Hearts->getReadable(); // returns 'suit.hearts'
Suit::Clubs->getReadable(); // returns 'suit.Clubs'
```

using the case value (only for string backed enums):

```php
#[ReadableEnum(prefix: 'suit.', useValueAsDefault: true)]
enum Suit: string implements ReadableEnumInterface
{
use ReadableEnumTrait;
case Hearts = 'hearts';
case Diamonds = 'diamonds';
case Clubs = 'clubs︎';
case Spades = '︎spades';
}
Suit::Hearts->getReadable(); // returns 'suit.hearts'
Suit::Clubs->getReadable(); // returns 'suit.clubs'
```

## 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`:
Expand Down
27 changes: 27 additions & 0 deletions src/Attribute/ReadableEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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\Attribute;

/**
* Autoconfigure a readable enum cases' labels, using the name or value + allow to configure a prefix and/or suffix for the key.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ReadableEnum
{
public function __construct(
public readonly ?string $prefix = null,
public readonly ?string $suffix = null,
public readonly bool $useValueAsDefault = false
) {
}
}
45 changes: 39 additions & 6 deletions src/ReadableEnumTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace Elao\Enum;

use Elao\Enum\Attribute\EnumCase;
use Elao\Enum\Attribute\ReadableEnum;
use Elao\Enum\Exception\LogicException;
use Elao\Enum\Exception\NameException;

Expand Down Expand Up @@ -69,42 +70,60 @@ public function getReadable(): string
/**
* {@inheritdoc}
*
* Implements readables using PHP 8 attributes, expecting an {@link EnumCase} on each case, with a label.
* Implements readables using PHP 8 attributes, expecting an {@link EnumCase} on each case, with a label,
* or uses the value as label if {@link ReadableEnum} is used on the class.
*/
public static function readables(): iterable
{
static $readables;

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

if (($readableEnumAttribute?->useValueAsDefault ?? false) && 'string' !== (string) $r->getBackingType()) {
throw new LogicException(sprintf(
'Cannot use "useValueAsDefault" with "#[%s]" attribute on enum "%s" as it\'s not a string backed enum.',
ReadableEnum::class,
static::class,
));
}

/** @var static $case */
foreach (static::cases() as $case) {
$attribute = $case->getEnumCaseAttribute();

if (null === $attribute) {
if (null === $attribute && null === $readableEnumAttribute) {
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',
'enum "%s" using the "%s" trait must define a "%s" attribute on every cases. Case "%s" is missing one. Alternatively, override the "%s()" method, or use the "%s" attribute on the enum class to use the value as default.',
static::class,
ReadableEnumTrait::class,
EnumCase::class,
$case->name,
__METHOD__,
ReadableEnum::class,
));
}

if (null === $attribute->label) {
if (null === $attribute?->label && null === $readableEnumAttribute) {
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',
'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, or use the "#[%s]" attribute on the enum class to use the value as default.',
static::class,
ReadableEnumTrait::class,
EnumCase::class,
$case->name,
__METHOD__,
ReadableEnum::class,
));
}

$readables[$case] = $attribute->label;
$readables[$case] = sprintf(
'%s%s%s',
$readableEnumAttribute?->prefix,
$attribute?->label ?? ($readableEnumAttribute->useValueAsDefault ? $case->value : $case->name),
$readableEnumAttribute?->suffix,
);
}
}

Expand All @@ -114,6 +133,20 @@ public static function readables(): iterable
}
}

/**
* @internal
*/
private static function getReadableEnumAttribute(): ?ReadableEnum
{
$r = new \ReflectionEnum(static::class);

if (null === $rAttr = $r->getAttributes(ReadableEnum::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
return null;
}

return $rAttr->newInstance();
}

/**
* As objects, {@link https://wiki.php.net/rfc/enumerations#splobjectstorage_and_weakmaps Enum cases cannot be used as keys in an array}.
* However, they can be used as keys in a SplObjectStorage or WeakMap.
Expand Down
136 changes: 136 additions & 0 deletions tests/Unit/ReadableEnumAttributeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?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\Unit;

use Elao\Enum\Attribute\EnumCase;
use Elao\Enum\Attribute\ReadableEnum;
use Elao\Enum\Exception\LogicException;
use Elao\Enum\ReadableEnumInterface;
use Elao\Enum\ReadableEnumTrait;
use PHPUnit\Framework\TestCase;

class ReadableEnumAttributeTest extends TestCase
{
public function testReadableEnumAttribute(): void
{
self::assertSame(
'suit.Clubs.label',
ReadableAttributeSuit::readableForValue(ReadableAttributeSuit::Clubs->value),
'uses the name as default, with suffix and prefix',
);
self::assertSame(
'suit.hearts.label',
ReadableAttributeSuit::readableForValue(ReadableAttributeSuit::Hearts->value),
'uses the explicit label value, with suffix and prefix',
);
}

public function testReadableEnumAttributeWithoutSuffixPrefix(): void
{
self::assertSame(
'Clubs',
ReadableAttributeSuitWithoutSuffixPrefix::readableForValue(ReadableAttributeSuitWithoutSuffixPrefix::Clubs->value),
'uses the name as default, without any suffix or prefix',
);
self::assertSame(
'hearts',
ReadableAttributeSuitWithoutSuffixPrefix::readableForValue(ReadableAttributeSuitWithoutSuffixPrefix::Hearts->value),
'uses the explicit label value, without any suffix or prefix',
);
}

public function testReadableEnumAttributeWithValueAsDefault(): void
{
self::assertSame(
'suit.♣︎.label',
ReadableSuitAttributeWithValueAsDefault::readableForValue(ReadableSuitAttributeWithValueAsDefault::Clubs->value),
'uses the value as default, with suffix and prefix',
);
self::assertSame(
'suit.hearts.label',
ReadableSuitAttributeWithValueAsDefault::readableForValue(ReadableSuitAttributeWithValueAsDefault::Hearts->value),
'uses the explicit label value, with suffix and prefix',
);
}

public function testReadableEnumAttributeWithValueAsDefaultThrowsOnPureEnum(): void
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Cannot use "useValueAsDefault" with "#[Elao\Enum\Attribute\ReadableEnum]" attribute on enum "Elao\Enum\Tests\Unit\PureEnumWithReadableAttribute" as it\'s not a string backed enum.');

PureEnumWithReadableAttribute::Foo->getReadable();
}

public function testReadableEnumAttributeWithValueAsDefaultThrowsOnIntBackedEnum(): void
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Cannot use "useValueAsDefault" with "#[Elao\Enum\Attribute\ReadableEnum]" attribute on enum "Elao\Enum\Tests\Unit\IntBackedEnumWithReadableAttribute" as it\'s not a string backed enum.');

IntBackedEnumWithReadableAttribute::Foo->getReadable();
}
}

#[ReadableEnum(prefix: 'suit.', suffix: '.label')]
enum ReadableAttributeSuit: string implements ReadableEnumInterface
{
use ReadableEnumTrait;

#[EnumCase('hearts')]
case Hearts = '♥︎';

case Diamonds = '♦︎';
case Clubs = '♣︎';
case Spades = '︎♠︎';
}

#[ReadableEnum]
enum ReadableAttributeSuitWithoutSuffixPrefix: string implements ReadableEnumInterface
{
use ReadableEnumTrait;

#[EnumCase('hearts')]
case Hearts = '♥︎';

case Diamonds = '♦︎';
case Clubs = '♣︎';
case Spades = '︎♠︎';
}

#[ReadableEnum(prefix: 'suit.', suffix: '.label', useValueAsDefault: true)]
enum ReadableSuitAttributeWithValueAsDefault: string implements ReadableEnumInterface
{
use ReadableEnumTrait;

#[EnumCase('hearts')]
case Hearts = '♥︎';

case Diamonds = '♦︎';
case Clubs = '♣︎';
case Spades = '︎♠︎';
}

#[ReadableEnum(useValueAsDefault: true)]
enum PureEnumWithReadableAttribute implements ReadableEnumInterface
{
use ReadableEnumTrait;

case Foo;
}

#[ReadableEnum(useValueAsDefault: true)]
enum IntBackedEnumWithReadableAttribute: int implements ReadableEnumInterface
{
use ReadableEnumTrait;

case Foo = 1;
}

0 comments on commit baae644

Please sign in to comment.