Skip to content

Commit

Permalink
Add ChoiceEnumTrait & SimpleChoiceEnum
Browse files Browse the repository at this point in the history
  • Loading branch information
ogizanagi committed Apr 9, 2018
1 parent ea73095 commit 5a75fe9
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 1 deletion.
51 changes: 51 additions & 0 deletions 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 */
private static $guessedValues = [];

/** @var array */
private static $guessedReadables = [];

/**
* @see EnumInterface::values()
*
* @return int[]|string[]
*/
public static function values(): array
{
return static::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 static::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 (static::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
{
static::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
{
static::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()
{
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
25 changes: 25 additions & 0 deletions src/SimpleChoiceEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?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.
*/
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'];
}
}
54 changes: 54 additions & 0 deletions tests/Unit/SimpleChoiceEnumTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?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\SimpleChoiceEnum;
use PHPUnit\Framework\TestCase;

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

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

class DummySimpleChoiceEnum extends SimpleChoiceEnum
{
const FOO = 'foo';
const BAR = 'bar';
const BAZ = 'baz';
}

final class DummySimpleChoiceEnumWithLabelOverride extends DummySimpleChoiceEnum
{
protected static function choices(): array
{
return array_replace(parent::choices(), [
static::FOO => 'Foo label',
]);
}
}

0 comments on commit 5a75fe9

Please sign in to comment.