Skip to content

Commit

Permalink
Implement ideas from RFC (#1)
Browse files Browse the repository at this point in the history
* On create object with invalid value throws ValueError instead UnexpectedValueException
* Use private constants in enum object
* Rename methods `toObjects()` to `cases()` and `toValues()` to `values()`
* Remove immutability
* Add method `getName()`
* Add method `tryFrom()`
* Add protected method `match()`
* Add info to change log
* Fix readme
  • Loading branch information
vjik authored May 27, 2021
1 parent 9cf6a45 commit 839e85e
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 96 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# PHP Enum Implementation Change Log

## 4.0.0 May 27, 2021

Implement ideas from [RFC Enumerations](https://wiki.php.net/rfc/enumerations):

- New: Add protected method `match()`.
- New: Add factory method `tryFrom()`.
- New: Add method `getName()`.
- Chg: Remove immutability objects.
- Chg: Rename methods `toObjects()` to `cases()` and `toValues()` to `values()`.
- Chg: Use private constants in enum object.
- Chg: On create object via method `from()` with invalid value throws `ValueError` instead `UnexpectedValueException`.

## 3.0.0 May 26, 2021

- Chg: Rewrite the package from scratch.
Expand Down
69 changes: 52 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fvjik%2Fphp-enum%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/vjik/php-enum/master)
[![static analysis](https://github.com/vjik/php-enum/workflows/static%20analysis/badge.svg)](https://github.com/vjik/php-enum/actions?query=workflow%3A%22static+analysis%22)

The package provide abstract class `Enum` that intended to create
[enumerated objects](https://en.wikipedia.org/wiki/Enumerated_type) with support [extra data](#extradata) and
auxiliary static functions [`toValues()`](#toList), [`toObjects()`](#toObjects) and [`isValid()`](#isValid).
The package implement ideas from [RFC Enumerations](https://wiki.php.net/rfc/enumerations) and provide abstract class `Enum` that intended to create
[enumerated objects](https://en.wikipedia.org/wiki/Enumerated_type) with support [extra data](#extradata) and auxiliary static functions [`values()`](#values), [`cases()`](#cases) and [`isValid()`](#isValid).

## Requirements

Expand Down Expand Up @@ -36,9 +35,9 @@ use Vjik\Enum\Enum;
*/
final class Status extends Enum
{
public const NEW = 'new';
public const PROCESS = 'process';
public const DONE = 'done';
private const NEW = 'new';
private const PROCESS = 'process';
private const DONE = 'done';
}
```

Expand All @@ -50,6 +49,17 @@ final class Status extends Enum
$process = Status::from('process');
```

On create object with invalid value throws `ValueError`.

#### By static method `tryFrom()`

```php
$process = Status::tryFrom('process'); // Status object with value "process"
$process = Status::tryFrom('not-exists'); // null
```

On create object with invalid value returns `null`.

#### By static method with a name identical to the constant name

Static methods are automatically implemented to provide quick access to an enum value.
Expand All @@ -58,9 +68,17 @@ Static methods are automatically implemented to provide quick access to an enum
$process = Status::PROCESS();
```

### Getting value and name

```php
Status::DONE()->getName(); // DONE
Status::DONE()->getValue(); // done
```

### <a name="extradata"></a>Class with extra data

Set data in the protected static function `data()` and create getters using the protected method `getPropertyValue()`:
Set data in the protected static function `data()` and create getters using the protected method `getPropertyValue()`.
Also you can create getter using protected method `match()`.

```php
use Vjik\Enum\Enum;
Expand All @@ -71,8 +89,8 @@ use Vjik\Enum\Enum;
*/
final class Action extends Enum
{
public const CREATE = 1;
public const UPDATE = 2;
private const CREATE = 1;
private const UPDATE = 2;

protected static function data(): array
{
Expand All @@ -91,33 +109,50 @@ final class Action extends Enum
/** @var string */
return $this->getPropertyValue('tip');
}

public function getColor(): string
{
return $this->match([
self::CREATE => 'red',
self::UPDATE => 'blue',
]);
}

public function getCode(): int
{
return $this->match([
self::CREATE => 1,
], 99);
}
}
```

Usage:

```php
echo Action::CREATE()->getTip();
echo Action::CREATE()->getColor();
echo Action::CREATE()->getCode();
```

### Auxiliary static functions

#### <a name="toValues"></a> List of values `toValues()`
#### <a name="values"></a> List of values `values()`

Returns array of pairs constant names and values.
Returns list of values.

```php
// ['CREATE' => 1, 'UPDATE' => 2]
Action::toValues();
// [1, 2]
Action::values();
```

#### <a name="toObjects"></a> List of objects `toObjects()`
#### <a name="cases"></a> List of objects `cases()`

Returns array of pairs constant names and objects:
Returns list of objects:

```php
// ['CREATE' => $createObject, 'UPDATE' => $updateObject]
Action::toObjects();
// [$createObject, $updateObject]
Action::cases();
```

#### <a name="isValid"></a> Validate value `isValid()`
Expand Down
111 changes: 78 additions & 33 deletions src/Enum.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
use BadMethodCallException;
use ReflectionClass;
use ReflectionClassConstant;
use UnexpectedValueException;
use ValueError;

use function constant;
use function defined;
use function array_key_exists;
use function in_array;

abstract class Enum
{
private string $name;
private mixed $value;

/**
Expand All @@ -27,15 +27,17 @@ abstract class Enum
*/
private static array $instances = [];

final protected function __construct(mixed $value)
final protected function __construct(string $name, mixed $value)
{
if (!self::isValid($value)) {
throw new UnexpectedValueException("Value '$value' is not part of the enum " . static::class . '.');
}

$this->name = $name;
$this->value = $value;
}

final public function getName(): string
{
return $this->name;
}

final public function getValue(): mixed
{
return $this->value;
Expand All @@ -51,7 +53,20 @@ final public function __toString(): string
*/
final public static function from(mixed $value): self
{
return new static($value);
$object = static::getInstanceByValue($value);
if ($object === null) {
throw new ValueError("Value '$value' is not part of the enum " . static::class . '.');
}

return $object;
}

/**
* @return static|self
*/
final public static function tryFrom(mixed $value): ?self
{
return static::getInstanceByValue($value);
}

/**
Expand All @@ -61,54 +76,43 @@ final public static function __callStatic(string $name, array $arguments): self
{
$class = static::class;
if (!isset(self::$instances[$class][$name])) {
$constant = $class . '::' . $name;
if (!defined($constant)) {
$enumValues = static::getEnumValues();
if (!array_key_exists($name, $enumValues)) {
$message = "No static method or enum constant '$name' in class " . static::class . '.';
throw new BadMethodCallException($message);
}
return self::$instances[$class][$name] = new static(constant($constant));
self::$instances[$class][$name] = new static($name, $enumValues[$name]);
}
return clone self::$instances[$class][$name];
return self::$instances[$class][$name];
}

final public static function toValues(): array
final public static function values(): array
{
$class = static::class;

if (!isset(static::$cache[$class])) {
/** @psalm-suppress TooManyArguments Remove this after fix https://github.com/vimeo/psalm/issues/5837 */
static::$cache[$class] = (new ReflectionClass($class))->getConstants(ReflectionClassConstant::IS_PUBLIC);
}

return static::$cache[$class];
return array_values(self::getEnumValues());
}

/**
* @return static[]
*/
final public static function toObjects(): array
final public static function cases(): array
{
$class = static::class;

$objects = [];
/**
* @var string $key
* @var mixed $value
*/
foreach (self::toValues() as $key => $value) {
if (isset(self::$instances[$class][$key])) {
$objects[$key] = clone self::$instances[$class][$key];
} else {
$objects[$key] = self::$instances[$class][$key] = new static($value);
/** @var mixed $value */
foreach (self::getEnumValues() as $key => $value) {
if (!isset(self::$instances[$class][$key])) {
self::$instances[$class][$key] = new static($key, $value);
}
$objects[] = self::$instances[$class][$key];
}

return $objects;
}

final public static function isValid(mixed $value): bool
{
return in_array($value, static::toValues(), true);
return in_array($value, static::getEnumValues(), true);
}

/**
Expand All @@ -124,4 +128,45 @@ final protected function getPropertyValue(string $key, mixed $default = null): m
/** @psalm-suppress MixedArrayOffset */
return static::data()[$this->value][$key] ?? $default;
}

final protected function match(array $data, mixed $default = null): mixed
{
/** @psalm-suppress MixedArrayOffset */
return $data[$this->value] ?? $default;
}

/**
* @return static|null
*/
private static function getInstanceByValue(mixed $value): ?self
{
$class = static::class;

/** @var mixed $enumValue */
foreach (self::getEnumValues() as $key => $enumValue) {
if ($enumValue === $value) {
if (!isset(self::$instances[$class][$key])) {
self::$instances[$class][$key] = new static($key, $value);
}
return self::$instances[$class][$key];
}
}

return null;
}

/**
* @psalm-return array<string,mixed>
*/
private static function getEnumValues(): array
{
$class = static::class;

if (!isset(static::$cache[$class])) {
/** @psalm-suppress TooManyArguments Remove this after fix https://github.com/vimeo/psalm/issues/5837 */
static::$cache[$class] = (new ReflectionClass($class))->getConstants(ReflectionClassConstant::IS_PRIVATE);
}

return static::$cache[$class];
}
}
Loading

0 comments on commit 839e85e

Please sign in to comment.