mirror of
https://github.com/archtechx/enums.git
synced 2025-12-12 09:44:03 +00:00
Comparable enum (#20)
* feat: comparable enum * test: comparable enum * ci: php-cs-fixer in repository scope * chore: add `Comparable` usage in README * ci: globally use `php-cs-fixer` * improve Comparable logic * test more PHP versions in CI * update ci job name * remove class name quoting in exceptions to match PHP behavior * migrate pest config * add comment to test --------- Co-authored-by: Samuel Štancl <samuel@archte.ch> Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
This commit is contained in:
parent
fb521d2dcb
commit
f0ea4c36c8
9 changed files with 188 additions and 21 deletions
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
|
@ -12,14 +12,24 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pest:
|
pest:
|
||||||
name: Tests (Pest) L${{ matrix.laravel }}
|
name: Tests (Pest) PHP${{ matrix.php }} L${{ matrix.laravel }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
laravel: [9, 10]
|
include:
|
||||||
|
- laravel: 10
|
||||||
|
php: 8.1
|
||||||
|
- laravel: 10
|
||||||
|
php: 8.2
|
||||||
|
- laravel: 10
|
||||||
|
php: 8.3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{matrix.php}}
|
||||||
- name: Install composer dependencies
|
- name: Install composer dependencies
|
||||||
run: composer require "illuminate/support:^${{ matrix.laravel }}.0"
|
run: composer require "illuminate/support:^${{ matrix.laravel }}.0"
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
|
|
||||||
60
README.md
60
README.md
|
|
@ -8,6 +8,7 @@ A collection of enum helpers for PHP.
|
||||||
- [`Options`](#options)
|
- [`Options`](#options)
|
||||||
- [`From`](#from)
|
- [`From`](#from)
|
||||||
- [`Metadata`](#metadata)
|
- [`Metadata`](#metadata)
|
||||||
|
- [`Comparable`](#comparable)
|
||||||
|
|
||||||
You can read more about the idea on [Twitter](https://twitter.com/archtechx/status/1495158228757270528). I originally wanted to include the `InvokableCases` helper in [`archtechx/helpers`](https://github.com/archtechx/helpers), but it makes more sense to make it a separate dependency and use it *inside* the other package.
|
You can read more about the idea on [Twitter](https://twitter.com/archtechx/status/1495158228757270528). I originally wanted to include the `InvokableCases` helper in [`archtechx/helpers`](https://github.com/archtechx/helpers), but it makes more sense to make it a separate dependency and use it *inside* the other package.
|
||||||
|
|
||||||
|
|
@ -366,6 +367,65 @@ enum TaskStatus: int
|
||||||
|
|
||||||
And if you're using the same meta property in multiple enums, you can create a dedicated trait that includes this `@method` annotation.
|
And if you're using the same meta property in multiple enums, you can create a dedicated trait that includes this `@method` annotation.
|
||||||
|
|
||||||
|
### Comparable
|
||||||
|
|
||||||
|
This helper lets you compare enums by `is()`, `isNot()`, `in()` and `notIn()` operators.
|
||||||
|
|
||||||
|
#### Apply the trait on your enum
|
||||||
|
```php
|
||||||
|
use ArchTech\Enums\Comparable;
|
||||||
|
|
||||||
|
enum TaskStatus: int
|
||||||
|
{
|
||||||
|
use Comparable;
|
||||||
|
|
||||||
|
case INCOMPLETE = 0;
|
||||||
|
case COMPLETED = 1;
|
||||||
|
case CANCELED = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Role
|
||||||
|
{
|
||||||
|
use Comparable;
|
||||||
|
|
||||||
|
case ADMINISTRATOR;
|
||||||
|
case SUBSCRIBER;
|
||||||
|
case GUEST;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use the `is()` method
|
||||||
|
```php
|
||||||
|
TaskStatus::INCOMPLETE->is(TaskStatus::INCOMPLETE); // true
|
||||||
|
TaskStatus::INCOMPLETE->is(TaskStatus::COMPLETED); // false
|
||||||
|
Role::ADMINISTRATOR->is(Role::ADMINISTRATOR); // true
|
||||||
|
Role::ADMINISTRATOR->is(Role::NOBODY); // false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use the `isNot()` method
|
||||||
|
```php
|
||||||
|
TaskStatus::INCOMPLETE->isNot(TaskStatus::INCOMPLETE); // false
|
||||||
|
TaskStatus::INCOMPLETE->isNot(TaskStatus::COMPLETED); // true
|
||||||
|
Role::ADMINISTRATOR->isNot(Role::ADMINISTRATOR); // false
|
||||||
|
Role::ADMINISTRATOR->isNot(Role::NOBODY); // true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use the `in()` method
|
||||||
|
```php
|
||||||
|
TaskStatus::INCOMPLETE->in([TaskStatus::INCOMPLETE, TaskStatus::COMPLETED]); // true
|
||||||
|
TaskStatus::INCOMPLETE->in([TaskStatus::COMPLETED, TaskStatus::CANCELED]); // false
|
||||||
|
Role::ADMINISTRATOR->in([Role::ADMINISTRATOR, Role::GUEST]); // true
|
||||||
|
Role::ADMINISTRATOR->in([Role::SUBSCRIBER, Role::GUEST]); // false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use the `notIn()` method
|
||||||
|
```php
|
||||||
|
TaskStatus::INCOMPLETE->notIn([TaskStatus::INCOMPLETE, TaskStatus::COMPLETED]); // false
|
||||||
|
TaskStatus::INCOMPLETE->notIn([TaskStatus::COMPLETED, TaskStatus::CANCELED]); // true
|
||||||
|
Role::ADMINISTRATOR->notIn([Role::ADMINISTRATOR, Role::GUEST]); // false
|
||||||
|
Role::ADMINISTRATOR->notIn([Role::SUBSCRIBER, Role::GUEST]); // true
|
||||||
|
```
|
||||||
|
|
||||||
## PHPStan
|
## PHPStan
|
||||||
|
|
||||||
To assist PHPStan when using invokable cases, you can include the PHPStan extensions into your own `phpstan.neon` file:
|
To assist PHPStan when using invokable cases, you can include the PHPStan extensions into your own `phpstan.neon` file:
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@
|
||||||
"php": "^8.1"
|
"php": "^8.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"orchestra/testbench": "^7.0|^8.0",
|
"orchestra/testbench": "^8.0",
|
||||||
"pestphp/pest": "^1.2|^2.0",
|
"larastan/larastan": "^2.4",
|
||||||
"pestphp/pest-plugin-laravel": "^1.0|^2.0",
|
"pestphp/pest": "^2.0",
|
||||||
"larastan/larastan": "^1.0|^2.7.0"
|
"pestphp/pest-plugin-laravel": "^2.0"
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
|
|
|
||||||
14
phpunit.xml
14
phpunit.xml
|
|
@ -1,9 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
|
||||||
<coverage processUncoveredFiles="true">
|
<coverage>
|
||||||
<include>
|
|
||||||
<directory suffix=".php">./src</directory>
|
|
||||||
</include>
|
|
||||||
<report>
|
<report>
|
||||||
<clover outputFile="coverage/phpunit/clover.xml"/>
|
<clover outputFile="coverage/phpunit/clover.xml"/>
|
||||||
<html outputDirectory="coverage/phpunit/html" lowUpperBound="35" highLowerBound="70"/>
|
<html outputDirectory="coverage/phpunit/html" lowUpperBound="35" highLowerBound="70"/>
|
||||||
|
|
@ -22,12 +19,15 @@
|
||||||
<env name="MAIL_DRIVER" value="array"/>
|
<env name="MAIL_DRIVER" value="array"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
<env name="SESSION_DRIVER" value="array"/>
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
|
||||||
<env name="DB_CONNECTION" value="testbench"/>
|
<env name="DB_CONNECTION" value="testbench"/>
|
||||||
<env name="DB_DATABASE" value="main"/>
|
<env name="DB_DATABASE" value="main"/>
|
||||||
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
||||||
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
||||||
|
|
||||||
<env name="AWS_DEFAULT_REGION" value="us-west-2"/>
|
<env name="AWS_DEFAULT_REGION" value="us-west-2"/>
|
||||||
</php>
|
</php>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">./src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
|
||||||
50
src/Comparable.php
Normal file
50
src/Comparable.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ArchTech\Enums;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Iterator;
|
||||||
|
use IteratorAggregate;
|
||||||
|
|
||||||
|
trait Comparable
|
||||||
|
{
|
||||||
|
public function is(mixed $enum): bool
|
||||||
|
{
|
||||||
|
return $this === $enum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isNot(mixed $enum): bool
|
||||||
|
{
|
||||||
|
return ! $this->is($enum);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function in(array|object $enums): bool
|
||||||
|
{
|
||||||
|
$iterator = $enums;
|
||||||
|
|
||||||
|
if (! is_array($enums)) {
|
||||||
|
if ($enums instanceof Iterator) {
|
||||||
|
$iterator = $enums;
|
||||||
|
} elseif ($enums instanceof IteratorAggregate) {
|
||||||
|
$iterator = $enums->getIterator();
|
||||||
|
} else {
|
||||||
|
throw new Exception('in() expects an iterable value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($iterator as $item) {
|
||||||
|
if ($item === $this) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notIn(array|object $enums): bool
|
||||||
|
{
|
||||||
|
return ! $this->in($enums);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ trait From
|
||||||
*/
|
*/
|
||||||
public static function fromName(string $case): static
|
public static function fromName(string $case): static
|
||||||
{
|
{
|
||||||
return static::tryFromName($case) ?? throw new ValueError('"' . $case . '" is not a valid name for enum "' . static::class . '"');
|
return static::tryFromName($case) ?? throw new ValueError('"' . $case . '" is not a valid name for enum ' . static::class . '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use ArchTech\Enums\{InvokableCases, Options, Names, Values, From, Metadata};
|
use ArchTech\Enums\{Comparable, InvokableCases, Options, Names, Values, From, Metadata};
|
||||||
use ArchTech\Enums\Meta\Meta;
|
use ArchTech\Enums\Meta\Meta;
|
||||||
use ArchTech\Enums\Meta\MetaProperty;
|
use ArchTech\Enums\Meta\MetaProperty;
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ class Instructions extends MetaProperty
|
||||||
#[Meta(Color::class, Desc::class)] // variadic syntax
|
#[Meta(Color::class, Desc::class)] // variadic syntax
|
||||||
enum Status: int
|
enum Status: int
|
||||||
{
|
{
|
||||||
use InvokableCases, Options, Names, Values, From, Metadata;
|
use InvokableCases, Options, Names, Values, From, Metadata, Comparable;
|
||||||
|
|
||||||
#[Color('orange')] #[Desc('Incomplete task')]
|
#[Color('orange')] #[Desc('Incomplete task')]
|
||||||
case PENDING = 0;
|
case PENDING = 0;
|
||||||
|
|
@ -49,7 +49,7 @@ enum Status: int
|
||||||
#[Meta([Color::class, Desc::class, Instructions::class])] // array
|
#[Meta([Color::class, Desc::class, Instructions::class])] // array
|
||||||
enum Role
|
enum Role
|
||||||
{
|
{
|
||||||
use InvokableCases, Options, Names, Values, From, Metadata;
|
use InvokableCases, Options, Names, Values, From, Metadata, Comparable;
|
||||||
|
|
||||||
#[Color('indigo')]
|
#[Color('indigo')]
|
||||||
#[Desc('Administrator')]
|
#[Desc('Administrator')]
|
||||||
|
|
|
||||||
44
tests/Pest/ComparableTest.php
Normal file
44
tests/Pest/ComparableTest.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
test('the is method checks for equality', function () {
|
||||||
|
expect(Status::PENDING->is(Status::PENDING))->toBeTrue();
|
||||||
|
expect(Status::PENDING->is(Status::DONE))->toBeFalse();
|
||||||
|
expect(Role::ADMIN->is(Role::ADMIN))->toBeTrue();
|
||||||
|
|
||||||
|
expect(Role::ADMIN->is(Role::GUEST))->toBeFalse();
|
||||||
|
expect(Role::ADMIN->is('admin'))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the isNot method checks for inequality', function () {
|
||||||
|
expect(Status::PENDING->isNot(Status::DONE))->toBeTrue();
|
||||||
|
expect(Status::PENDING->isNot(Status::PENDING))->toBeFalse();
|
||||||
|
expect(Status::PENDING->isNot(Role::ADMIN))->toBeTrue();
|
||||||
|
expect(Role::ADMIN->isNot(Role::GUEST))->toBeTrue();
|
||||||
|
|
||||||
|
expect(Role::ADMIN->isNot(Role::ADMIN))->toBeFalse();
|
||||||
|
expect(Role::ADMIN->isNot('admin'))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the in method checks for presence in an array', function () {
|
||||||
|
expect(Status::PENDING->in([Status::PENDING, Status::DONE]))->toBeTrue();
|
||||||
|
expect(Role::ADMIN->in([Role::ADMIN]))->toBeTrue();
|
||||||
|
|
||||||
|
expect(Status::PENDING->in([Status::DONE]))->toBeFalse();
|
||||||
|
expect(Status::PENDING->in([Role::ADMIN, Role::GUEST]))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the not in method checks for absence in an array', function () {
|
||||||
|
expect(Status::PENDING->notIn([Status::DONE]))->toBeTrue();
|
||||||
|
expect(Role::ADMIN->notIn([Role::GUEST]))->toBeTrue();
|
||||||
|
|
||||||
|
expect(Status::PENDING->notIn([Status::PENDING, Status::DONE]))->toBeFalse();
|
||||||
|
expect(Role::ADMIN->notIn([Role::ADMIN, Role::GUEST]))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the in and notIn methods work with Laravel collections', function () {
|
||||||
|
expect(Status::PENDING->in(collect([Status::PENDING, Status::DONE])))->toBeTrue();
|
||||||
|
expect(Role::ADMIN->in(collect([Status::PENDING, Role::GUEST])))->toBeFalse();
|
||||||
|
|
||||||
|
expect(Status::DONE->notIn(collect([Status::PENDING])))->toBeTrue();
|
||||||
|
expect(Role::ADMIN->notIn(collect([Role::ADMIN, Status::PENDING])))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
@ -4,9 +4,12 @@ it('does not override the default BackedEnum from method')
|
||||||
->expect(Status::from(0))
|
->expect(Status::from(0))
|
||||||
->toBe(Status::PENDING);
|
->toBe(Status::PENDING);
|
||||||
|
|
||||||
|
// Shortened exception message due to inconsistency between PHP 8.1 and 8.2+
|
||||||
|
// 8.1: 2 is not a valid backing value for enum "Status"
|
||||||
|
// 8.2+: 2 is not a valid backing value for enum Status
|
||||||
it('does not override the default BackedEnum from method with errors', function () {
|
it('does not override the default BackedEnum from method with errors', function () {
|
||||||
Status::from(2);
|
Status::from(2);
|
||||||
})->throws(ValueError::class, '2 is not a valid backing value for enum "Status"');
|
})->throws(ValueError::class, '2 is not a valid backing value for enum');
|
||||||
|
|
||||||
it('does not override the default BackedEnum tryFrom method')
|
it('does not override the default BackedEnum tryFrom method')
|
||||||
->expect(Status::tryFrom(1))
|
->expect(Status::tryFrom(1))
|
||||||
|
|
@ -22,7 +25,7 @@ it('can select a case by name with from() for pure enums')
|
||||||
|
|
||||||
it('throws a value error when selecting a non-existent case with from() for pure enums', function () {
|
it('throws a value error when selecting a non-existent case with from() for pure enums', function () {
|
||||||
Role::from('NOBODY');
|
Role::from('NOBODY');
|
||||||
})->throws(ValueError::class, '"NOBODY" is not a valid name for enum "Role"');
|
})->throws(ValueError::class, '"NOBODY" is not a valid name for enum Role');
|
||||||
|
|
||||||
it('can select a case by name with tryFrom() for pure enums')
|
it('can select a case by name with tryFrom() for pure enums')
|
||||||
->expect(Role::tryFrom('GUEST'))
|
->expect(Role::tryFrom('GUEST'))
|
||||||
|
|
@ -38,7 +41,7 @@ it('can select a case by name with fromName() for pure enums')
|
||||||
|
|
||||||
it('throws a value error when selecting a non-existent case by name with fromName() for pure enums', function () {
|
it('throws a value error when selecting a non-existent case by name with fromName() for pure enums', function () {
|
||||||
Role::fromName('NOBODY');
|
Role::fromName('NOBODY');
|
||||||
})->throws(ValueError::class, '"NOBODY" is not a valid name for enum "Role"');
|
})->throws(ValueError::class, '"NOBODY" is not a valid name for enum Role');
|
||||||
|
|
||||||
it('can select a case by name with tryFromName() for pure enums')
|
it('can select a case by name with tryFromName() for pure enums')
|
||||||
->expect(Role::tryFromName('GUEST'))
|
->expect(Role::tryFromName('GUEST'))
|
||||||
|
|
@ -54,7 +57,7 @@ it('can select a case by name with fromName() for backed enums')
|
||||||
|
|
||||||
it('throws a value error when selecting a non-existent case by name with fromName() for backed enums', function () {
|
it('throws a value error when selecting a non-existent case by name with fromName() for backed enums', function () {
|
||||||
Status::fromName('NOTHING');
|
Status::fromName('NOTHING');
|
||||||
})->throws(ValueError::class, '"NOTHING" is not a valid name for enum "Status"');
|
})->throws(ValueError::class, '"NOTHING" is not a valid name for enum Status');
|
||||||
|
|
||||||
it('can select a case by name with tryFromName() for backed enums')
|
it('can select a case by name with tryFromName() for backed enums')
|
||||||
->expect(Status::tryFromName('DONE'))
|
->expect(Status::tryFromName('DONE'))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue