Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ChoiceEnumTrait & SimpleChoiceEnum #49

Merged
merged 1 commit into from
Apr 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Table of Contents
* [Installation](#installation)
* [Usage](#usage)
* [Readable enums](#readable-enums)
* [Choice enums](#choice-enums)
* [Flagged enums](#flagged-enums)
* [Compare](#compare)
* [Shortcuts](#shortcuts)
Expand Down Expand Up @@ -226,6 +227,58 @@ $enum = Gender::get(Gender::MALE);
$translator->trans($enum); // returns 'Male'
```

## Choice enums

Choice enums are a more opinionated version of readable enums. Using the `ChoiceEnumTrait` in your enum, you'll only
need to implement a `choices()` method instead of both `EnumInterface::values()` and `ReadableEnum::readables()` ones:

```php
<?php

use Elao\Enum\ChoiceEnumTrait;
use Elao\Enum\ReadableEnum;

final class Gender extends ReadableEnum
{
use ChoiceEnumTrait;

const UNKNOW = 'unknown';
const MALE = 'male';
const FEMALE = 'female';

public static function choices(): array
{
return [
self::UNKNOW => 'Unknown',
self::MALE => 'Male',
self::FEMALE => 'Female',
];
}
}
```

It is convenient as it implements the two `values` & `readables` methods for you, which means you don't have to keep it in sync anymore.

The `SimpleChoiceEnum` base class allows you to benefit from both choice enums conveniency along with enumerated values auto-discoverability through public constants:


```php
<?php

use Elao\Enum\SimpleChoiceEnum;

final class Gender extends SimpleChoiceEnum
{
const UNKNOW = 'unknown';
const MALE = 'male';
const FEMALE = 'female';
}
```

In addition, it'll provide default labels for each enumerated values based on a humanized version of their constant name
(i.e: "MALE" becomes "Male". "SOME_VALUE" becomes "Some value").
In case you need more accurate labels, simply override the `SimpleChoiceEnum::choices()` implementation.

## Flagged enums

Flagged enumerations are used for bitwise operations.
Expand Down
53 changes: 52 additions & 1 deletion src/AutoDiscoveredValuesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,50 @@

namespace Elao\Enum;

use Elao\Enum\Exception\LogicException;

/**
* Auto-discover enumerated values from public constants.
*/
trait AutoDiscoveredValuesTrait
{
/** @var array */
/** @internal */
private static $guessedValues = [];

/** @internal */
private static $guessedReadables = [];

/**
* @see EnumInterface::values()
*
* @return int[]|string[]
*/
public static function values(): array
{
return self::autodiscoveredValues();
}

/**
* @see ChoiceEnumTrait::choices()
*/
protected static function choices(): array
{
if (!in_array(ChoiceEnumTrait::class, class_uses(self::class, false), true)) {
throw new LogicException(sprintf(
'Method "%s" is only meant to be used when using the "%s" trait which is not used in "%s"',
__METHOD__,
ChoiceEnumTrait::class,
static::class
));
}

return self::autodiscoveredReadables();
}

/**
* @internal
*/
private static function autodiscoveredValues(): array
{
$enumType = static::class;

Expand All @@ -45,4 +78,22 @@ public static function values(): array

return self::$guessedValues[$enumType];
}

/**
* @internal
*/
private static function autodiscoveredReadables(): array
{
$enumType = static::class;

if (!isset(self::$guessedReadables[$enumType])) {
$constants = (new \ReflectionClass($enumType))->getConstants();
foreach (self::autodiscoveredValues() as $value) {
$constantName = array_search($value, $constants, true);
self::$guessedReadables[$enumType][$value] = ucfirst(strtolower(str_replace('_', ' ', $constantName)));
}
}

return self::$guessedReadables[$enumType];
}
}
76 changes: 76 additions & 0 deletions src/ChoiceEnumTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

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

namespace Elao\Enum;

use Elao\Enum\Exception\LogicException;

/**
* Discover readable enumerated values by returning the enumerated values as keys and their labels as values
* in {@link \Elao\Enum\ChoiceEnumTrait::choices()}, replacing the need to provide both:
* - {@link \Elao\Enum\ReadableEnumInterface::readables()}
* - {@link \Elao\Enum\ReadableEnumInterface::values()}
*
* Meant to be used within a {@link \Elao\Enum\ReadableEnumInterface} implementation.
*/
trait ChoiceEnumTrait
{
/**
* @see EnumInterface::values()
*
* @return int[]|string[]
*/
public static function values(): array
{
self::checkForChoiceEnumTraitMisuses();

$values = array_keys(static::choices());

if (is_a(static::class, FlaggedEnum::class, true)) {
$values = array_values(array_filter($values, function ($v): bool {
return 0 === ($v & $v - 1);
}));
}

return $values;
}

/**
* @see ReadableEnumInterface::readables()
*
* @return string[] labels indexed by enumerated value
*/
public static function readables(): array
{
self::checkForChoiceEnumTraitMisuses();

return static::choices();
}

/**
* @return string[] The enumerated values as keys and their labels as values.
*/
abstract protected static function choices(): array;

/**
* @internal
*/
private static function checkForChoiceEnumTraitMisuses()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First time I see this kind of trait guarding. But I guess it is needed, since at this point trait relationships are starting to be complicated. So 👍

{
if (!is_a(static::class, ReadableEnumInterface::class, true)) {
throw new LogicException(sprintf(
'The "%s" trait is meant to be used by "%s" implementations, but "%s" does not implement it.',
ChoiceEnumTrait::class,
ReadableEnumInterface::class,
static::class
));
}
}
}
2 changes: 1 addition & 1 deletion src/ReadableEnumInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface ReadableEnumInterface extends EnumInterface
/**
* Gets an array of the human representations indexed by possible values.
*
* @return array
* @return string[] labels indexed by enumerated value
*/
public static function readables(): array;

Expand Down
26 changes: 26 additions & 0 deletions src/SimpleChoiceEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

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

namespace Elao\Enum;

/**
* An opinionated enum implementation:
*
* - auto-discovers enumerated values from public constants.
* - implements {@link \Elao\Enum\ReadableEnumInterface} with default labels
* identical to enumerated values's constant name.
*/
class SimpleChoiceEnum extends ReadableEnum
{
use AutoDiscoveredValuesTrait;
use ChoiceEnumTrait {
ChoiceEnumTrait::values insteadof AutoDiscoveredValuesTrait;
}
}
21 changes: 21 additions & 0 deletions tests/Unit/AutoDiscoveredValuesTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ public function testItAutoDiscoveredValuesBasedOnAvailableBitFlagConstants()
{
$this->assertSame([1, 2, 4], AutoDiscoveredFlaggedEnum::values());
}

/**
* @expectedException \Elao\Enum\Exception\LogicException
* @expectedExceptionMessage Method "Elao\Enum\AutoDiscoveredValuesTrait::choices" is only meant to be used when using the "Elao\Enum\ChoiceEnumTrait" trait which is not used in "Elao\Enum\Tests\Unit\AutoDiscoveredEnumMisusingChoices"
*/
public function testThrowsOnChoicesMisuses()
{
AutoDiscoveredEnumMisusingChoices::foo();
}
}

final class AutoDiscoveredEnum extends Enum
Expand All @@ -57,3 +66,15 @@ final class AutoDiscoveredFlaggedEnum extends FlaggedEnum
const NOT_A_BIT_FLAG = 3;
const NOT_EVEN_AN_INT = 'not_even_an_int';
}

final class AutoDiscoveredEnumMisusingChoices extends Enum
{
use AutoDiscoveredValuesTrait;

const FOO = 'foo';

public static function foo()
{
self::choices();
}
}
112 changes: 112 additions & 0 deletions tests/Unit/ChoiceEnumTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

/*
* 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\ChoiceEnumTrait;
use Elao\Enum\Enum;
use Elao\Enum\FlaggedEnum;
use Elao\Enum\ReadableEnum;
use PHPUnit\Framework\TestCase;

class ChoiceEnumTraitTest extends TestCase
{
public function testItProvidesValuesAndReadablesImplementations()
{
$this->assertSame(['foo', 'bar', 'baz'], ChoiceEnum::values());
$this->assertSame([
ChoiceEnum::FOO => 'Foo label',
ChoiceEnum::BAR => 'Bar label',
ChoiceEnum::BAZ => 'Baz label',
], ChoiceEnum::readables());
}

public function testItFiltersValuesForFlaggedEnumImplementations()
{
$this->assertSame([1, 2, 4], FlaggedEnumWithChoiceEnumTrait::values());
$this->assertSame([
FlaggedEnumWithChoiceEnumTrait::EXECUTE => 'Execute',
FlaggedEnumWithChoiceEnumTrait::WRITE => 'Write',
FlaggedEnumWithChoiceEnumTrait::READ => 'Read',
FlaggedEnumWithChoiceEnumTrait::WRITE | FlaggedEnumWithChoiceEnumTrait::READ => 'Read & write',
FlaggedEnumWithChoiceEnumTrait::EXECUTE | FlaggedEnumWithChoiceEnumTrait::READ => 'Read & execute',
FlaggedEnumWithChoiceEnumTrait::ALL => 'All permissions',
], FlaggedEnumWithChoiceEnumTrait::readables());
}

/**
* @expectedException \Elao\Enum\Exception\LogicException
* @expectedExceptionMessage The "Elao\Enum\ChoiceEnumTrait" trait is meant to be used by "Elao\Enum\ReadableEnumInterface" implementations, but "Elao\Enum\Tests\Unit\NonReadableEnumWithChoiceEnumTrait" does not implement it.
*/
public function testValuesThrowsOnNonReadable()
{
NonReadableEnumWithChoiceEnumTrait::values();
}

/**
* @expectedException \Elao\Enum\Exception\LogicException
* @expectedExceptionMessage The "Elao\Enum\ChoiceEnumTrait" trait is meant to be used by "Elao\Enum\ReadableEnumInterface" implementations, but "Elao\Enum\Tests\Unit\NonReadableEnumWithChoiceEnumTrait" does not implement it.
*/
public function testReadableThrowsOnNonReadable()
{
NonReadableEnumWithChoiceEnumTrait::readables();
}
}

final class ChoiceEnum extends ReadableEnum
{
use ChoiceEnumTrait;

const FOO = 'foo';
const BAR = 'bar';
const BAZ = 'baz';

protected static function choices(): array
{
return [
static::FOO => 'Foo label',
static::BAR => 'Bar label',
static::BAZ => 'Baz label',
];
}
}

final class FlaggedEnumWithChoiceEnumTrait extends FlaggedEnum
{
use ChoiceEnumTrait;

const EXECUTE = 1;
const WRITE = 2;
const READ = 4;

const ALL = self::EXECUTE | self::WRITE | self::READ;

public static function choices(): array
{
return [
static::EXECUTE => 'Execute',
static::WRITE => 'Write',
static::READ => 'Read',
static::WRITE | static::READ => 'Read & write',
static::EXECUTE | static::READ => 'Read & execute',
static::ALL => 'All permissions',
];
}
}

final class NonReadableEnumWithChoiceEnumTrait extends Enum
{
use ChoiceEnumTrait;

protected static function choices(): array
{
return ['foo' => 'Foo label'];
}
}
Loading