Commit 69816bb0 authored by Jan Kuchař's avatar Jan Kuchař

Merge branch 'cleanup' into 'master'

Cleanup & strictness

Closes #1

See merge request !13
parents 9847c914 1d18b2c7
Pipeline #14406 passed with stages
in 34 seconds
/vendor/
composer.lock
.idea
**/output/*
\ No newline at end of file
......@@ -2,9 +2,12 @@
namespace Grifart\Enum;
/**
* Allows you to use you defined constants automatically as enum value.
* Without explicitly implementing each enum value.
*/
trait AutoInstances
{
// todo: better define this interface
abstract protected static function getConstantToScalar(): array;
/** @param string|int $scalar */
......@@ -13,8 +16,8 @@ trait AutoInstances
protected static function provideInstances(): array
{
$instances = [];
foreach (static::getConstantToScalar() as $constantName => $primitiveValue) {
$instances[] = new static($primitiveValue);
foreach (static::getConstantToScalar() as $scalarValue) {
$instances[] = new static($scalarValue);
}
return $instances;
}
......
......@@ -6,12 +6,13 @@ use Grifart\Enum\Internal\InstanceRegister;
use Grifart\Enum\Internal\Meta;
/**
* Enum
* Enumeration class with support for strong-typing support and behaviour-rich values.
*
* Three concepts:
* - constant name = used to refer to enum value
* - value = the enum instance
* - scalar value = scalar value of enum, must be unique, used for serialization
* Three basic concepts:
* - **value** = the enum instance
* - **scalar** = scalar identifier of enum value; typically used in persistence layer to refer to particular value
* - **constant** = each value has associated class constant, which is used to refer to value from code.
* Constant name is used to generate static method for each of them. Constants are therefore usually not public.
*/
abstract class Enum
{
......@@ -32,13 +33,12 @@ abstract class Enum
}
/**
* @return string[]|int[]
* @return array<string,string|int>
*/
protected static function getConstantToScalar(): array
{
try {
return (new \ReflectionClass(static::class))
->getConstants();
return (new \ReflectionClass(static::class))->getConstants();
} catch (\ReflectionException $e) {
throw new ReflectionFailedException($e);
}
......@@ -71,13 +71,21 @@ abstract class Enum
return $value;
}
private static function getMeta(): Meta
private static function getMeta(bool $checkIfAccessingRootDirectly = true): Meta
{
$rootClass = static::getRootClass();
if ($checkIfAccessingRootDirectly && $rootClass !== static::class) {
throw new UsageException(
'You have accessed static enum method on non-root class '
. "('$rootClass' is a root class)"
);
}
return InstanceRegister::get(
static::getEnumClassName(),
function (): Meta {
$rootClass,
function () use ($rootClass): Meta {
return Meta::from(
static::getEnumClassName(),
$rootClass,
static::getConstantToScalar(),
static::provideInstances()
);
......@@ -85,18 +93,17 @@ abstract class Enum
);
}
private static function getEnumClassName(): string
private static function getRootClass(): string
{
try {
$ref = new \ReflectionClass(static::class);
if ($ref->isAnonymous()) { // anonymous objects are used for values
$ref = $ref->getMethod('provideInstances')->getDeclaringClass();
}
return (new \ReflectionClass(static::class))
->getMethod('provideInstances')
->getDeclaringClass()
->getName();
} catch (\ReflectionException $e) {
throw new ReflectionFailedException($e);
}
return $ref->getName();
}
......@@ -139,7 +146,7 @@ abstract class Enum
*/
public function getConstantName(): string
{
return self::getMeta()->getConstantNameForScalar(
return $this::getMeta(FALSE)->getConstantNameForScalar(
$this->toScalar()
);
}
......
......@@ -4,18 +4,22 @@ namespace Grifart\Enum\Internal;
use Grifart\Enum\UsageException;
/**
* Checks if registering enum does not contain error.
*/
final class ConsistencyChecker
{
public static function checkAnnotations(Meta $enumMeta): void
{
$enumReflection = new \ReflectionClass($enumMeta->getClass());
self::checkCallStaticAnnotations($enumMeta, $enumReflection);
self::checkAllInstancesProvided($enumMeta, $enumReflection->getName());
self::checkCallStaticAnnotations($enumMeta);
self::checkAllInstancesProvided($enumMeta);
self::checkAbstractAndFinal($enumMeta);
}
private static function checkCallStaticAnnotations(Meta $enumMeta, \ReflectionClass $enumReflection): void
private static function checkCallStaticAnnotations(Meta $enumMeta): void
{
$enumReflection = $enumMeta->getClassReflection();
$docBlock = $enumReflection->getDocComment();
$className = $enumReflection->getShortName();
if ($docBlock === false) {
......@@ -37,7 +41,7 @@ final class ConsistencyChecker
// todo: @method annotations without constants
}
private static function checkAllInstancesProvided(Meta $enumMeta, string $className): void
private static function checkAllInstancesProvided(Meta $enumMeta): void
{
// todo: instances without constants
......@@ -48,4 +52,17 @@ final class ConsistencyChecker
}
}
}
private static function checkAbstractAndFinal(Meta $enumMeta): void
{
$enumReflection = $enumMeta->getClassReflection();
if (!$enumReflection->isFinal() && !$enumReflection->isAbstract()) {
throw new UsageException(
"Enum root class must be either abstract or final.\n"
. "Final is used when one type is enough for all enum instance values.\n"
. 'Abstract is used when enum values are always instances of child classes of enum root class.'
);
}
}
}
......@@ -2,28 +2,33 @@
namespace Grifart\Enum\Internal;
/**
* Keeps track of all enum instances organized by enum root classes.
*/
final class InstanceRegister
{
/** @var \Grifart\Enum\Internal\Meta[] */
private static $instances = [];
public static function get(string $enumClass, callable $registrator = null): Meta
public static function get(string $enumClass, callable $registrar): Meta
{
if (!isset(self::$instances[$enumClass]) && $registrator !== null) {
self::register($registrator());
if (!isset(self::$instances[$enumClass])) {
self::register($enumClass, $registrar());
}
return self::$instances[$enumClass];
}
public static function register(Meta $meta): void
public static function register(string $className, Meta $meta): void
{
// check consistency of enum when assertions are enabled
\assert($meta->getClass() === $className, 'Provided Meta object is for different enum class that was originally registered.');
// check consistency of enum when assertions are enabled (typically non-production code)
assert(
(function () use ($meta): bool {
ConsistencyChecker::checkAnnotations($meta);
return true;
})()
);
self::$instances[$meta->getClass()] = $meta;
self::$instances[$className] = $meta;
}
}
......@@ -4,6 +4,7 @@ namespace Grifart\Enum\Internal;
use Grifart\Enum\Enum;
use Grifart\Enum\MissingValueDeclarationException;
use Grifart\Enum\ReflectionFailedException;
use Grifart\Enum\UsageException;
final class Meta
......@@ -78,6 +79,15 @@ final class Meta
return $this->class;
}
public function getClassReflection(): \ReflectionClass
{
try {
return new \ReflectionClass($this->getClass());
} catch (\ReflectionException $e) {
throw new ReflectionFailedException($e);
}
}
/**
* @return string[]
*/
......
......@@ -4,8 +4,8 @@
* Contains all exceptions used in project.
*
* - Usage exceptions: leads directly to fix by programmer. They are never caught and should never happen on production.
* - Runtime exception: they represent valid case in domain logic. They should be handled at runtime and caught.
* Therefore every error should have separate exception type.
* - Runtime exception: they represent valid case in domain logic. They should be handled at runtime and caught by user.
* Therefore every error should have separate exception type (they can create inheritance tree)
*/
namespace Grifart\Enum;
......
......@@ -7,7 +7,7 @@ require __DIR__ . '/../bootstrap.php';
* @method static EnumString VALUE1()
* @method static EnumString VALUE2()
*/
class EnumString extends \Grifart\Enum\Enum
final class EnumString extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......@@ -26,7 +26,7 @@ class EnumString extends \Grifart\Enum\Enum
* @method static EnumInt VALUE1()
* @method static EnumInt VALUE2()
*/
class EnumInt extends \Grifart\Enum\Enum
final class EnumInt extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......
......@@ -6,7 +6,7 @@ require __DIR__ . '/../bootstrap.php';
* @method static OrderState ACTIVE()
* @method static OrderState DELIVERED()
*/
class OrderState extends \Grifart\Enum\Enum
final class OrderState extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......
......@@ -5,7 +5,7 @@ require __DIR__ . '/../bootstrap.php';
* @method static EqualsState NEW()
* @method static EqualsState ACTIVE()
*/
class EqualsState extends \Grifart\Enum\Enum
final class EqualsState extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......
......@@ -7,7 +7,7 @@ require __DIR__ . '/../bootstrap.php';
* @method static Enum1 VALUE1()
* @method static Enum1 VALUE2()
*/
class Enum1 extends \Grifart\Enum\Enum
final class Enum1 extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
protected const VALUE1 = 'value1';
......
......@@ -7,7 +7,7 @@ require __DIR__ . '/../bootstrap.php';
* @method static Enum1 VALUE1()
* @method static Enum1 VALUE2()
*/
class Enum1 extends \Grifart\Enum\Enum
final class Enum1 extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......
<?php declare(strict_types=1);
namespace Tests\Grifart\Enum\Consistency;
use Grifart\Enum\UsageException;
use Tester\Assert;
require __DIR__ . '/../bootstrap.php';
class UndecidedEnum extends \Grifart\Enum\Enum
{
protected static function provideInstances(): array
{
return [];
}
}
Assert::exception(
function () {
UndecidedEnum::getAvailableValues();
},
UsageException::class,
"Enum root class must be either abstract or final.\n"
. "Final is used when one type is enough for all enum instance values.\n"
. 'Abstract is used when enum values are always instances of child classes of enum root class.'
);
<?php declare(strict_types=1);
require __DIR__ . '/../bootstrap.php';
class MethodAnnotationsMissing extends \Grifart\Enum\Enum
final class MethodAnnotationsMissing extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......
......@@ -11,7 +11,7 @@ require __DIR__ . '/../bootstrap.php';
* @method static MissingInstanceEnum STATE_A()
* @method static MissingInstanceEnum STATE_B()
*/
class MissingInstanceEnum extends \Grifart\Enum\Enum
abstract class MissingInstanceEnum extends \Grifart\Enum\Enum
{
protected const STATE_A = 'a';
protected const STATE_B = 'b';
......
......@@ -5,7 +5,7 @@ require __DIR__ . '/../../bootstrap.php';
use Tester\Assert;
class OrderState {
final class OrderState {
public const NEW = 'new';
public const PROCESSING = 'processing';
}
......
......@@ -9,7 +9,7 @@ use Tester\Assert;
* @method static OrderState NEW()
* @method static OrderState PROCESSING()
*/
class OrderState extends \Grifart\Enum\Enum {
final class OrderState extends \Grifart\Enum\Enum {
use \Grifart\Enum\AutoInstances;
public const NEW = 'new';
public const PROCESSING = 'processing';
......
......@@ -9,7 +9,7 @@ use Tester\Assert;
* @method static OrderState NEW()
* @method static OrderState PROCESSING()
*/
class OrderState extends \Grifart\Enum\Enum {
final class OrderState extends \Grifart\Enum\Enum {
use \Grifart\Enum\AutoInstances;
private const NEW = 'new';
private const PROCESSING = 'processing';
......
......@@ -9,7 +9,7 @@ use Tester\Assert;
* @method static OrderState NEW()
* @method static OrderState PROCESSING()
*/
class OrderState extends \Grifart\Enum\Enum {
final class OrderState extends \Grifart\Enum\Enum {
use \Grifart\Enum\AutoInstances;
private const NEW = 'new';
private const PROCESSING = 'processing';
......
......@@ -5,7 +5,7 @@ require __DIR__ . '/../bootstrap.php';
* @method static ReflectionConstantNames NEW()
* @method static ReflectionConstantNames ACTIVE()
*/
class ReflectionConstantNames extends \Grifart\Enum\Enum
final class ReflectionConstantNames extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......
......@@ -5,7 +5,7 @@ require __DIR__ . '/../bootstrap.php';
* @method static AvailableValuesEnum NEW()
* @method static AvailableValuesEnum ACTIVE()
*/
class AvailableValuesEnum extends \Grifart\Enum\Enum
final class AvailableValuesEnum extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......
......@@ -5,7 +5,7 @@ require __DIR__ . '/../bootstrap.php';
/**
* @method static Enum1 VALUE()
*/
class Enum1 extends \Grifart\Enum\Enum
final class Enum1 extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
protected const VALUE = 'value';
......
......@@ -7,7 +7,7 @@ require __DIR__ . '/../bootstrap.php';
/**
* @method static Enum1 VALUE()
*/
class Enum1 extends \Grifart\Enum\Enum
final class Enum1 extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
protected const VALUE = 'value';
......
<?php declare(strict_types=1);
namespace TestFullClasses;
use Grifart\Enum\UsageException;
use Tester\Assert;
require __DIR__ . '/../bootstrap.php';
/**
* @method static FullClassesAsValuesEnum VALUE1()
* @method static FullClassesAsValuesEnum VALUE2()
*/
abstract class FullClassesAsValuesEnum extends \Grifart\Enum\Enum
{
private const VALUE1 = 1;
private const VALUE2 = 2;
protected static function provideInstances(): array
{
return [
new Value1(self::VALUE1),
new Value2(self::VALUE2),
];
}
}
final class Value1 extends FullClassesAsValuesEnum { }
class Value2 extends FullClassesAsValuesEnum { }
// Standard APIs:
Assert::equal(FullClassesAsValuesEnum::VALUE1()->toScalar(), 1);
Assert::equal(FullClassesAsValuesEnum::VALUE2()->toScalar(), 2);
Assert::type(Value1::class, FullClassesAsValuesEnum::VALUE1());
Assert::type(Value2::class, FullClassesAsValuesEnum::VALUE2());
Assert::same(
[
FullClassesAsValuesEnum::VALUE1(),
FullClassesAsValuesEnum::VALUE2()
],
FullClassesAsValuesEnum::getAvailableValues()
);
Assert::same(FullClassesAsValuesEnum::VALUE1(), FullClassesAsValuesEnum::fromScalar(1));
// ## Wrong usage & edge-cases
// wrong usage:
$expectNonRootAccess = function(callable $fn) {
Assert::exception(
$fn,
UsageException::class,
"You have accessed static enum method on non-root class ('TestFullClasses\\FullClassesAsValuesEnum' is a root class)"
);
};
$expectNonRootAccess(function () {
Value1::getAvailableValues();
});
$expectNonRootAccess(function () {
Value1::fromScalar('1');
});
$expectNonRootAccess(function () {
Value1::VALUE1();
});
$expectNonRootAccess(function () {
Value1::VALUE2();
});
// valid edge case: this is valid and accesses registers the same way as calls above
Assert::same(
'VALUE1',
FullClassesAsValuesEnum::VALUE1()
->getConstantName()
);
......@@ -5,7 +5,7 @@ require __DIR__ . '/../bootstrap.php';
/**
* @method static Enum1 VALUE()
*/
class Enum1 extends \Grifart\Enum\Enum
final class Enum1 extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......@@ -15,7 +15,7 @@ class Enum1 extends \Grifart\Enum\Enum
/**
* @method static Enum2 VALUE()
*/
class Enum2 extends \Grifart\Enum\Enum
final class Enum2 extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......
......@@ -6,7 +6,7 @@ require __DIR__ . '/../bootstrap.php';
* @method static EnumMixedKeys VALUE_STRING()
* @method static EnumMixedKeys VALUE_INT()
*/
class EnumMixedKeys extends \Grifart\Enum\Enum
final class EnumMixedKeys extends \Grifart\Enum\Enum
{
use Grifart\Enum\AutoInstances;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment