Initial commit after upgrading to php 8.3 and splitting to seperated tool chain
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
coverage: none
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress
|
||||
- name: PHPStan
|
||||
run: composer phpstan
|
||||
- name: Code style check
|
||||
run: composer cs-check
|
||||
- name: Tests
|
||||
run: composer test
|
||||
@@ -0,0 +1,3 @@
|
||||
/vendor/
|
||||
/.php-cs-fixer.cache
|
||||
/.phpunit.result.cache
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Finder;
|
||||
|
||||
$finder = Finder::create()
|
||||
->in([
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/tests',
|
||||
])
|
||||
->name('*.php')
|
||||
->notName('*.blade.php')
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCS(true);
|
||||
|
||||
return (new Config())
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PER-CS2.0' => true,
|
||||
'@PHP83Migration' => true,
|
||||
'ordered_imports' => ['sort_algorithm' => 'alpha'],
|
||||
'no_unused_imports' => true,
|
||||
'global_namespace_import' => ['import_classes' => false, 'import_constants' => false, 'import_functions' => false],
|
||||
'single_quote' => true,
|
||||
'explicit_string_variable' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'trim_array_spaces' => true,
|
||||
'no_whitespace_before_comma_in_array' => true,
|
||||
'whitespace_after_comma_in_array' => ['ensure_single_space' => true],
|
||||
'declare_strict_types' => true,
|
||||
'strict_param' => true,
|
||||
'strict_comparison' => true,
|
||||
'phpdoc_align' => ['align' => 'left'],
|
||||
'phpdoc_order' => true,
|
||||
'phpdoc_trim' => true,
|
||||
'phpdoc_scalar' => true,
|
||||
'no_superfluous_phpdoc_tags' => ['remove_inheritdoc' => true],
|
||||
'yoda_style' => false,
|
||||
'no_useless_else' => true,
|
||||
'no_useless_return' => true,
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']],
|
||||
'blank_line_before_statement' => ['statements' => ['return', 'throw', 'try', 'if', 'foreach', 'for', 'while']],
|
||||
])
|
||||
->setFinder($finder);
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM php:8.3-cli-alpine
|
||||
|
||||
RUN apk add --no-cache git unzip
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
WORKDIR /app
|
||||
@@ -0,0 +1,32 @@
|
||||
PHP = docker compose run --rm php
|
||||
|
||||
.PHONY: install test phpstan cs-fix cs-check audit shell build
|
||||
|
||||
build:
|
||||
docker compose build
|
||||
|
||||
install:
|
||||
$(PHP) composer install
|
||||
|
||||
vendor:
|
||||
$(MAKE) install
|
||||
|
||||
test: vendor
|
||||
$(PHP) vendor/bin/phpunit --testsuite Unit --colors=always
|
||||
|
||||
phpstan: vendor
|
||||
$(PHP) vendor/bin/phpstan analyse --no-progress --ansi --memory-limit=512M
|
||||
|
||||
cs-fix: vendor
|
||||
$(PHP) vendor/bin/php-cs-fixer fix --ansi
|
||||
|
||||
cs-check: vendor
|
||||
$(PHP) vendor/bin/php-cs-fixer fix --dry-run --diff --ansi
|
||||
|
||||
audit: vendor
|
||||
$(PHP) composer audit
|
||||
|
||||
ci: audit phpstan cs-check test
|
||||
|
||||
shell:
|
||||
docker compose run --rm php sh
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "tez/utils-enum",
|
||||
"type": "library",
|
||||
"description": "Enum traits and utilities",
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Tez\\Utils\\Enum\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tez\\Utils\\Tests\\Enum\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"jetbrains/phpstorm-attributes": "^1.0",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"cs-fix": "php-cs-fixer fix",
|
||||
"cs-check": "php-cs-fixer fix --dry-run --diff",
|
||||
"phpstan": "phpstan analyse",
|
||||
"test": "phpunit --testsuite Unit --colors=always"
|
||||
}
|
||||
}
|
||||
Generated
+4566
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
php:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- .:/app
|
||||
- composer-cache:/tmp/composer
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
environment:
|
||||
COMPOSER_HOME: /tmp/composer
|
||||
|
||||
volumes:
|
||||
composer-cache:
|
||||
@@ -0,0 +1,8 @@
|
||||
parameters:
|
||||
level: 9
|
||||
paths:
|
||||
- src
|
||||
- tests
|
||||
treatPhpDocTypesAsCertain: false
|
||||
parallel:
|
||||
maximumNumberOfProcesses: 1
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Enum;
|
||||
|
||||
/**
|
||||
* Extends EnumTrait for string- and int-backed enums.
|
||||
* Overrides is() and toArray() to use backing values, and adds
|
||||
* values(), hasValue(), fromName(), tryFromName(), and toValueArray().
|
||||
*
|
||||
* @phpstan-require-implements \BackedEnum
|
||||
*/
|
||||
trait BackedEnumTrait
|
||||
{
|
||||
use EnumTrait;
|
||||
|
||||
/**
|
||||
* Returns true if this case matches the given value by identity or backing value.
|
||||
* Passing a UnitEnum instance only matches by identity.
|
||||
*/
|
||||
public function is(int|string|\UnitEnum|null $value): bool
|
||||
{
|
||||
if ($value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($value === $this) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!($value instanceof \UnitEnum)) {
|
||||
return $this === static::tryFrom($value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return array<int|string, string> */
|
||||
public static function toArray(): array
|
||||
{
|
||||
return array_reduce(
|
||||
static::cases(),
|
||||
static fn($carry, $case) => $carry + [$case->value => $case->name],
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a case by backing value or name, optionally case-insensitive.
|
||||
* Returns null when no match is found or when $value is null.
|
||||
*/
|
||||
public static function find(\UnitEnum|string|int|null $value, bool $ignoreCase = false): ?static
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof static) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
foreach (static::cases() as $case) {
|
||||
if (!($value instanceof \UnitEnum)) {
|
||||
$caseVal = $ignoreCase && is_string($case->value) ? strtoupper($case->value) : $case->value;
|
||||
$searchVal = $ignoreCase && is_string($value) ? strtoupper($value) : $value;
|
||||
|
||||
if ($caseVal === $searchVal) {
|
||||
return $case;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$caseName = $ignoreCase ? strtoupper($case->name) : $case->name;
|
||||
$searchName = $ignoreCase ? strtoupper($value) : $value;
|
||||
|
||||
if ($caseName === $searchName) {
|
||||
return $case;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all backing values of the enum.
|
||||
*
|
||||
* @return list<int|string>
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(static::cases(), 'value');
|
||||
}
|
||||
|
||||
/** Returns true if the given backing value belongs to any case of this enum. */
|
||||
public static function hasValue(int|string $value): bool
|
||||
{
|
||||
return in_array($value, static::values(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the case with the given name, or throws ValueError if not found.
|
||||
* Equivalent to a name-based from() for backed enums.
|
||||
*/
|
||||
public static function fromName(string $name): static
|
||||
{
|
||||
return static::find($name)
|
||||
?? throw new \ValueError("{$name} is not a valid name for " . static::class);
|
||||
}
|
||||
|
||||
/** Returns the case with the given name, or null if not found. */
|
||||
public static function tryFromName(string $name): ?static
|
||||
{
|
||||
return static::find($name);
|
||||
}
|
||||
|
||||
/** @return array<string, int|string> */
|
||||
public static function toValueArray(): array
|
||||
{
|
||||
return array_reduce(
|
||||
static::cases(),
|
||||
static fn($carry, $case) => $carry + [$case->name => $case->value],
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Enum;
|
||||
|
||||
/**
|
||||
* Bitmask helpers for int-backed enums used as flag sets.
|
||||
*
|
||||
* Case values must be powers of 2 (1, 2, 4, 8, …) for correct behaviour.
|
||||
* This constraint is not enforced at runtime.
|
||||
*
|
||||
* @phpstan-require-implements \BackedEnum
|
||||
*
|
||||
* @method static list<static> cases()
|
||||
*/
|
||||
trait BitFlagTrait
|
||||
{
|
||||
/**
|
||||
* Combines the given cases into a single bitmask by OR-ing their values.
|
||||
* Returns 0 when called with no arguments.
|
||||
*/
|
||||
public static function combine(self ...$cases): int
|
||||
{
|
||||
$mask = 0;
|
||||
|
||||
foreach ($cases as $case) {
|
||||
/** @var int $value */
|
||||
$value = $case->value;
|
||||
$mask |= $value;
|
||||
}
|
||||
|
||||
return $mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all cases whose bit is set in the given mask.
|
||||
* Returns an empty array for a mask of 0.
|
||||
*
|
||||
* @return list<static>
|
||||
*/
|
||||
public static function fromBitmask(int $mask): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach (static::cases() as $case) {
|
||||
/** @var int $value */
|
||||
$value = $case->value;
|
||||
|
||||
if ($value !== 0 && ($mask & $value) === $value) {
|
||||
$result[] = $case;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** Returns true if this case's bit is set in the given mask. */
|
||||
public function isSetIn(int $mask): bool
|
||||
{
|
||||
/** @var int $value */
|
||||
$value = $this->value;
|
||||
|
||||
return ($mask & $value) === $value;
|
||||
}
|
||||
|
||||
/** Returns the mask with this case's bit added (set). */
|
||||
public function addTo(int $mask): int
|
||||
{
|
||||
/** @var int $value */
|
||||
$value = $this->value;
|
||||
|
||||
return $mask | $value;
|
||||
}
|
||||
|
||||
/** Returns the mask with this case's bit removed (cleared). */
|
||||
public function removeFrom(int $mask): int
|
||||
{
|
||||
/** @var int $value */
|
||||
$value = $this->value;
|
||||
|
||||
return $mask & ~$value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Enum;
|
||||
|
||||
/**
|
||||
* Comparison helpers for int-backed enums.
|
||||
*
|
||||
* @phpstan-require-implements \BackedEnum
|
||||
*/
|
||||
trait ComparableTrait
|
||||
{
|
||||
/** Returns true if this case's integer value is strictly greater than the other's. */
|
||||
public function isGreaterThan(self $other): bool
|
||||
{
|
||||
/** @var int $a */
|
||||
$a = $this->value;
|
||||
/** @var int $b */
|
||||
$b = $other->value;
|
||||
|
||||
return $a > $b;
|
||||
}
|
||||
|
||||
/** Returns true if this case's integer value is greater than or equal to the other's. */
|
||||
public function isGreaterThanOrEqual(self $other): bool
|
||||
{
|
||||
return $this === $other || $this->isGreaterThan($other);
|
||||
}
|
||||
|
||||
/** Returns true if this case's integer value is strictly less than the other's. */
|
||||
public function isLessThan(self $other): bool
|
||||
{
|
||||
/** @var int $a */
|
||||
$a = $this->value;
|
||||
/** @var int $b */
|
||||
$b = $other->value;
|
||||
|
||||
return $a < $b;
|
||||
}
|
||||
|
||||
/** Returns true if this case's integer value is less than or equal to the other's. */
|
||||
public function isLessThanOrEqual(self $other): bool
|
||||
{
|
||||
return $this === $other || $this->isLessThan($other);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when this case is between $min and $max (inclusive).
|
||||
*/
|
||||
public function between(self $min, self $max): bool
|
||||
{
|
||||
return $this->isGreaterThanOrEqual($min) && $this->isLessThanOrEqual($max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this case clamped to [$min, $max].
|
||||
* If this value is below $min, $min is returned.
|
||||
* If this value is above $max, $max is returned.
|
||||
*/
|
||||
public function clamp(self $min, self $max): static
|
||||
{
|
||||
if ($this->isLessThan($min)) {
|
||||
return $min;
|
||||
}
|
||||
|
||||
if ($this->isGreaterThan($max)) {
|
||||
return $max;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Enum;
|
||||
|
||||
/**
|
||||
* Add-on trait providing collection-style selection helpers for any enum.
|
||||
* Can be combined with EnumTrait or BackedEnumTrait.
|
||||
*
|
||||
* @phpstan-require-implements \UnitEnum
|
||||
*
|
||||
* @method static list<static> cases()
|
||||
*/
|
||||
trait EnumCollectionTrait
|
||||
{
|
||||
/** Returns a random case. */
|
||||
public static function random(): static
|
||||
{
|
||||
$cases = static::cases();
|
||||
|
||||
return $cases[array_rand($cases)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all cases except the given ones.
|
||||
*
|
||||
* @return list<static>
|
||||
*/
|
||||
public static function except(self ...$excluded): array
|
||||
{
|
||||
return array_values(
|
||||
array_filter(
|
||||
static::cases(),
|
||||
static fn(self $case): bool => !in_array($case, $excluded, true),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the given cases (preserves order of the enum definition).
|
||||
*
|
||||
* @return list<static>
|
||||
*/
|
||||
public static function only(self ...$included): array
|
||||
{
|
||||
return array_values(
|
||||
array_filter(
|
||||
static::cases(),
|
||||
static fn(self $case): bool => in_array($case, $included, true),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Enum;
|
||||
|
||||
/**
|
||||
* Base trait for pure (unit) enums.
|
||||
* Provides identity checks, lookup, filtering, and ordering helpers.
|
||||
* Use BackedEnumTrait instead when your enum has backing values.
|
||||
*
|
||||
* @phpstan-require-implements \UnitEnum
|
||||
*
|
||||
* @method static list<static> cases()
|
||||
*/
|
||||
trait EnumTrait
|
||||
{
|
||||
/**
|
||||
* Returns true if this case matches the given value by identity or name.
|
||||
* Null always returns false.
|
||||
*/
|
||||
public function is(int|string|\UnitEnum|null $value): bool
|
||||
{
|
||||
if ($value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($value === $this) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return $this->name === $value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
public static function toArray(): array
|
||||
{
|
||||
return array_reduce(
|
||||
static::cases(),
|
||||
static fn($carry, $case) => $carry + [$case->name => $case->name],
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this case matches any value in the given list.
|
||||
*
|
||||
* @param int[]|string[]|\UnitEnum[]|null $values
|
||||
*/
|
||||
public function isOneOf(?array $values): bool
|
||||
{
|
||||
foreach ($values ?? [] as $value) {
|
||||
if ($this->is($value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this case does not match any value in the given list.
|
||||
*
|
||||
* @param int[]|string[]|\UnitEnum[]|null $values
|
||||
*/
|
||||
public function isNoneOf(?array $values): bool
|
||||
{
|
||||
return !$this->isOneOf($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a case by identity, name, or (for BackedEnums) backing value.
|
||||
* Returns null when no match is found or when $value is null.
|
||||
* Set $ignoreCase to true for case-insensitive name and string-value matching.
|
||||
*/
|
||||
public static function find(\UnitEnum|string|int|null $value, bool $ignoreCase = false): ?static
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof static) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
foreach (self::cases() as $case) {
|
||||
if ($case instanceof \BackedEnum && !($value instanceof \UnitEnum)) {
|
||||
$caseVal = $ignoreCase && is_string($case->value) ? strtoupper($case->value) : $case->value;
|
||||
$searchVal = $ignoreCase && is_string($value) ? strtoupper($value) : $value;
|
||||
|
||||
if ($caseVal === $searchVal) {
|
||||
return $case;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$caseName = $ignoreCase ? strtoupper($case->name) : $case->name;
|
||||
$searchName = $ignoreCase ? strtoupper($value) : $value;
|
||||
|
||||
if ($caseName === $searchName) {
|
||||
return $case;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all cases for which the callback returns true.
|
||||
*
|
||||
* @return list<static>
|
||||
*/
|
||||
public static function filter(callable $callback): array
|
||||
{
|
||||
return array_values(array_filter(static::cases(), $callback));
|
||||
}
|
||||
|
||||
/** Returns the zero-based position of this case in the enum definition. */
|
||||
public function ordinal(): int
|
||||
{
|
||||
$index = array_search($this, static::cases(), true);
|
||||
assert($index !== false);
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/** Returns the first defined case. */
|
||||
public static function first(): static
|
||||
{
|
||||
return static::cases()[0];
|
||||
}
|
||||
|
||||
/** Returns the last defined case. */
|
||||
public static function last(): static
|
||||
{
|
||||
$cases = static::cases();
|
||||
|
||||
return $cases[count($cases) - 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Enum;
|
||||
|
||||
/**
|
||||
* Navigation helpers for stepping to the adjacent case by definition order.
|
||||
* Designed for state machines and ordered workflows.
|
||||
* Compatible with both pure (UnitEnum) and backed enums.
|
||||
*
|
||||
* Order is determined by the enum definition order (ordinal), not by backing value.
|
||||
* Returns null at the boundaries — no wrap-around.
|
||||
*
|
||||
* @phpstan-require-implements \UnitEnum
|
||||
*
|
||||
* @method static list<static> cases()
|
||||
*/
|
||||
trait StepTrait
|
||||
{
|
||||
/** Returns the next case by definition order, or null if already the last. */
|
||||
public function next(): ?static
|
||||
{
|
||||
$cases = static::cases();
|
||||
$index = array_search($this, $cases, true);
|
||||
assert($index !== false);
|
||||
|
||||
return $cases[$index + 1] ?? null;
|
||||
}
|
||||
|
||||
/** Returns the previous case by definition order, or null if already the first. */
|
||||
public function prev(): ?static
|
||||
{
|
||||
$cases = static::cases();
|
||||
$index = array_search($this, $cases, true);
|
||||
assert($index !== false);
|
||||
|
||||
return $index > 0 ? $cases[$index - 1] : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Enum;
|
||||
|
||||
/**
|
||||
* Provides human-readable labels for enum cases without any framework dependency.
|
||||
*
|
||||
* Override {@see labels()} in the enum to provide custom label mappings.
|
||||
* Falls back to the case name when no label is defined.
|
||||
*
|
||||
* @phpstan-require-implements \UnitEnum
|
||||
*
|
||||
* @method static list<static> cases()
|
||||
*/
|
||||
trait TranslatableTrait
|
||||
{
|
||||
/**
|
||||
* Override this method in the enum to provide custom label mappings.
|
||||
* Keys must be case names (e.g. self::Active->name).
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable label for this case.
|
||||
* Falls back to the case name if no label is configured.
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return static::labels()[$this->name] ?? $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all cases mapped to their labels.
|
||||
*
|
||||
* @return array<string, string> [caseName => label]
|
||||
*/
|
||||
public static function labelsArray(): array
|
||||
{
|
||||
$labels = [];
|
||||
|
||||
foreach (static::cases() as $case) {
|
||||
$labels[$case->name] = $case->label();
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Enum;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Enum\BackedEnumTrait;
|
||||
|
||||
// String-backed enum
|
||||
enum Color: string
|
||||
{
|
||||
use BackedEnumTrait;
|
||||
|
||||
case Red = 'red';
|
||||
case Green = 'green';
|
||||
case Blue = 'blue';
|
||||
}
|
||||
|
||||
// Integer-backed enum
|
||||
enum Priority: int
|
||||
{
|
||||
use BackedEnumTrait;
|
||||
|
||||
case Low = 1;
|
||||
case Medium = 2;
|
||||
case High = 3;
|
||||
}
|
||||
|
||||
final class BackedEnumTraitTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// values()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testValuesReturnsAllStringValues(): void
|
||||
{
|
||||
self::assertSame(['red', 'green', 'blue'], Color::values());
|
||||
}
|
||||
|
||||
public function testValuesReturnsAllIntValues(): void
|
||||
{
|
||||
self::assertSame([1, 2, 3], Priority::values());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// hasValue()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testHasValueReturnsTrueForExistingValue(): void
|
||||
{
|
||||
self::assertTrue(Color::hasValue('green'));
|
||||
self::assertTrue(Priority::hasValue(2));
|
||||
}
|
||||
|
||||
public function testHasValueReturnsFalseForUnknownValue(): void
|
||||
{
|
||||
self::assertFalse(Color::hasValue('yellow'));
|
||||
self::assertFalse(Priority::hasValue(99));
|
||||
}
|
||||
|
||||
public function testHasValueIsStrictlyTyped(): void
|
||||
{
|
||||
// integer 1 must not match string '1'
|
||||
self::assertFalse(Color::hasValue(1));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// fromName()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testFromNameReturnsCorrectCase(): void
|
||||
{
|
||||
self::assertSame(Color::Blue, Color::fromName('Blue'));
|
||||
self::assertSame(Priority::High, Priority::fromName('High'));
|
||||
}
|
||||
|
||||
public function testFromNameThrowsValueErrorForUnknownName(): void
|
||||
{
|
||||
$this->expectException(\ValueError::class);
|
||||
Color::fromName('Purple');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// tryFromName()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testTryFromNameReturnsCorrectCase(): void
|
||||
{
|
||||
self::assertSame(Color::Red, Color::tryFromName('Red'));
|
||||
}
|
||||
|
||||
public function testTryFromNameReturnsNullForUnknownName(): void
|
||||
{
|
||||
self::assertNull(Color::tryFromName('Magenta'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// toArray() — inherited via EnumTrait, but backed path tested here
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testToArrayReturnsValueToNameMapping(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['red' => 'Red', 'green' => 'Green', 'blue' => 'Blue'],
|
||||
Color::toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testToArrayForIntBackedEnum(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[1 => 'Low', 2 => 'Medium', 3 => 'High'],
|
||||
Priority::toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// is() with scalar (backed enum path)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testIsReturnsTrueForMatchingValue(): void
|
||||
{
|
||||
self::assertTrue(Color::Green->is('green'));
|
||||
}
|
||||
|
||||
public function testIsReturnsFalseForOtherValue(): void
|
||||
{
|
||||
self::assertFalse(Color::Green->is('red'));
|
||||
}
|
||||
|
||||
public function testIsReturnsTrueForIntValue(): void
|
||||
{
|
||||
self::assertTrue(Priority::Medium->is(2));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// find() — backed enum paths (value + case-insensitive)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testFindByValue(): void
|
||||
{
|
||||
self::assertSame(Color::Blue, Color::find('blue'));
|
||||
}
|
||||
|
||||
public function testFindByValueCaseInsensitive(): void
|
||||
{
|
||||
self::assertSame(Color::Red, Color::find('RED', ignoreCase: true));
|
||||
}
|
||||
|
||||
public function testFindByNameCaseInsensitive(): void
|
||||
{
|
||||
self::assertSame(Color::Green, Color::find('green', ignoreCase: true));
|
||||
}
|
||||
|
||||
public function testFindReturnsNullForUnknown(): void
|
||||
{
|
||||
self::assertNull(Color::find('violet'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// toValueArray()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testToValueArrayReturnsNameToValueMapping(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['Red' => 'red', 'Green' => 'green', 'Blue' => 'blue'],
|
||||
Color::toValueArray(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testToValueArrayForIntBackedEnum(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['Low' => 1, 'Medium' => 2, 'High' => 3],
|
||||
Priority::toValueArray(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Enum;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Enum\BackedEnumTrait;
|
||||
use Tez\Utils\Enum\BitFlagTrait;
|
||||
|
||||
enum Permission: int
|
||||
{
|
||||
use BackedEnumTrait;
|
||||
use BitFlagTrait;
|
||||
|
||||
case Read = 1;
|
||||
case Write = 2;
|
||||
case Delete = 4;
|
||||
case Admin = 8;
|
||||
}
|
||||
|
||||
final class BitFlagTraitTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// combine()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testCombineWithNoArgumentsReturnsZero(): void
|
||||
{
|
||||
self::assertSame(0, Permission::combine());
|
||||
}
|
||||
|
||||
public function testCombineWithSingleCase(): void
|
||||
{
|
||||
self::assertSame(1, Permission::combine(Permission::Read));
|
||||
}
|
||||
|
||||
public function testCombineWithTwoCases(): void
|
||||
{
|
||||
self::assertSame(3, Permission::combine(Permission::Read, Permission::Write));
|
||||
}
|
||||
|
||||
public function testCombineWithAllCases(): void
|
||||
{
|
||||
self::assertSame(15, Permission::combine(
|
||||
Permission::Read,
|
||||
Permission::Write,
|
||||
Permission::Delete,
|
||||
Permission::Admin,
|
||||
));
|
||||
}
|
||||
|
||||
public function testCombineIsDeduplicated(): void
|
||||
{
|
||||
self::assertSame(1, Permission::combine(Permission::Read, Permission::Read));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// fromBitmask()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testFromBitmaskWithZeroReturnsEmptyArray(): void
|
||||
{
|
||||
self::assertSame([], Permission::fromBitmask(0));
|
||||
}
|
||||
|
||||
public function testFromBitmaskWithSingleBitReturnsSingleCase(): void
|
||||
{
|
||||
self::assertSame([Permission::Read], Permission::fromBitmask(1));
|
||||
}
|
||||
|
||||
public function testFromBitmaskWithCombinedMaskReturnsMatchingCases(): void
|
||||
{
|
||||
$mask = Permission::combine(Permission::Read, Permission::Write);
|
||||
|
||||
self::assertSame([Permission::Read, Permission::Write], Permission::fromBitmask($mask));
|
||||
}
|
||||
|
||||
public function testFromBitmaskWithAllBitsReturnsAllCases(): void
|
||||
{
|
||||
$mask = Permission::combine(
|
||||
Permission::Read,
|
||||
Permission::Write,
|
||||
Permission::Delete,
|
||||
Permission::Admin,
|
||||
);
|
||||
|
||||
self::assertSame(
|
||||
[Permission::Read, Permission::Write, Permission::Delete, Permission::Admin],
|
||||
Permission::fromBitmask($mask),
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// isSetIn()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testIsSetInReturnsTrueWhenBitIsPresent(): void
|
||||
{
|
||||
$mask = Permission::combine(Permission::Read, Permission::Write);
|
||||
|
||||
self::assertTrue(Permission::Read->isSetIn($mask));
|
||||
self::assertTrue(Permission::Write->isSetIn($mask));
|
||||
}
|
||||
|
||||
public function testIsSetInReturnsFalseWhenBitIsAbsent(): void
|
||||
{
|
||||
$mask = Permission::combine(Permission::Read, Permission::Write);
|
||||
|
||||
self::assertFalse(Permission::Delete->isSetIn($mask));
|
||||
self::assertFalse(Permission::Admin->isSetIn($mask));
|
||||
}
|
||||
|
||||
public function testIsSetInReturnsFalseForZeroMask(): void
|
||||
{
|
||||
self::assertFalse(Permission::Read->isSetIn(0));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// addTo()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testAddToSetsBit(): void
|
||||
{
|
||||
$mask = Permission::combine(Permission::Read);
|
||||
$mask = Permission::Write->addTo($mask);
|
||||
|
||||
self::assertSame(3, $mask);
|
||||
self::assertTrue(Permission::Write->isSetIn($mask));
|
||||
}
|
||||
|
||||
public function testAddToIsIdempotent(): void
|
||||
{
|
||||
$mask = Permission::combine(Permission::Read);
|
||||
$mask = Permission::Read->addTo($mask);
|
||||
|
||||
self::assertSame(1, $mask);
|
||||
}
|
||||
|
||||
public function testAddToOnEmptyMask(): void
|
||||
{
|
||||
self::assertSame(4, Permission::Delete->addTo(0));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// removeFrom()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testRemoveFromClearsBit(): void
|
||||
{
|
||||
$mask = Permission::combine(Permission::Read, Permission::Write);
|
||||
$mask = Permission::Write->removeFrom($mask);
|
||||
|
||||
self::assertSame(1, $mask);
|
||||
self::assertFalse(Permission::Write->isSetIn($mask));
|
||||
}
|
||||
|
||||
public function testRemoveFromIsIdempotent(): void
|
||||
{
|
||||
$mask = Permission::combine(Permission::Read);
|
||||
$mask = Permission::Write->removeFrom($mask);
|
||||
|
||||
self::assertSame(1, $mask);
|
||||
}
|
||||
|
||||
public function testRemoveFromOnZeroMaskReturnsZero(): void
|
||||
{
|
||||
self::assertSame(0, Permission::Read->removeFrom(0));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Workflow: add then remove
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testAddThenRemoveRestoresOriginalMask(): void
|
||||
{
|
||||
$original = Permission::combine(Permission::Read, Permission::Delete);
|
||||
$modified = Permission::Write->addTo($original);
|
||||
$restored = Permission::Write->removeFrom($modified);
|
||||
|
||||
self::assertSame($original, $restored);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Enum;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Enum\BackedEnumTrait;
|
||||
use Tez\Utils\Enum\ComparableTrait;
|
||||
|
||||
enum Level: int
|
||||
{
|
||||
use BackedEnumTrait;
|
||||
use ComparableTrait;
|
||||
|
||||
case Low = 1;
|
||||
case Medium = 2;
|
||||
case High = 3;
|
||||
case Ultra = 4;
|
||||
}
|
||||
|
||||
final class ComparableTraitTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// isGreaterThan()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testIsGreaterThanReturnsTrueWhenHigher(): void
|
||||
{
|
||||
self::assertTrue(Level::High->isGreaterThan(Level::Low));
|
||||
}
|
||||
|
||||
public function testIsGreaterThanReturnsFalseWhenEqual(): void
|
||||
{
|
||||
self::assertFalse(Level::Medium->isGreaterThan(Level::Medium));
|
||||
}
|
||||
|
||||
public function testIsGreaterThanReturnsFalseWhenLower(): void
|
||||
{
|
||||
self::assertFalse(Level::Low->isGreaterThan(Level::High));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// isGreaterThanOrEqual()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testIsGreaterThanOrEqualReturnsTrueWhenHigher(): void
|
||||
{
|
||||
self::assertTrue(Level::High->isGreaterThanOrEqual(Level::Low));
|
||||
}
|
||||
|
||||
public function testIsGreaterThanOrEqualReturnsTrueWhenEqual(): void
|
||||
{
|
||||
self::assertTrue(Level::Medium->isGreaterThanOrEqual(Level::Medium));
|
||||
}
|
||||
|
||||
public function testIsGreaterThanOrEqualReturnsFalseWhenLower(): void
|
||||
{
|
||||
self::assertFalse(Level::Low->isGreaterThanOrEqual(Level::High));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// isLessThan()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testIsLessThanReturnsTrueWhenLower(): void
|
||||
{
|
||||
self::assertTrue(Level::Low->isLessThan(Level::High));
|
||||
}
|
||||
|
||||
public function testIsLessThanReturnsFalseWhenEqual(): void
|
||||
{
|
||||
self::assertFalse(Level::Medium->isLessThan(Level::Medium));
|
||||
}
|
||||
|
||||
public function testIsLessThanReturnsFalseWhenHigher(): void
|
||||
{
|
||||
self::assertFalse(Level::High->isLessThan(Level::Low));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// isLessThanOrEqual()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testIsLessThanOrEqualReturnsTrueWhenLower(): void
|
||||
{
|
||||
self::assertTrue(Level::Low->isLessThanOrEqual(Level::High));
|
||||
}
|
||||
|
||||
public function testIsLessThanOrEqualReturnsTrueWhenEqual(): void
|
||||
{
|
||||
self::assertTrue(Level::Medium->isLessThanOrEqual(Level::Medium));
|
||||
}
|
||||
|
||||
public function testIsLessThanOrEqualReturnsFalseWhenHigher(): void
|
||||
{
|
||||
self::assertFalse(Level::High->isLessThanOrEqual(Level::Low));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// between()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testBetweenReturnsTrueWhenWithinRange(): void
|
||||
{
|
||||
self::assertTrue(Level::Medium->between(Level::Low, Level::High));
|
||||
}
|
||||
|
||||
public function testBetweenReturnsTrueAtLowerBound(): void
|
||||
{
|
||||
self::assertTrue(Level::Low->between(Level::Low, Level::High));
|
||||
}
|
||||
|
||||
public function testBetweenReturnsTrueAtUpperBound(): void
|
||||
{
|
||||
self::assertTrue(Level::High->between(Level::Low, Level::High));
|
||||
}
|
||||
|
||||
public function testBetweenReturnsFalseWhenBelowRange(): void
|
||||
{
|
||||
self::assertFalse(Level::Low->between(Level::Medium, Level::Ultra));
|
||||
}
|
||||
|
||||
public function testBetweenReturnsFalseWhenAboveRange(): void
|
||||
{
|
||||
self::assertFalse(Level::Ultra->between(Level::Low, Level::High));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// clamp()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testClampReturnsValueWhenWithinRange(): void
|
||||
{
|
||||
self::assertSame(Level::Medium, Level::Medium->clamp(Level::Low, Level::High));
|
||||
}
|
||||
|
||||
public function testClampReturnsMinWhenBelowRange(): void
|
||||
{
|
||||
self::assertSame(Level::Medium, Level::Low->clamp(Level::Medium, Level::Ultra));
|
||||
}
|
||||
|
||||
public function testClampReturnsMaxWhenAboveRange(): void
|
||||
{
|
||||
self::assertSame(Level::High, Level::Ultra->clamp(Level::Low, Level::High));
|
||||
}
|
||||
|
||||
public function testClampReturnsMinWhenEqualToMin(): void
|
||||
{
|
||||
self::assertSame(Level::Low, Level::Low->clamp(Level::Low, Level::High));
|
||||
}
|
||||
|
||||
public function testClampReturnsMaxWhenEqualToMax(): void
|
||||
{
|
||||
self::assertSame(Level::High, Level::High->clamp(Level::Low, Level::High));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Enum;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Enum\EnumCollectionTrait;
|
||||
|
||||
enum Suit: string
|
||||
{
|
||||
use EnumCollectionTrait;
|
||||
|
||||
case Hearts = 'H';
|
||||
case Diamonds = 'D';
|
||||
case Clubs = 'C';
|
||||
case Spades = 'S';
|
||||
}
|
||||
|
||||
enum Compass
|
||||
{
|
||||
use EnumCollectionTrait;
|
||||
|
||||
case North;
|
||||
case East;
|
||||
case South;
|
||||
case West;
|
||||
}
|
||||
|
||||
final class EnumCollectionTraitTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// random()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testRandomReturnsACaseInstance(): void
|
||||
{
|
||||
$case = Suit::random();
|
||||
|
||||
self::assertInstanceOf(Suit::class, $case);
|
||||
}
|
||||
|
||||
public function testRandomEventuallyReturnsAllCases(): void
|
||||
{
|
||||
$seen = [];
|
||||
|
||||
for ($i = 0; $i < 200; $i++) {
|
||||
$seen[Suit::random()->name] = true;
|
||||
}
|
||||
|
||||
self::assertCount(4, $seen, 'Expected all 4 suits to appear within 200 draws');
|
||||
}
|
||||
|
||||
public function testRandomWorksForPureEnum(): void
|
||||
{
|
||||
$case = Compass::random();
|
||||
|
||||
self::assertInstanceOf(Compass::class, $case);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// except()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testExceptRemovesGivenCases(): void
|
||||
{
|
||||
$result = Suit::except(Suit::Hearts, Suit::Spades);
|
||||
|
||||
self::assertSame([Suit::Diamonds, Suit::Clubs], $result);
|
||||
}
|
||||
|
||||
public function testExceptWithNoCasesReturnsAll(): void
|
||||
{
|
||||
self::assertSame(Suit::cases(), Suit::except());
|
||||
}
|
||||
|
||||
public function testExceptWithAllCasesReturnsEmpty(): void
|
||||
{
|
||||
$result = Suit::except(...Suit::cases());
|
||||
|
||||
self::assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testExceptPreservesEnumOrder(): void
|
||||
{
|
||||
$result = Suit::except(Suit::Diamonds);
|
||||
|
||||
self::assertSame([Suit::Hearts, Suit::Clubs, Suit::Spades], $result);
|
||||
}
|
||||
|
||||
public function testExceptWorksForPureEnum(): void
|
||||
{
|
||||
$result = Compass::except(Compass::North, Compass::South);
|
||||
|
||||
self::assertSame([Compass::East, Compass::West], $result);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// only()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testOnlyReturnsRequestedCases(): void
|
||||
{
|
||||
$result = Suit::only(Suit::Hearts, Suit::Spades);
|
||||
|
||||
self::assertSame([Suit::Hearts, Suit::Spades], $result);
|
||||
}
|
||||
|
||||
public function testOnlyPreservesEnumDefinitionOrder(): void
|
||||
{
|
||||
// Pass in reverse order — result must follow enum definition order
|
||||
$result = Suit::only(Suit::Spades, Suit::Hearts);
|
||||
|
||||
self::assertSame([Suit::Hearts, Suit::Spades], $result);
|
||||
}
|
||||
|
||||
public function testOnlyWithNoCasesReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], Suit::only());
|
||||
}
|
||||
|
||||
public function testOnlyWithAllCasesReturnsAll(): void
|
||||
{
|
||||
$result = Suit::only(...Suit::cases());
|
||||
|
||||
self::assertSame(Suit::cases(), $result);
|
||||
}
|
||||
|
||||
public function testOnlyWorksForPureEnum(): void
|
||||
{
|
||||
$result = Compass::only(Compass::East, Compass::West);
|
||||
|
||||
self::assertSame([Compass::East, Compass::West], $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Enum;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Enum\BackedEnumTrait;
|
||||
use Tez\Utils\Enum\EnumTrait;
|
||||
|
||||
// Pure (unit) enum used across all EnumTrait tests
|
||||
enum Season
|
||||
{
|
||||
use EnumTrait;
|
||||
|
||||
case Spring;
|
||||
case Summer;
|
||||
case Autumn;
|
||||
case Winter;
|
||||
}
|
||||
|
||||
// Backed enum to verify backed paths of EnumTrait (toArray, is, find)
|
||||
enum Direction: string
|
||||
{
|
||||
use BackedEnumTrait;
|
||||
|
||||
case North = 'north';
|
||||
case South = 'south';
|
||||
case East = 'east';
|
||||
case West = 'west';
|
||||
}
|
||||
|
||||
final class EnumTraitTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// is()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testIsReturnsTrueForSameInstance(): void
|
||||
{
|
||||
self::assertTrue(Season::Summer->is(Season::Summer));
|
||||
}
|
||||
|
||||
public function testIsReturnsFalseForNull(): void
|
||||
{
|
||||
self::assertFalse(Season::Summer->is(null));
|
||||
}
|
||||
|
||||
public function testIsReturnsFalseForDifferentCase(): void
|
||||
{
|
||||
self::assertFalse(Season::Summer->is(Season::Winter));
|
||||
}
|
||||
|
||||
public function testIsReturnsTrueForMatchingStringName(): void
|
||||
{
|
||||
self::assertTrue(Season::Autumn->is('Autumn'));
|
||||
}
|
||||
|
||||
public function testIsReturnsFalseForNonMatchingStringName(): void
|
||||
{
|
||||
self::assertFalse(Season::Autumn->is('Spring'));
|
||||
}
|
||||
|
||||
public function testIsReturnsTrueForBackedEnumByValue(): void
|
||||
{
|
||||
self::assertTrue(Direction::North->is('north'));
|
||||
}
|
||||
|
||||
public function testIsReturnsFalseForBackedEnumWithWrongValue(): void
|
||||
{
|
||||
self::assertFalse(Direction::North->is('south'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// isOneOf() / isNoneOf()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testIsOneOfReturnsTrueWhenMatchFound(): void
|
||||
{
|
||||
self::assertTrue(Season::Winter->isOneOf([Season::Autumn, Season::Winter]));
|
||||
}
|
||||
|
||||
public function testIsOneOfReturnsFalseWhenNoMatch(): void
|
||||
{
|
||||
self::assertFalse(Season::Spring->isOneOf([Season::Autumn, Season::Winter]));
|
||||
}
|
||||
|
||||
public function testIsOneOfReturnsFalseForNull(): void
|
||||
{
|
||||
self::assertFalse(Season::Spring->isOneOf(null));
|
||||
}
|
||||
|
||||
public function testIsNoneOfIsInverseOfIsOneOf(): void
|
||||
{
|
||||
self::assertTrue(Season::Spring->isNoneOf([Season::Autumn, Season::Winter]));
|
||||
self::assertFalse(Season::Winter->isNoneOf([Season::Autumn, Season::Winter]));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// toArray()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testToArrayForPureEnumReturnsNameToNameMapping(): void
|
||||
{
|
||||
$expected = [
|
||||
'Spring' => 'Spring',
|
||||
'Summer' => 'Summer',
|
||||
'Autumn' => 'Autumn',
|
||||
'Winter' => 'Winter',
|
||||
];
|
||||
|
||||
self::assertSame($expected, Season::toArray());
|
||||
}
|
||||
|
||||
public function testToArrayForBackedEnumReturnsValueToNameMapping(): void
|
||||
{
|
||||
$expected = [
|
||||
'north' => 'North',
|
||||
'south' => 'South',
|
||||
'east' => 'East',
|
||||
'west' => 'West',
|
||||
];
|
||||
|
||||
self::assertSame($expected, Direction::toArray());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// find()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testFindReturnsNullForNull(): void
|
||||
{
|
||||
self::assertNull(Season::find(null));
|
||||
}
|
||||
|
||||
public function testFindReturnsEnumInstanceWhenPassedItself(): void
|
||||
{
|
||||
self::assertSame(Season::Spring, Season::find(Season::Spring));
|
||||
}
|
||||
|
||||
public function testFindByNameForPureEnum(): void
|
||||
{
|
||||
self::assertSame(Season::Summer, Season::find('Summer'));
|
||||
}
|
||||
|
||||
public function testFindReturnsNullForUnknownName(): void
|
||||
{
|
||||
self::assertNull(Season::find('Unknown'));
|
||||
}
|
||||
|
||||
public function testFindIgnoreCasePureEnum(): void
|
||||
{
|
||||
self::assertSame(Season::Winter, Season::find('winter', ignoreCase: true));
|
||||
}
|
||||
|
||||
public function testFindByValueForBackedEnum(): void
|
||||
{
|
||||
self::assertSame(Direction::East, Direction::find('east'));
|
||||
}
|
||||
|
||||
public function testFindIgnoreCaseBackedEnum(): void
|
||||
{
|
||||
self::assertSame(Direction::West, Direction::find('WEST', ignoreCase: true));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// filter()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testFilterReturnsCasesMatchingCallback(): void
|
||||
{
|
||||
$result = Season::filter(fn(Season $c) => in_array($c, [Season::Spring, Season::Summer], true));
|
||||
|
||||
self::assertCount(2, $result);
|
||||
self::assertContains(Season::Spring, $result);
|
||||
self::assertContains(Season::Summer, $result);
|
||||
}
|
||||
|
||||
public function testFilterReturnsEmptyArrayWhenNothingMatches(): void
|
||||
{
|
||||
$result = Season::filter(fn() => false);
|
||||
|
||||
self::assertSame([], $result);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ordinal()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testOrdinalReturnsZeroBasedPosition(): void
|
||||
{
|
||||
self::assertSame(0, Season::Spring->ordinal());
|
||||
self::assertSame(1, Season::Summer->ordinal());
|
||||
self::assertSame(2, Season::Autumn->ordinal());
|
||||
self::assertSame(3, Season::Winter->ordinal());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// first() / last()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testFirstReturnsFirstCase(): void
|
||||
{
|
||||
self::assertSame(Season::Spring, Season::first());
|
||||
}
|
||||
|
||||
public function testLastReturnsLastCase(): void
|
||||
{
|
||||
self::assertSame(Season::Winter, Season::last());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Enum;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Enum\EnumTrait;
|
||||
use Tez\Utils\Enum\StepTrait;
|
||||
|
||||
enum OrderStatus
|
||||
{
|
||||
use EnumTrait;
|
||||
use StepTrait;
|
||||
|
||||
case Draft;
|
||||
case Pending;
|
||||
case Shipped;
|
||||
case Delivered;
|
||||
case Cancelled;
|
||||
}
|
||||
|
||||
final class StepTraitTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// next()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testNextReturnsFollowingCase(): void
|
||||
{
|
||||
self::assertSame(OrderStatus::Shipped, OrderStatus::Pending->next());
|
||||
}
|
||||
|
||||
public function testNextFromFirstReturnsSecond(): void
|
||||
{
|
||||
self::assertSame(OrderStatus::Pending, OrderStatus::Draft->next());
|
||||
}
|
||||
|
||||
public function testNextFromLastReturnsNull(): void
|
||||
{
|
||||
self::assertNull(OrderStatus::Cancelled->next());
|
||||
}
|
||||
|
||||
public function testNextFromSecondToLastReturnsLast(): void
|
||||
{
|
||||
self::assertSame(OrderStatus::Cancelled, OrderStatus::Delivered->next());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// prev()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testPrevReturnsPrecedingCase(): void
|
||||
{
|
||||
self::assertSame(OrderStatus::Draft, OrderStatus::Pending->prev());
|
||||
}
|
||||
|
||||
public function testPrevFromLastReturnsPenultimate(): void
|
||||
{
|
||||
self::assertSame(OrderStatus::Delivered, OrderStatus::Cancelled->prev());
|
||||
}
|
||||
|
||||
public function testPrevFromFirstReturnsNull(): void
|
||||
{
|
||||
self::assertNull(OrderStatus::Draft->prev());
|
||||
}
|
||||
|
||||
public function testPrevFromSecondReturnsFirst(): void
|
||||
{
|
||||
self::assertSame(OrderStatus::Draft, OrderStatus::Pending->prev());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Chaining
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testNextThenPrevReturnsSameCase(): void
|
||||
{
|
||||
$case = OrderStatus::Pending;
|
||||
|
||||
self::assertSame($case, $case->next()?->prev());
|
||||
}
|
||||
|
||||
public function testPrevThenNextReturnsSameCase(): void
|
||||
{
|
||||
$case = OrderStatus::Shipped;
|
||||
|
||||
self::assertSame($case, $case->prev()?->next());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Single-case enum edge case
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testNextAndPrevReturnNullForOnlyCase(): void
|
||||
{
|
||||
self::assertNull(Alone::Only->next());
|
||||
self::assertNull(Alone::Only->prev());
|
||||
}
|
||||
}
|
||||
|
||||
enum Alone
|
||||
{
|
||||
use StepTrait;
|
||||
|
||||
case Only;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Enum;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Enum\BackedEnumTrait;
|
||||
use Tez\Utils\Enum\TranslatableTrait;
|
||||
|
||||
enum StatusWithLabels: string
|
||||
{
|
||||
use BackedEnumTrait;
|
||||
use TranslatableTrait;
|
||||
|
||||
case Active = 'active';
|
||||
case Pending = 'pending';
|
||||
case Archived = 'archived';
|
||||
|
||||
/** @return array<string, string> */
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
self::Active->name => 'Aktiv',
|
||||
self::Pending->name => 'Ausstehend',
|
||||
// Archived intentionally has no label → fallback to name
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
enum PureWithLabels
|
||||
{
|
||||
use TranslatableTrait;
|
||||
|
||||
case Open;
|
||||
case Closed;
|
||||
|
||||
/** @return array<string, string> */
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
self::Open->name => 'Offen',
|
||||
self::Closed->name => 'Geschlossen',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
enum NoLabels: int
|
||||
{
|
||||
use TranslatableTrait;
|
||||
|
||||
case One = 1;
|
||||
case Two = 2;
|
||||
}
|
||||
|
||||
final class TranslatableTraitTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// label()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testLabelReturnsConfiguredString(): void
|
||||
{
|
||||
self::assertSame('Aktiv', StatusWithLabels::Active->label());
|
||||
self::assertSame('Ausstehend', StatusWithLabels::Pending->label());
|
||||
}
|
||||
|
||||
public function testLabelFallsBackToCaseNameWhenNotConfigured(): void
|
||||
{
|
||||
self::assertSame('Archived', StatusWithLabels::Archived->label());
|
||||
}
|
||||
|
||||
public function testLabelWorksForPureEnum(): void
|
||||
{
|
||||
self::assertSame('Offen', PureWithLabels::Open->label());
|
||||
self::assertSame('Geschlossen', PureWithLabels::Closed->label());
|
||||
}
|
||||
|
||||
public function testLabelFallsBackWhenNoLabelsMethodOverridden(): void
|
||||
{
|
||||
self::assertSame('One', NoLabels::One->label());
|
||||
self::assertSame('Two', NoLabels::Two->label());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// labelsArray()
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testLabelsArrayReturnsAllCasesMappedToLabels(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
'Active' => 'Aktiv',
|
||||
'Pending' => 'Ausstehend',
|
||||
'Archived' => 'Archived',
|
||||
],
|
||||
StatusWithLabels::labelsArray(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testLabelsArrayWorksForPureEnum(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
'Open' => 'Offen',
|
||||
'Closed' => 'Geschlossen',
|
||||
],
|
||||
PureWithLabels::labelsArray(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testLabelsArrayFallsBackToNamesWhenEmpty(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
'One' => 'One',
|
||||
'Two' => 'Two',
|
||||
],
|
||||
NoLabels::labelsArray(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user