Skip to content

Commit edc0ca9

Browse files
author
Kirill Nesmeyanov
committed
Add unit enum support
1 parent bcb3dbf commit edc0ca9

File tree

6 files changed

+221
-13
lines changed

6 files changed

+221
-13
lines changed

src/Platform/StandardPlatform.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ public function getTypes(): iterable
3333
yield new Builder\SimpleTypeBuilder('mixed', Type\MixedType::class);
3434

3535
// Adds support for the "bool" type
36-
yield new Builder\SimpleTypeBuilder('bool', Type\BoolType::class);
36+
yield new Builder\SimpleTypeBuilder(['bool', 'boolean'], Type\BoolType::class);
3737

3838
// Adds support for the "string" type
3939
yield new Builder\SimpleTypeBuilder('string', Type\StringType::class);
4040

4141
// Adds support for the "int" type
42-
yield new Builder\IntTypeBuilder('int');
42+
yield new Builder\SimpleTypeBuilder(['int', 'integer'], Type\IntType::class);
4343

4444
// Adds support for the "float" type
45-
yield new Builder\SimpleTypeBuilder('float', Type\FloatType::class);
45+
yield new Builder\SimpleTypeBuilder(['float', 'double', 'real'], Type\FloatType::class);
4646

4747
// Adds support for the "array" type
48-
yield new Builder\ArrayTypeBuilder('array');
48+
yield new Builder\SimpleTypeBuilder('array', Type\ArrayType::class);
4949

5050
// Adds support for the "?T" statement
5151
yield new Builder\NullableTypeBuilder();
@@ -74,6 +74,9 @@ public function getTypes(): iterable
7474
// Adds support for the "BackedEnum" type
7575
yield new Builder\BackedEnumTypeBuilder();
7676

77+
// Adds support for the "UnitEnum" type
78+
yield new Builder\UnitEnumTypeBuilder();
79+
7780
// Adds support for the "Path\To\Class" statement
7881
yield new Builder\ObjectTypeBuilder($this->driver);
7982
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Type\Builder;
6+
7+
use TypeLang\Mapper\Exception\Definition\InternalTypeException;
8+
use TypeLang\Mapper\Runtime\Parser\TypeParserInterface;
9+
use TypeLang\Mapper\Runtime\Repository\TypeRepositoryInterface;
10+
use TypeLang\Mapper\Type\UnitEnumType;
11+
use TypeLang\Parser\Node\Stmt\NamedTypeNode;
12+
use TypeLang\Parser\Node\Stmt\TypeStatement;
13+
14+
/**
15+
* @template-extends Builder<NamedTypeNode, UnitEnumType>
16+
*/
17+
class UnitEnumTypeBuilder extends Builder
18+
{
19+
public function isSupported(TypeStatement $statement): bool
20+
{
21+
if (!$statement instanceof NamedTypeNode) {
22+
return false;
23+
}
24+
25+
/** @var non-empty-string $enum */
26+
$enum = $statement->name->toString();
27+
28+
return \enum_exists($statement->name->toString())
29+
&& !\is_subclass_of($enum, \BackedEnum::class);
30+
}
31+
32+
public function build(
33+
TypeStatement $statement,
34+
TypeRepositoryInterface $types,
35+
TypeParserInterface $parser,
36+
): UnitEnumType {
37+
$this->expectNoShapeFields($statement);
38+
$this->expectNoTemplateArguments($statement);
39+
40+
$names = \iterator_to_array($this->getEnumCaseNames($statement), false);
41+
42+
if ($names === []) {
43+
throw InternalTypeException::becauseInternalTypeErrorOccurs(
44+
type: $statement,
45+
message: 'The "{{type}}" enum requires at least one case',
46+
);
47+
}
48+
49+
return new UnitEnumType(
50+
// @phpstan-ignore-next-line
51+
class: $statement->name->toString(),
52+
cases: $names,
53+
);
54+
}
55+
56+
/**
57+
* @return \Traversable<array-key, non-empty-string>
58+
*/
59+
private function getEnumCaseNames(NamedTypeNode $statement): \Traversable
60+
{
61+
/** @var class-string<\UnitEnum> $enum */
62+
$enum = $statement->name->toString();
63+
64+
foreach ($enum::cases() as $case) {
65+
// @phpstan-ignore-next-line : Enum case name cannot be empty
66+
yield $case->name;
67+
}
68+
}
69+
}

src/Type/UnitEnumType.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Type;
6+
7+
use TypeLang\Mapper\Type\UnitEnumType\UnitEnumTypeDenormalizer;
8+
use TypeLang\Mapper\Type\UnitEnumType\UnitEnumTypeNormalizer;
9+
10+
/**
11+
* @template-extends AsymmetricType<UnitEnumTypeNormalizer, UnitEnumTypeDenormalizer>
12+
*/
13+
class UnitEnumType extends AsymmetricType
14+
{
15+
/**
16+
* @param class-string<\UnitEnum> $class
17+
* @param non-empty-list<non-empty-string> $cases
18+
*/
19+
public function __construct(string $class, array $cases)
20+
{
21+
parent::__construct(
22+
normalizer: new UnitEnumTypeNormalizer($class),
23+
denormalizer: new UnitEnumTypeDenormalizer($class, $cases),
24+
);
25+
}
26+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Type\UnitEnumType;
6+
7+
use TypeLang\Mapper\Exception\Mapping\InvalidValueException;
8+
use TypeLang\Mapper\Exception\Mapping\RuntimeExceptionInterface;
9+
use TypeLang\Mapper\Runtime\Context;
10+
use TypeLang\Mapper\Type\TypeInterface;
11+
12+
class UnitEnumTypeDenormalizer implements TypeInterface
13+
{
14+
/**
15+
* @param class-string<\UnitEnum> $class
16+
* @param non-empty-list<non-empty-string> $cases
17+
*/
18+
public function __construct(
19+
protected readonly string $class,
20+
protected readonly array $cases,
21+
) {}
22+
23+
public function match(mixed $value, Context $context): bool
24+
{
25+
if (!\is_string($value) || $value === '') {
26+
return false;
27+
}
28+
29+
return \in_array($value, $this->cases, true);
30+
}
31+
32+
/**
33+
* Converts a scalar representation of an enum to an enum case object.
34+
*
35+
* @throws InvalidValueException
36+
* @throws RuntimeExceptionInterface
37+
* @throws \Throwable
38+
*/
39+
public function cast(mixed $value, Context $context): \UnitEnum
40+
{
41+
if (!$this->match($value, $context)) {
42+
throw InvalidValueException::createFromContext(
43+
value: $value,
44+
context: $context,
45+
);
46+
}
47+
48+
try {
49+
/** @var \UnitEnum */
50+
return \constant($this->class . '::' . $value);
51+
} catch (\Error $e) {
52+
throw InvalidValueException::createFromContext(
53+
value: $value,
54+
context: $context,
55+
previous: $e,
56+
);
57+
}
58+
}
59+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Type\UnitEnumType;
6+
7+
use TypeLang\Mapper\Exception\Mapping\InvalidValueException;
8+
use TypeLang\Mapper\Runtime\Context;
9+
use TypeLang\Mapper\Type\TypeInterface;
10+
11+
class UnitEnumTypeNormalizer implements TypeInterface
12+
{
13+
/**
14+
* @param class-string<\UnitEnum> $class
15+
*/
16+
public function __construct(
17+
protected readonly string $class,
18+
) {}
19+
20+
public function match(mixed $value, Context $context): bool
21+
{
22+
return $value instanceof $this->class;
23+
}
24+
25+
/**
26+
* Converts enum case (of {@see \UnitEnum}) objects to their
27+
* scalar representation.
28+
*
29+
* @throws InvalidValueException
30+
*/
31+
public function cast(mixed $value, Context $context): string
32+
{
33+
if (!$value instanceof $this->class) {
34+
throw InvalidValueException::createFromContext(
35+
value: $value,
36+
context: $context,
37+
);
38+
}
39+
40+
return $value->name;
41+
}
42+
}

tests/Feature/Platform/standard.feature

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ Feature: Checking for presence of type definitions in the Standard Platform
88
Then the type must be defined
99
Examples:
1010
| type | reason |
11-
| self | PHP 5.0+ |
12-
| parent | PHP 5.0+ |
11+
# TODO: Self references not supported yet
12+
# | self | PHP 5.0+ |
13+
# TODO: Parent references not supported yet
14+
# | parent | PHP 5.0+ |
1315

1416
| array | PHP 5.1+ |
1517
| TypeLang\Mapper\Tests\Feature\Platform\Stub\ObjectStub | PHP 5.1+ |
16-
| TypeLang\Mapper\Tests\Feature\Platform\Stub\InterfaceStub | PHP 5.1+ |
18+
# TODO: Interfaces not supported yet
19+
# | TypeLang\Mapper\Tests\Feature\Platform\Stub\InterfaceStub | PHP 5.1+ |
1720

18-
| callable | PHP 5.4+ |
21+
# TODO: Callable types not supported yet
22+
# | callable | PHP 5.4+ |
1923

2024
| int | PHP 7.0+ |
2125
| integer | PHP 7.0+ alias |
@@ -26,24 +30,29 @@ Feature: Checking for presence of type definitions in the Standard Platform
2630
| double | PHP 7.0+ alias |
2731
| string | PHP 7.0+ |
2832

29-
| void | PHP 7.1+ |
33+
# TODO: Void types not supported yet
34+
# | void | PHP 7.1+ |
3035
| ?int | PHP 7.1+ |
3136
| iterable | PHP 7.1+ |
3237

3338
| object | PHP 7.2+ |
3439

3540
| int\|false | PHP 8.0+ |
36-
| static | PHP 8.0+ |
41+
# TODO: Static references not supported yet
42+
# | static | PHP 8.0+ |
3743
| mixed | PHP 8.0+ |
3844

39-
| never | PHP 8.1+ |
40-
| int&string | PHP 8.1+ |
45+
# TODO: Void types not supported yet
46+
# | never | PHP 8.1+ |
47+
# TODO: Intersection types not supported yet
48+
# | int&string | PHP 8.1+ |
4149
| TypeLang\Mapper\Tests\Feature\Platform\Stub\UnitEnumStub | PHP 8.1+ |
4250
| TypeLang\Mapper\Tests\Feature\Platform\Stub\IntBackedEnumStub | PHP 8.1+ |
4351
| TypeLang\Mapper\Tests\Feature\Platform\Stub\StringBackedEnumStub | PHP 8.1+ |
4452

4553
| null | PHP 8.2+ |
4654
| false | PHP 8.2+ |
4755
| true | PHP 8.2+ |
48-
| int\|(true&int) | PHP 8.2+ |
56+
# TODO: Intersection types not supported yet
57+
# | int\|(true&int) | PHP 8.2+ |
4958

0 commit comments

Comments
 (0)