├── LICENSE ├── README.md ├── behat.yml ├── composer.json ├── resources ├── .meta-storm.xml └── config.schema.json └── src ├── DenormalizerInterface.php ├── Exception ├── Definition │ ├── DefinitionException.php │ ├── InternalTypeException.php │ ├── PropertyTypeNotFoundException.php │ ├── Shape │ │ ├── ShapeFieldException.php │ │ ├── ShapeFieldsException.php │ │ └── ShapeFieldsNotSupportedException.php │ ├── Template │ │ ├── Hint │ │ │ ├── TemplateArgumentHintException.php │ │ │ ├── TemplateArgumentHintsException.php │ │ │ └── TemplateArgumentHintsNotSupportedException.php │ │ ├── InvalidTemplateArgumentException.php │ │ ├── MissingTemplateArgumentsException.php │ │ ├── TemplateArgumentException.php │ │ ├── TemplateArgumentsCountException.php │ │ ├── TemplateArgumentsException.php │ │ ├── TemplateArgumentsNotSupportedException.php │ │ └── TooManyTemplateArgumentsException.php │ └── TypeNotFoundException.php ├── Environment │ ├── ComposerPackageRequiredException.php │ └── EnvironmentException.php ├── MapperExceptionInterface.php ├── Mapping │ ├── FinalExceptionInterface.php │ ├── InvalidIterableKeyException.php │ ├── InvalidIterableValueException.php │ ├── InvalidObjectValueException.php │ ├── InvalidValueException.php │ ├── InvalidValueOfTypeException.php │ ├── IterableException.php │ ├── IterableKeyException.php │ ├── IterableValueException.php │ ├── MissingRequiredObjectFieldException.php │ ├── NonInstantiatableObjectException.php │ ├── ObjectException.php │ ├── ObjectFieldException.php │ ├── ObjectValueException.php │ ├── RuntimeException.php │ ├── ValueException.php │ └── ValueOfTypeException.php └── Template.php ├── Mapper.php ├── Mapping ├── DiscriminatorMap.php ├── Driver │ ├── ArrayConfigDriver.php │ ├── ArrayConfigDriver │ │ └── SchemaValidator.php │ ├── AttributeDriver.php │ ├── CachedDriver.php │ ├── DocBlockDriver.php │ ├── DocBlockDriver │ │ ├── ClassPropertyTypeDriver.php │ │ └── PromotedPropertyTypeDriver.php │ ├── Driver.php │ ├── DriverInterface.php │ ├── InMemoryCachedDriver.php │ ├── LoadableDriver.php │ ├── NullDriver.php │ ├── PHPConfigDriver.php │ ├── PHPConfigFileDriver.php │ ├── Psr16CachedDriver.php │ └── ReflectionDriver.php ├── Introspection │ ├── ClassIntrospection.php │ ├── IntrospectionInterface.php │ └── PropertyIntrospection.php ├── MapName.php ├── MapType.php ├── Metadata │ ├── ClassMetadata.php │ ├── DiscriminatorMapMetadata.php │ ├── EmptyConditionMetadata.php │ ├── ExpressionConditionMetadata.php │ ├── MatchConditionMetadata.php │ ├── Metadata.php │ ├── NullConditionMetadata.php │ ├── PropertyMetadata.php │ └── TypeMetadata.php ├── NormalizeAsArray.php ├── SkipWhen.php ├── SkipWhenEmpty.php └── SkipWhenNull.php ├── NormalizerInterface.php ├── Platform ├── DelegatePlatform.php ├── EmptyPlatform.php ├── GrammarFeature.php ├── Platform.php ├── PlatformInterface.php ├── Standard │ ├── Builder │ │ ├── ArrayTypeBuilder.php │ │ ├── BackedEnumTypeBuilder.php │ │ ├── BoolLiteralTypeBuilder.php │ │ ├── Builder.php │ │ ├── CallableTypeBuilder.php │ │ ├── ClassTypeBuilder.php │ │ ├── DateTimeTypeBuilder.php │ │ ├── FloatLiteralTypeBuilder.php │ │ ├── IntLiteralTypeBuilder.php │ │ ├── IntRangeTypeBuilder.php │ │ ├── ListTypeBuilder.php │ │ ├── NamedTypeBuilder.php │ │ ├── NullTypeBuilder.php │ │ ├── NullableTypeBuilder.php │ │ ├── ObjectTypeBuilder.php │ │ ├── PsrContainerTypeBuilder.php │ │ ├── SimpleTypeBuilder.php │ │ ├── TypeBuilderInterface.php │ │ ├── TypesListBuilder.php │ │ ├── UnionTypeBuilder.php │ │ └── UnitEnumTypeBuilder.php │ └── Type │ │ ├── ArrayKeyType.php │ │ ├── ArrayType.php │ │ ├── AsymmetricType.php │ │ ├── BackedEnumType.php │ │ ├── BackedEnumType │ │ ├── BackedEnumTypeDenormalizer.php │ │ └── BackedEnumTypeNormalizer.php │ │ ├── BoolLiteralType.php │ │ ├── BoolType.php │ │ ├── ClassType.php │ │ ├── ClassType │ │ ├── ClassTypeDenormalizer.php │ │ └── ClassTypeNormalizer.php │ │ ├── DateTimeType.php │ │ ├── DateTimeType │ │ ├── DateTimeTypeDenormalizer.php │ │ └── DateTimeTypeNormalizer.php │ │ ├── FloatLiteralType.php │ │ ├── FloatType.php │ │ ├── IntLiteralType.php │ │ ├── IntRangeType.php │ │ ├── IntType.php │ │ ├── ListType.php │ │ ├── MixedType.php │ │ ├── NullType.php │ │ ├── NullableType.php │ │ ├── ObjectType.php │ │ ├── ObjectType │ │ ├── ObjectTypeDenormalizer.php │ │ └── ObjectTypeNormalizer.php │ │ ├── StringType.php │ │ ├── TypeInterface.php │ │ ├── UnionType.php │ │ ├── UnitEnumType.php │ │ └── UnitEnumType │ │ ├── UnitEnumTypeDenormalizer.php │ │ └── UnitEnumTypeNormalizer.php └── StandardPlatform.php └── Runtime ├── ClassInstantiator ├── ClassInstantiatorInterface.php └── ReflectionClassInstantiator.php ├── Configuration.php ├── ConfigurationInterface.php ├── Context.php ├── Context ├── ChildContext.php ├── Direction.php ├── DirectionInterface.php └── RootContext.php ├── Parser ├── InMemoryTypeParser.php ├── LoggableTypeParser.php ├── TraceableTypeParser.php ├── TypeParser.php ├── TypeParserFacade.php ├── TypeParserFacadeInterface.php └── TypeParserInterface.php ├── Path ├── Entry │ ├── ArrayIndexEntry.php │ ├── Entry.php │ ├── EntryInterface.php │ ├── ObjectEntry.php │ ├── ObjectPropertyEntry.php │ └── UnionLeafEntry.php ├── JsonPathPrinter.php ├── Path.php ├── PathInterface.php ├── PathPrinterInterface.php └── PathProviderInterface.php ├── PropertyAccessor ├── NullPropertyAccessor.php ├── PropertyAccessorInterface.php └── ReflectionPropertyAccessor.php ├── Repository ├── InMemoryTypeRepository.php ├── LoggableTypeRepository.php ├── Reference │ ├── NativeReferencesReader.php │ ├── NullReferencesReader.php │ └── ReferencesReaderInterface.php ├── ReferencesResolver.php ├── TraceableTypeRepository.php ├── TypeDecorator │ ├── LoggableType.php │ ├── TraceableType.php │ ├── TypeDecorator.php │ └── TypeDecoratorInterface.php ├── TypeRepository.php ├── TypeRepositoryDecorator.php ├── TypeRepositoryDecoratorInterface.php ├── TypeRepositoryFacade.php ├── TypeRepositoryFacadeInterface.php └── TypeRepositoryInterface.php ├── Tracing ├── SpanInterface.php ├── SymfonyStopwatchTracer.php ├── SymfonyStopwatchTracer │ └── SymfonyStopwatchSpan.php └── TracerInterface.php └── Value ├── JsonValuePrinter.php ├── PHPValuePrinter.php ├── SymfonyValuePrinter.php └── ValuePrinterInterface.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nesmeyanov Kirill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | paths: 5 | - '%paths.base%/tests/Feature' 6 | contexts: 7 | # Executors 8 | - TypeLang\Mapper\Tests\Context\Assert\TypeCastAssertions 9 | - TypeLang\Mapper\Tests\Context\Assert\TypeMatchAssertions 10 | - TypeLang\Mapper\Tests\Context\Assert\TypeStatementAssertions 11 | - TypeLang\Mapper\Tests\Context\Assert\ValueAssertions 12 | # Providers 13 | - TypeLang\Mapper\Tests\Context\Provider\ConfigurationContext 14 | - TypeLang\Mapper\Tests\Context\Provider\MappingContext 15 | - TypeLang\Mapper\Tests\Context\Provider\MetadataContext 16 | - TypeLang\Mapper\Tests\Context\Provider\PlatformContext 17 | - TypeLang\Mapper\Tests\Context\Provider\TypeContext 18 | - TypeLang\Mapper\Tests\Context\Provider\TypeContext\EnumTypeContext 19 | - TypeLang\Mapper\Tests\Context\Provider\TypeParserContext 20 | - TypeLang\Mapper\Tests\Context\Provider\TypeRepositoryContext 21 | # Utils 22 | - TypeLang\Mapper\Tests\Context\Support\VarDumperContext 23 | - TypeLang\Mapper\Tests\Context\Support\MarkersContext 24 | 25 | extensions: 26 | TypeLang\Mapper\Tests\Extension\ContextArgumentTransformerExtension: 27 | capture: 28 | start: '{{' 29 | end: '}}' 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-lang/mapper", 3 | "type": "library", 4 | "description": "Library for mapping variable types to other types", 5 | "keywords": ["types", "serializer", "mapper", "hydrator", "transformer", "normalizer", "denormalizer", "marshal", "unmarshal"], 6 | "license": "MIT", 7 | "support": { 8 | "source": "https://github.com/php-type-language/mapper", 9 | "issues": "https://github.com/php-type-language/mapper/issues" 10 | }, 11 | "require": { 12 | "php": "^8.1", 13 | "psr/log": "^1.0|^2.0|^3.0", 14 | "psr/simple-cache": "^1.0|^2.0|^3.0", 15 | "type-lang/parser": "^1.4", 16 | "type-lang/printer": "^1.3" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "TypeLang\\Mapper\\": "src" 21 | } 22 | }, 23 | "require-dev": { 24 | "behat/behat": "^3.14", 25 | "friendsofphp/php-cs-fixer": "^3.53", 26 | "jetbrains/phpstorm-attributes": "^1.0", 27 | "justinrainbow/json-schema": "^6.0", 28 | "monolog/monolog": "^3.7", 29 | "phpstan/phpstan": "^2.1", 30 | "phpstan/phpstan-strict-rules": "^2.0", 31 | "phpunit/phpunit": "^10.5|^11.0", 32 | "symfony/cache": "^5.4|^6.0|^7.0", 33 | "symfony/expression-language": "^5.4|^6.0|^7.0", 34 | "symfony/property-access": "^5.4|^6.0|^7.0", 35 | "symfony/stopwatch": "^5.4|^6.0|^7.0", 36 | "symfony/var-dumper": "^5.4|^6.0|^7.0", 37 | "type-lang/phpdoc": "^1.0", 38 | "type-lang/phpdoc-standard-tags": "^1.0" 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "TypeLang\\Mapper\\Tests\\": "tests" 43 | } 44 | }, 45 | "suggest": { 46 | "type-lang/phpdoc-standard-tags": "(^1.0) Required for DocBlockDriver mapping driver support", 47 | "justinrainbow/json-schema": "(^5.3|^6.0) Required for configuration drivers validation" 48 | }, 49 | "extra": { 50 | "branch-alias": { 51 | "dev-master": "1.x-dev", 52 | "dev-main": "1.x-dev" 53 | } 54 | }, 55 | "config": { 56 | "sort-packages": true, 57 | "platform-check": true, 58 | "bin-compat": "full", 59 | "optimize-autoloader": true, 60 | "preferred-install": { 61 | "*": "dist" 62 | } 63 | }, 64 | "scripts": { 65 | "build": "@php bin/build", 66 | 67 | "test": ["@test:unit", "@test:feature"], 68 | "test:unit": "phpunit --testdox --testsuite=unit", 69 | "test:feature": "behat", 70 | 71 | "linter": "@linter:check", 72 | "linter:check": "phpstan analyse --configuration phpstan.neon", 73 | "linter:baseline": "phpstan analyse --configuration phpstan.neon --generate-baseline", 74 | 75 | "phpcs": "@phpcs:check", 76 | "phpcs:check": "php-cs-fixer fix --config=.php-cs-fixer.php --allow-risky=yes --dry-run --verbose --diff", 77 | "phpcs:fix": "php-cs-fixer fix --config=.php-cs-fixer.php --allow-risky=yes --verbose --diff" 78 | }, 79 | "minimum-stability": "dev", 80 | "prefer-stable": true 81 | } 82 | -------------------------------------------------------------------------------- /src/DenormalizerInterface.php: -------------------------------------------------------------------------------- 1 | denormalize(\json_decode('{ 20 | * "id": 42, 21 | * "name": "Kirill" 22 | * }'), ExampleClass::class); 23 | * ``` 24 | * 25 | * @param non-empty-string $type 26 | * 27 | * @throws RuntimeException in case of runtime mapping exception occurs 28 | * @throws DefinitionException in case of type building exception occurs 29 | * @throws \Throwable in case of any internal error occurs 30 | */ 31 | public function denormalize(mixed $value, string $type): mixed; 32 | 33 | /** 34 | * Returns {@see true} if the value can be denormalized for the given type. 35 | * 36 | * @param non-empty-string $type 37 | * 38 | * @throws DefinitionException in case of type building exception occurs 39 | * @throws \Throwable in case of any internal error occurs 40 | */ 41 | public function isDenormalizable(mixed $value, string $type): bool; 42 | } 43 | -------------------------------------------------------------------------------- /src/Exception/Definition/DefinitionException.php: -------------------------------------------------------------------------------- 1 | message = $this->template = new Template($template, $this); 27 | } 28 | 29 | /** 30 | * Returns the type statement whose definition caused the error. 31 | * 32 | * @api 33 | */ 34 | public function getType(): TypeStatement 35 | { 36 | return $this->type; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Exception/Definition/InternalTypeException.php: -------------------------------------------------------------------------------- 1 | class; 56 | } 57 | 58 | /** 59 | * @api 60 | * 61 | * @return non-empty-string 62 | */ 63 | public function getProperty(): string 64 | { 65 | return $this->property; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Exception/Definition/Shape/ShapeFieldException.php: -------------------------------------------------------------------------------- 1 | field; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/Definition/Shape/ShapeFieldsException.php: -------------------------------------------------------------------------------- 1 | hint = $argument->hint; 26 | 27 | parent::__construct( 28 | argument: $argument, 29 | type: $type, 30 | template: $template, 31 | code: $code, 32 | previous: $previous, 33 | ); 34 | } 35 | 36 | /** 37 | * @api 38 | */ 39 | public function findArgumentHint(): ?Identifier 40 | { 41 | return $this->hint; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Exception/Definition/Template/Hint/TemplateArgumentHintsException.php: -------------------------------------------------------------------------------- 1 | argument->value; 40 | } 41 | 42 | /** 43 | * Returns the type statement in which the error occurred. 44 | * 45 | * @api 46 | */ 47 | public function getExpectedType(): TypeStatement 48 | { 49 | return $this->expected; 50 | } 51 | 52 | public static function becauseTemplateArgumentIsInvalid( 53 | TypeStatement $expected, 54 | TemplateArgumentNode $argument, 55 | TypeStatement $type, 56 | ?\Throwable $previous = null, 57 | ): self { 58 | $template = 'Passed template argument #{{index}} of type {{type}} must ' 59 | . 'be of type {{expected}}, but {{argument}} given'; 60 | 61 | return new self( 62 | expected: $expected, 63 | argument: $argument, 64 | type: $type, 65 | template: $template, 66 | previous: $previous, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Exception/Definition/Template/MissingTemplateArgumentsException.php: -------------------------------------------------------------------------------- 1 | $minSupportedArgumentsCount 16 | * @param int<0, max> $maxSupportedArgumentsCount 17 | */ 18 | public static function becauseTemplateArgumentsRangeRequired( 19 | int $minSupportedArgumentsCount, 20 | int $maxSupportedArgumentsCount, 21 | NamedTypeNode $type, 22 | ?\Throwable $previous = null, 23 | ): self { 24 | $template = 'Type "{{type}}" expects at least %s template argument(s), ' 25 | . 'but {{passedArgumentsCount}} were passed'; 26 | 27 | $template = $minSupportedArgumentsCount === $maxSupportedArgumentsCount 28 | ? \sprintf($template, '{{minSupportedArgumentsCount}}') 29 | : \sprintf($template, 'from {{minSupportedArgumentsCount}} to {{maxSupportedArgumentsCount}}'); 30 | 31 | return new self( 32 | passedArgumentsCount: $type->arguments?->count() ?? 0, 33 | minSupportedArgumentsCount: $minSupportedArgumentsCount, 34 | maxSupportedArgumentsCount: $maxSupportedArgumentsCount, 35 | type: $type, 36 | template: $template, 37 | previous: $previous, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Exception/Definition/Template/TemplateArgumentException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | protected readonly int $index; 20 | 21 | public function __construct( 22 | protected readonly TemplateArgumentNode $argument, 23 | TypeStatement $type, 24 | string $template, 25 | int $code = 0, 26 | ?\Throwable $previous = null, 27 | ) { 28 | $this->index = self::fetchArgumentIndex($argument, $type); 29 | 30 | parent::__construct($type, $template, $code, $previous); 31 | } 32 | 33 | /** 34 | * @return int<0, max> 35 | */ 36 | private static function fetchArgumentIndex(TemplateArgumentNode $argument, TypeStatement $type): int 37 | { 38 | $index = 0; 39 | 40 | if (!$type instanceof NamedTypeNode) { 41 | return $index; 42 | } 43 | 44 | foreach ($type->arguments ?? [] as $actual) { 45 | if ($actual === $argument) { 46 | return $index + 1; 47 | } 48 | 49 | ++$index; 50 | } 51 | 52 | return $index; 53 | } 54 | 55 | /** 56 | * @api 57 | */ 58 | public function getArgument(): TemplateArgumentNode 59 | { 60 | return $this->argument; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Exception/Definition/Template/TemplateArgumentsCountException.php: -------------------------------------------------------------------------------- 1 | $passedArgumentsCount 16 | * @param int<0, max> $minSupportedArgumentsCount 17 | * @param int<0, max> $maxSupportedArgumentsCount 18 | */ 19 | public function __construct( 20 | protected readonly int $passedArgumentsCount, 21 | protected readonly int $minSupportedArgumentsCount, 22 | protected readonly int $maxSupportedArgumentsCount, 23 | NamedTypeNode $type, 24 | string $template, 25 | int $code = 0, 26 | ?\Throwable $previous = null, 27 | ) { 28 | parent::__construct( 29 | type: $type, 30 | template: $template, 31 | code: $code, 32 | previous: $previous, 33 | ); 34 | } 35 | 36 | /** 37 | * @api 38 | * 39 | * @return int<0, max> 40 | */ 41 | public function getPassedArgumentsCount(): int 42 | { 43 | return $this->passedArgumentsCount; 44 | } 45 | 46 | /** 47 | * @api 48 | * 49 | * @return int<0, max> 50 | */ 51 | public function getMinSupportedArgumentsCount(): int 52 | { 53 | return $this->minSupportedArgumentsCount; 54 | } 55 | 56 | /** 57 | * @api 58 | * 59 | * @return int<0, max> 60 | */ 61 | public function getMaxSupportedArgumentsCount(): int 62 | { 63 | return $this->maxSupportedArgumentsCount; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Exception/Definition/Template/TemplateArgumentsException.php: -------------------------------------------------------------------------------- 1 | arguments?->count() ?? 0, 23 | minSupportedArgumentsCount: 0, 24 | maxSupportedArgumentsCount: 0, 25 | type: $type, 26 | template: $template, 27 | previous: $previous, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exception/Definition/Template/TooManyTemplateArgumentsException.php: -------------------------------------------------------------------------------- 1 | $minSupportedArgumentsCount 16 | * @param int<0, max> $maxSupportedArgumentsCount 17 | */ 18 | public static function becauseTemplateArgumentsRangeOverflows( 19 | int $minSupportedArgumentsCount, 20 | int $maxSupportedArgumentsCount, 21 | NamedTypeNode $type, 22 | ?\Throwable $previous = null, 23 | ): self { 24 | $template = 'Type "{{type}}" only accepts %s template argument(s), ' 25 | . 'but {{passedArgumentsCount}} were passed'; 26 | 27 | $template = $minSupportedArgumentsCount === $maxSupportedArgumentsCount 28 | ? \sprintf($template, '{{minSupportedArgumentsCount}}') 29 | : \sprintf($template, 'from {{minSupportedArgumentsCount}} to {{maxSupportedArgumentsCount}}'); 30 | 31 | return new self( 32 | passedArgumentsCount: $type->arguments?->count() ?? 0, 33 | minSupportedArgumentsCount: $minSupportedArgumentsCount, 34 | maxSupportedArgumentsCount: $maxSupportedArgumentsCount, 35 | type: $type, 36 | template: $template, 37 | previous: $previous, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Exception/Definition/TypeNotFoundException.php: -------------------------------------------------------------------------------- 1 | package; 32 | } 33 | 34 | /** 35 | * @param non-empty-string $package 36 | * @param non-empty-string $purpose 37 | */ 38 | public static function becausePackageNotInstalled( 39 | string $package, 40 | string $purpose, 41 | ?\Throwable $previous = null, 42 | ): self { 43 | $template = 'The {{package}} component is required to %s. ' 44 | . 'Try running "composer require %s"'; 45 | 46 | return new self( 47 | package: $package, 48 | template: \sprintf($template, $purpose, $package), 49 | previous: $previous, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Exception/Environment/EnvironmentException.php: -------------------------------------------------------------------------------- 1 | message = $this->template = new Template($template, $this); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Exception/MapperExceptionInterface.php: -------------------------------------------------------------------------------- 1 | $index 14 | * @param iterable $value 15 | */ 16 | public static function createFromPath( 17 | int $index, 18 | mixed $key, 19 | iterable $value, 20 | PathInterface $path, 21 | ?\Throwable $previous = null, 22 | ): self { 23 | $template = 'The key {{key}} on index {{index}} in {{value}} is invalid'; 24 | 25 | return new self( 26 | index: $index, 27 | key: $key, 28 | value: $value, 29 | path: $path, 30 | template: $template, 31 | previous: $previous, 32 | ); 33 | } 34 | 35 | /** 36 | * @param int<0, max> $index 37 | * @param iterable $value 38 | */ 39 | public static function createFromContext( 40 | int $index, 41 | mixed $key, 42 | iterable $value, 43 | Context $context, 44 | ?\Throwable $previous = null, 45 | ): self { 46 | return self::createFromPath( 47 | index: $index, 48 | key: $key, 49 | value: $value, 50 | path: $context->getPath(), 51 | previous: $previous, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Exception/Mapping/InvalidIterableValueException.php: -------------------------------------------------------------------------------- 1 | $index 14 | * @param iterable $value 15 | */ 16 | public static function createFromPath( 17 | mixed $element, 18 | int $index, 19 | mixed $key, 20 | mixed $value, 21 | PathInterface $path, 22 | ?\Throwable $previous = null, 23 | ): self { 24 | $template = 'Passed value {{element}} on {{key}} in {{value}} is invalid'; 25 | 26 | if (!\is_scalar($key)) { 27 | $template = \str_replace('{{key}}', '{{key}} (on index {{index}})', $template); 28 | } elseif (\is_array($value) && \array_is_list($value)) { 29 | $template = \str_replace('{{key}}', 'index {{index}}', $template); 30 | } 31 | 32 | return new self( 33 | element: $element, 34 | index: $index, 35 | key: $key, 36 | value: $value, 37 | path: $path, 38 | template: $template, 39 | previous: $previous, 40 | ); 41 | } 42 | 43 | /** 44 | * @param int<0, max> $index 45 | * @param iterable $value 46 | */ 47 | public static function createFromContext( 48 | mixed $element, 49 | int $index, 50 | mixed $key, 51 | mixed $value, 52 | Context $context, 53 | ?\Throwable $previous = null, 54 | ): self { 55 | return self::createFromPath( 56 | element: $element, 57 | index: $index, 58 | key: $key, 59 | value: $value, 60 | path: $context->getPath(), 61 | previous: $previous, 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Exception/Mapping/InvalidObjectValueException.php: -------------------------------------------------------------------------------- 1 | |object $value 16 | */ 17 | public static function createFromPath( 18 | mixed $element, 19 | string $field, 20 | ?TypeStatement $expected, 21 | array|object $value, 22 | PathInterface $path, 23 | ?\Throwable $previous = null, 24 | ): self { 25 | $template = 'Passed value in {{field}} of {{value}} must be of type {{expected}}, but {{element}} given'; 26 | 27 | return new self( 28 | element: $element, 29 | field: $field, 30 | expected: $expected ?? self::mixedTypeStatement(), 31 | value: $value, 32 | path: $path, 33 | template: $template, 34 | previous: $previous, 35 | ); 36 | } 37 | 38 | /** 39 | * @param non-empty-string $field 40 | * @param array|object $value 41 | */ 42 | public static function createFromContext( 43 | mixed $element, 44 | string $field, 45 | ?TypeStatement $expected, 46 | array|object $value, 47 | Context $context, 48 | ?\Throwable $previous = null, 49 | ): self { 50 | return self::createFromPath( 51 | element: $element, 52 | field: $field, 53 | expected: $expected, 54 | value: $value, 55 | path: $context->getPath(), 56 | previous: $previous, 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Exception/Mapping/InvalidValueException.php: -------------------------------------------------------------------------------- 1 | getPath(), 35 | previous: $previous, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Exception/Mapping/InvalidValueOfTypeException.php: -------------------------------------------------------------------------------- 1 | getPath(), 41 | previous: $previous, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Exception/Mapping/IterableException.php: -------------------------------------------------------------------------------- 1 | $value 14 | */ 15 | public function __construct( 16 | iterable $value, 17 | PathInterface $path, 18 | string $template, 19 | int $code = 0, 20 | ?\Throwable $previous = null, 21 | ) { 22 | parent::__construct( 23 | value: $value, 24 | path: $path, 25 | template: $template, 26 | code: $code, 27 | previous: $previous, 28 | ); 29 | } 30 | 31 | /** 32 | * Unlike {@see ValueException::getValue()}, this exception 33 | * value can only be {@see iterable}. 34 | * 35 | * @return iterable 36 | */ 37 | public function getValue(): iterable 38 | { 39 | /** @var iterable */ 40 | return $this->value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Exception/Mapping/IterableKeyException.php: -------------------------------------------------------------------------------- 1 | $index 13 | * @param iterable $value 14 | */ 15 | public function __construct( 16 | protected readonly int $index, 17 | protected readonly mixed $key, 18 | iterable $value, 19 | PathInterface $path, 20 | string $template, 21 | int $code = 0, 22 | ?\Throwable $previous = null, 23 | ) { 24 | parent::__construct( 25 | value: $value, 26 | path: $path, 27 | template: $template, 28 | code: $code, 29 | previous: $previous, 30 | ); 31 | } 32 | 33 | /** 34 | * Returns ordered index of an element. 35 | * 36 | * @return int<0, max> 37 | */ 38 | public function getIndex(): int 39 | { 40 | return $this->index; 41 | } 42 | 43 | /** 44 | * Returns the real key of the element. 45 | * 46 | * Note that the value can be any ({@see mixed}) and may not necessarily 47 | * be compatible with PHP array keys ({@see int} or {@see string}). 48 | */ 49 | public function getKey(): mixed 50 | { 51 | return $this->key; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Exception/Mapping/IterableValueException.php: -------------------------------------------------------------------------------- 1 | $index 13 | * @param iterable $value 14 | */ 15 | public function __construct( 16 | protected readonly mixed $element, 17 | int $index, 18 | mixed $key, 19 | iterable $value, 20 | PathInterface $path, 21 | string $template, 22 | int $code = 0, 23 | ?\Throwable $previous = null, 24 | ) { 25 | parent::__construct( 26 | index: $index, 27 | key: $key, 28 | value: $value, 29 | path: $path, 30 | template: $template, 31 | code: $code, 32 | previous: $previous, 33 | ); 34 | } 35 | 36 | public function getElement(): mixed 37 | { 38 | return $this->element; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Exception/Mapping/MissingRequiredObjectFieldException.php: -------------------------------------------------------------------------------- 1 | |object $value 16 | */ 17 | public static function createFromPath( 18 | string $field, 19 | ?TypeStatement $expected, 20 | array|object $value, 21 | PathInterface $path, 22 | ?\Throwable $previous = null, 23 | ): self { 24 | $template = 'Object {{value}} requires missing field {{field}} of type {{expected}}'; 25 | 26 | return new self( 27 | field: $field, 28 | expected: $expected ?? self::mixedTypeStatement(), 29 | value: $value, 30 | path: $path, 31 | template: $template, 32 | previous: $previous, 33 | ); 34 | } 35 | 36 | /** 37 | * @param non-empty-string $field 38 | * @param iterable $value 39 | */ 40 | public static function createFromContext( 41 | string $field, 42 | ?TypeStatement $expected, 43 | array|object $value, 44 | Context $context, 45 | ?\Throwable $previous = null, 46 | ): self { 47 | return self::createFromPath( 48 | field: $field, 49 | expected: $expected, 50 | value: $value, 51 | path: $context->getPath(), 52 | previous: $previous, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Exception/Mapping/NonInstantiatableObjectException.php: -------------------------------------------------------------------------------- 1 | $value 15 | */ 16 | public function __construct( 17 | TypeStatement $expected, 18 | \ReflectionClass $value, 19 | PathInterface $path, 20 | string $template, 21 | int $code = 0, 22 | ?\Throwable $previous = null, 23 | ) { 24 | parent::__construct($expected, $value, $path, $template, $code, $previous); 25 | } 26 | 27 | /** 28 | * @param \ReflectionClass $value 29 | */ 30 | public static function createFromPath( 31 | ?TypeStatement $expected, 32 | \ReflectionClass $value, 33 | PathInterface $path, 34 | ?\Throwable $previous = null, 35 | ): self { 36 | $template = \sprintf('Unable to instantiate %s of {{expected}}', match (true) { 37 | $value->isAbstract() => 'abstract class', 38 | $value->isInterface() => 'interface', 39 | default => 'unknown non-instantiable type', 40 | }); 41 | 42 | return new self( 43 | expected: $expected ?? self::mixedTypeStatement(), 44 | value: $value, 45 | path: $path, 46 | template: $template, 47 | previous: $previous, 48 | ); 49 | } 50 | 51 | /** 52 | * @param \ReflectionClass $value 53 | */ 54 | public static function createFromContext( 55 | ?TypeStatement $expected, 56 | \ReflectionClass $value, 57 | Context $context, 58 | ?\Throwable $previous = null, 59 | ): self { 60 | return self::createFromPath( 61 | expected: $expected, 62 | value: $value, 63 | path: $context->getPath(), 64 | previous: $previous, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Exception/Mapping/ObjectException.php: -------------------------------------------------------------------------------- 1 | |object $value 16 | */ 17 | public function __construct( 18 | TypeStatement $expected, 19 | array|object $value, 20 | PathInterface $path, 21 | string $template, 22 | int $code = 0, 23 | ?\Throwable $previous = null, 24 | ) { 25 | parent::__construct( 26 | expected: $expected, 27 | value: $value, 28 | path: $path, 29 | template: $template, 30 | code: $code, 31 | previous: $previous, 32 | ); 33 | } 34 | 35 | protected static function mixedTypeStatement(): TypeStatement 36 | { 37 | return new NamedTypeNode('mixed'); 38 | } 39 | 40 | /** 41 | * Unlike {@see ValueException::getValue()}, this method must return 42 | * only {@see object} or {@see array}. 43 | * 44 | * @return array|object 45 | */ 46 | public function getValue(): array|object 47 | { 48 | /** @var array|object */ 49 | return $this->value; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Exception/Mapping/ObjectFieldException.php: -------------------------------------------------------------------------------- 1 | |object $value 14 | */ 15 | public function __construct( 16 | protected readonly mixed $field, 17 | TypeStatement $expected, 18 | array|object $value, 19 | PathInterface $path, 20 | string $template, 21 | int $code = 0, 22 | ?\Throwable $previous = null, 23 | ) { 24 | parent::__construct( 25 | expected: $expected, 26 | value: $value, 27 | path: $path, 28 | template: $template, 29 | code: $code, 30 | previous: $previous, 31 | ); 32 | } 33 | 34 | /** 35 | * Returns the field of an object-like value. 36 | * 37 | * Note that the value can be any ({@see mixed}) and may not necessarily 38 | * be compatible with PHP array keys ({@see int} or {@see string}). 39 | */ 40 | public function getField(): mixed 41 | { 42 | return $this->field; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Exception/Mapping/ObjectValueException.php: -------------------------------------------------------------------------------- 1 | |object $value 15 | */ 16 | public function __construct( 17 | protected readonly mixed $element, 18 | string $field, 19 | TypeStatement $expected, 20 | array|object $value, 21 | PathInterface $path, 22 | string $template, 23 | int $code = 0, 24 | ?\Throwable $previous = null, 25 | ) { 26 | parent::__construct( 27 | field: $field, 28 | expected: $expected, 29 | value: $value, 30 | path: $path, 31 | template: $template, 32 | code: $code, 33 | previous: $previous, 34 | ); 35 | } 36 | 37 | /** 38 | * Unlike {@see ObjectFieldException::getField()}, method 39 | * must return only non-empty {@see string}. 40 | * 41 | * @return non-empty-string 42 | */ 43 | public function getField(): string 44 | { 45 | /** @var non-empty-string */ 46 | return $this->field; 47 | } 48 | 49 | public function getElement(): mixed 50 | { 51 | return $this->element; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Exception/Mapping/RuntimeException.php: -------------------------------------------------------------------------------- 1 | path = clone $path; 26 | $this->message = $this->template = self::createTemplate( 27 | template: $template, 28 | context: $this, 29 | path: $path, 30 | ); 31 | } 32 | 33 | private static function createTemplate(string $template, \Throwable $context, PathInterface $path): Template 34 | { 35 | $suffix = ''; 36 | 37 | if (!$path->isEmpty()) { 38 | $suffix = ' at {{path}}'; 39 | } 40 | 41 | return new Template( 42 | template: $template . $suffix, 43 | context: $context, 44 | ); 45 | } 46 | 47 | /** 48 | * @template T of \Throwable 49 | * 50 | * @param T $e 51 | * 52 | * @return T 53 | */ 54 | public static function tryAdopt(\Throwable $e, Context $context): \Throwable 55 | { 56 | try { 57 | $property = new \ReflectionProperty($e, 'message'); 58 | $message = $property->getValue($e); 59 | 60 | if (!\is_string($message)) { 61 | return $e; 62 | } 63 | 64 | $property->setValue($e, (string) self::createTemplate( 65 | template: $message, 66 | context: $e, 67 | path: clone $context->getPath(), 68 | )); 69 | } catch (\Throwable) { 70 | return $e; 71 | } 72 | 73 | return $e; 74 | } 75 | 76 | public function getPath(): PathInterface 77 | { 78 | return $this->path; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Exception/Mapping/ValueException.php: -------------------------------------------------------------------------------- 1 | value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Exception/Mapping/ValueOfTypeException.php: -------------------------------------------------------------------------------- 1 | expected; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Mapping/DiscriminatorMap.php: -------------------------------------------------------------------------------- 1 | Admin::class, 11 | * 'moderator' => Moderator::class, 12 | * 'user' => User::class, 13 | * 'any' => 'array' 14 | * ])] 15 | * abstract class Account {} 16 | * 17 | * final class Admin extends Account {} 18 | * final class Moderator extends Account {} 19 | * final class User extends Account {} 20 | * ``` 21 | */ 22 | #[\Attribute(\Attribute::TARGET_CLASS)] 23 | class DiscriminatorMap 24 | { 25 | public function __construct( 26 | /** 27 | * The property holding the type discriminator 28 | * 29 | * @var non-empty-string 30 | */ 31 | public readonly string $field, 32 | /** 33 | * The mapping between field value and types, i.e. 34 | * 35 | * ``` 36 | * [ 37 | * 'admin_user' => AdminUser::class, 38 | * 'admin_users' => 'list', 39 | * ] 40 | * 41 | * @var non-empty-array 42 | */ 43 | public readonly array $map, 44 | /** 45 | * Default type if the discriminator field ({@see $field}) is missing 46 | * or does not match the mapping rules ({@see $map}) 47 | * 48 | * @var non-empty-string|null 49 | */ 50 | public readonly ?string $otherwise = null, 51 | ) {} 52 | } 53 | -------------------------------------------------------------------------------- /src/Mapping/Driver/CachedDriver.php: -------------------------------------------------------------------------------- 1 | $class 26 | * 27 | * @return non-empty-string 28 | */ 29 | protected function getKey(\ReflectionClass $class): string 30 | { 31 | return $this->prefix 32 | . self::getKeyValue($class) 33 | . self::getKeySuffix($class); 34 | } 35 | 36 | /** 37 | * @param \ReflectionClass $class 38 | * 39 | * @return non-empty-string 40 | */ 41 | protected static function getKeyValue(\ReflectionClass $class): string 42 | { 43 | return \str_replace(['\\', "\0"], '_', $class->getName()); 44 | } 45 | 46 | /** 47 | * @param \ReflectionClass $class 48 | */ 49 | protected static function getKeySuffix(\ReflectionClass $class): string 50 | { 51 | $pathname = $class->getFileName(); 52 | 53 | if ($pathname === false) { 54 | return ''; 55 | } 56 | 57 | return (string) \filemtime($pathname); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Mapping/Driver/DocBlockDriver/ClassPropertyTypeDriver.php: -------------------------------------------------------------------------------- 1 | getDocBlockFromProperty($property); 29 | 30 | foreach ($phpdoc as $tag) { 31 | if ($this->isExpectedVarTag($tag)) { 32 | /** @var VarTag $tag */ 33 | return $tag->getType(); 34 | } 35 | } 36 | 37 | return null; 38 | } 39 | 40 | private function isExpectedVarTag(TagInterface $tag): bool 41 | { 42 | return $tag instanceof VarTag 43 | && $tag->getName() === $this->varTagName; 44 | } 45 | 46 | private function getDocBlockFromProperty(\ReflectionProperty $property): DocBlock 47 | { 48 | $comment = $property->getDocComment(); 49 | 50 | if ($comment === false) { 51 | return new DocBlock(); 52 | } 53 | 54 | return $this->parser->parse($comment); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Mapping/Driver/DocBlockDriver/PromotedPropertyTypeDriver.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $constructors = []; 20 | 21 | /** 22 | * @param non-empty-string $paramTagName 23 | */ 24 | public function __construct( 25 | private readonly string $paramTagName, 26 | private readonly ClassPropertyTypeDriver $classProperties, 27 | private readonly ParserInterface $parser, 28 | ) {} 29 | 30 | /** 31 | * Return type for given property from docblock. 32 | */ 33 | public function findType(\ReflectionProperty $property, PropertyMetadata $meta): ?TypeStatement 34 | { 35 | $result = $this->classProperties->findType($property); 36 | 37 | if ($result !== null) { 38 | return $result; 39 | } 40 | 41 | $class = $property->getDeclaringClass(); 42 | 43 | $phpdoc = $this->constructors[$class->getName()] 44 | ??= $this->getDocBlockFromPromotedProperty($class); 45 | 46 | foreach ($phpdoc as $tag) { 47 | if ($this->isExpectedParamTag($tag, $meta)) { 48 | /** @var ParamTag $tag */ 49 | return $tag->getType(); 50 | } 51 | } 52 | 53 | return null; 54 | } 55 | 56 | /** 57 | * Cleanup memory after task completion. 58 | */ 59 | public function cleanup(): void 60 | { 61 | $this->constructors = []; 62 | } 63 | 64 | private function isExpectedParamTag(TagInterface $tag, PropertyMetadata $meta): bool 65 | { 66 | return $tag instanceof ParamTag 67 | && $tag->getName() === $this->paramTagName 68 | && $tag->getVariableName() === $meta->getName(); 69 | } 70 | 71 | /** 72 | * @param \ReflectionClass $class 73 | */ 74 | private function getDocBlockFromPromotedProperty(\ReflectionClass $class): DocBlock 75 | { 76 | $constructor = $class->getConstructor(); 77 | 78 | if ($constructor === null) { 79 | return new DocBlock(); 80 | } 81 | 82 | $comment = $constructor->getDocComment(); 83 | 84 | if ($comment === false) { 85 | return new DocBlock(); 86 | } 87 | 88 | return $this->parser->parse($comment); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Mapping/Driver/Driver.php: -------------------------------------------------------------------------------- 1 | delegate->getClassMetadata($class, $types, $parser); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mapping/Driver/DriverInterface.php: -------------------------------------------------------------------------------- 1 | $class 17 | * 18 | * @return ClassMetadata 19 | */ 20 | public function getClassMetadata( 21 | \ReflectionClass $class, 22 | TypeRepositoryInterface $types, 23 | TypeParserInterface $parser, 24 | ): ClassMetadata; 25 | } 26 | -------------------------------------------------------------------------------- /src/Mapping/Driver/InMemoryCachedDriver.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | private array $memory = []; 17 | 18 | public function getClassMetadata( 19 | \ReflectionClass $class, 20 | TypeRepositoryInterface $types, 21 | TypeParserInterface $parser, 22 | ): ClassMetadata { 23 | // @phpstan-ignore-next-line : class-string key contains ClassMetadata instance 24 | return $this->memory[$class->name] 25 | ??= parent::getClassMetadata($class, $types, $parser); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Mapping/Driver/LoadableDriver.php: -------------------------------------------------------------------------------- 1 | > 20 | */ 21 | private static array $metadata = []; 22 | 23 | public function __construct( 24 | DriverInterface $delegate = new NullDriver(), 25 | ) { 26 | parent::__construct($delegate); 27 | } 28 | 29 | /** 30 | * @template TArg of object 31 | * 32 | * @param \ReflectionClass $class 33 | * 34 | * @return ClassMetadata 35 | * @throws \Throwable in case of internal error occurred 36 | */ 37 | public function getClassMetadata( 38 | \ReflectionClass $class, 39 | TypeRepositoryInterface $types, 40 | TypeParserInterface $parser, 41 | ): ClassMetadata { 42 | if (isset(self::$metadata[$class->getName()])) { 43 | /** @var ClassMetadata */ 44 | return self::$metadata[$class->getName()]; 45 | } 46 | 47 | self::$metadata[$class->getName()] = $metadata 48 | = parent::getClassMetadata($class, $types, $parser); 49 | 50 | $this->load($class, $metadata, $types, $parser); 51 | 52 | try { 53 | return $metadata; 54 | } finally { 55 | self::$metadata = []; 56 | } 57 | } 58 | 59 | /** 60 | * @template TArg of object 61 | * 62 | * @param \ReflectionClass $reflection 63 | * @param ClassMetadata $class 64 | * 65 | * @throws DefinitionException in case of type cannot be defined 66 | * @throws \Throwable in case of internal error occurred 67 | */ 68 | abstract protected function load( 69 | \ReflectionClass $reflection, 70 | ClassMetadata $class, 71 | TypeRepositoryInterface $types, 72 | TypeParserInterface $parser, 73 | ): void; 74 | } 75 | -------------------------------------------------------------------------------- /src/Mapping/Driver/NullDriver.php: -------------------------------------------------------------------------------- 1 | getName()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Mapping/Driver/PHPConfigDriver.php: -------------------------------------------------------------------------------- 1 | > $config 13 | */ 14 | public function __construct( 15 | private readonly array $config, 16 | DriverInterface $delegate = new NullDriver(), 17 | ?ExpressionLanguage $expression = null, 18 | ) { 19 | parent::__construct($delegate, $expression); 20 | } 21 | 22 | protected function getConfiguration(\ReflectionClass $class): ?array 23 | { 24 | return $this->config[$class->name] ?? null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Mapping/Driver/PHPConfigFileDriver.php: -------------------------------------------------------------------------------- 1 | $class 24 | * 25 | * @return non-empty-string 26 | */ 27 | private function getFilename(\ReflectionClass $class): string 28 | { 29 | return \str_replace('\\', '.', $class->getName()) 30 | . '.php'; 31 | } 32 | 33 | /** 34 | * @param \ReflectionClass $class 35 | * 36 | * @return non-empty-string 37 | */ 38 | private function getPathname(\ReflectionClass $class): string 39 | { 40 | return $this->directory . '/' . $this->getFilename($class); 41 | } 42 | 43 | protected function getConfiguration(\ReflectionClass $class): ?array 44 | { 45 | $pathname = $this->getPathname($class); 46 | 47 | if (\is_file($pathname)) { 48 | \ob_start(); 49 | $result = require $pathname; 50 | \ob_end_clean(); 51 | 52 | if (!\is_array($result)) { 53 | throw new \InvalidArgumentException(\sprintf( 54 | 'Configuration file "%s" must contain array, but "%s" given', 55 | // @phpstan-ignore-next-line 56 | \realpath($pathname) ?: $pathname, 57 | \get_debug_type($result), 58 | )); 59 | } 60 | 61 | return $result; 62 | } 63 | 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Mapping/Driver/Psr16CachedDriver.php: -------------------------------------------------------------------------------- 1 | getKey($class); 29 | 30 | $result = $this->cache->get($index); 31 | 32 | if ($result instanceof ClassMetadata) { 33 | return $result; 34 | } 35 | 36 | $result = parent::getClassMetadata($class, $types, $parser); 37 | 38 | $this->cache->set($index, $result, $this->ttl); 39 | 40 | return $result; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Mapping/Introspection/ClassIntrospection.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | private readonly ClassMetadata $metadata, 23 | ) {} 24 | 25 | public function getTypeStatement(Context $context): TypeStatement 26 | { 27 | if (!$context->isDetailedTypes()) { 28 | return new NamedTypeNode($this->metadata->getName()); 29 | } 30 | 31 | $fields = []; 32 | 33 | foreach ($this->metadata->getProperties() as $property) { 34 | $fields[] = $this->getFieldNode($property, $context); 35 | } 36 | 37 | if ($fields === []) { 38 | return new NamedTypeNode($this->metadata->getName()); 39 | } 40 | 41 | return new NamedTypeNode($this->metadata->getName(), fields: new FieldsListNode($fields)); 42 | } 43 | 44 | private function getFieldNode(PropertyMetadata $metadata, Context $context): FieldNode 45 | { 46 | $name = $metadata->getName(); 47 | 48 | if ($context->isDenormalization()) { 49 | $name = $metadata->getExportName(); 50 | } 51 | 52 | return new NamedFieldNode( 53 | key: $name, 54 | of: $metadata->getTypeStatement($context), 55 | optional: $metadata->hasDefaultValue(), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Mapping/Introspection/IntrospectionInterface.php: -------------------------------------------------------------------------------- 1 | metadata->findTypeInfo(); 21 | 22 | if ($info === null) { 23 | return new NamedTypeNode('mixed'); 24 | } 25 | 26 | $statement = clone $info->getTypeStatement(); 27 | 28 | if ($context->isDetailedTypes() || !$statement instanceof NamedTypeNode) { 29 | return $statement; 30 | } 31 | 32 | return new NamedTypeNode($statement->name); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Mapping/MapName.php: -------------------------------------------------------------------------------- 1 | expression->getNodes(); 30 | 31 | return (bool) $nodes->evaluate([], [ 32 | $this->getContextVariableName() => $object, 33 | ]); 34 | } 35 | 36 | /** 37 | * @api 38 | * 39 | * @return non-empty-string 40 | */ 41 | public function getContextVariableName(): string 42 | { 43 | return $this->context; 44 | } 45 | 46 | /** 47 | * @api 48 | */ 49 | public function getExpression(): ParsedExpression 50 | { 51 | return $this->expression; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Mapping/Metadata/MatchConditionMetadata.php: -------------------------------------------------------------------------------- 1 | timestamp = $createdAt ?? $this->getCurrentTimestamp(); 14 | } 15 | 16 | private function getCurrentTimestamp(): int 17 | { 18 | try { 19 | $date = new \DateTimeImmutable(); 20 | 21 | return $date->getTimestamp(); 22 | } catch (\Throwable) { 23 | return 0; 24 | } 25 | } 26 | 27 | /** 28 | * Returns the metadata creation timestamp in seconds. 29 | * 30 | * @api 31 | */ 32 | public function getTimestamp(): int 33 | { 34 | return $this->timestamp; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Mapping/Metadata/NullConditionMetadata.php: -------------------------------------------------------------------------------- 1 | type; 23 | } 24 | 25 | public function getTypeStatement(): TypeStatement 26 | { 27 | return $this->statement; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mapping/NormalizeAsArray.php: -------------------------------------------------------------------------------- 1 | normalize(new ExampleClass( 23 | * id: 42, 24 | * name: 'Kirill', 25 | * )); 26 | * ``` 27 | * 28 | * @param non-empty-string|null $type 29 | * 30 | * @throws RuntimeException in case of runtime mapping exception occurs 31 | * @throws DefinitionException in case of type building exception occurs 32 | * @throws \Throwable in case of any internal error occurs 33 | */ 34 | public function normalize(mixed $value, ?string $type = null): mixed; 35 | 36 | /** 37 | * Returns {@see true} if the value can be normalized for the given type. 38 | * 39 | * In case that the type is specified as {@see null}, it is automatically 40 | * inferred from the passed value. 41 | * 42 | * @param non-empty-string|null $type 43 | * 44 | * @throws DefinitionException in case of type building exception occurs 45 | * @throws \Throwable in case of any internal error occurs 46 | */ 47 | public function isNormalizable(mixed $value, ?string $type = null): bool; 48 | } 49 | -------------------------------------------------------------------------------- /src/Platform/DelegatePlatform.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private readonly array $features; 17 | 18 | /** 19 | * @var list> 20 | */ 21 | private readonly array $types; 22 | 23 | /** 24 | * @param iterable> $types 25 | * @param iterable $features 26 | */ 27 | public function __construct( 28 | private readonly PlatformInterface $delegate, 29 | iterable $types = [], 30 | iterable $features = [], 31 | ) { 32 | $this->types = \array_values([...$types]); 33 | $this->features = \array_values([...$features]); 34 | } 35 | 36 | public function getName(): string 37 | { 38 | return $this->delegate->getName(); 39 | } 40 | 41 | public function getTypes(): iterable 42 | { 43 | yield from $this->types; 44 | yield from $this->delegate->getTypes(); 45 | } 46 | 47 | public function isFeatureSupported(GrammarFeature $feature): bool 48 | { 49 | return \in_array($feature, $this->features, true) 50 | || $this->delegate->isFeatureSupported($feature); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Platform/EmptyPlatform.php: -------------------------------------------------------------------------------- 1 | `. 31 | */ 32 | case Generics; 33 | 34 | /** 35 | * Enables logical union types such as `T | U`. 36 | */ 37 | case Union; 38 | 39 | /** 40 | * Enables logical intersection types such as `T & U`. 41 | */ 42 | case Intersection; 43 | 44 | /** 45 | * Enables list types such as `T[]`. 46 | */ 47 | case List; 48 | 49 | /** 50 | * Enables offset types such as `T[U]`. 51 | */ 52 | case Offsets; 53 | 54 | /** 55 | * Enables or disables support for template argument 56 | * hints such as `T`. 57 | */ 58 | case Hints; 59 | 60 | /** 61 | * Enables or disables support for attributes such as `#[attr]`. 62 | */ 63 | case Attributes; 64 | } 65 | -------------------------------------------------------------------------------- /src/Platform/Platform.php: -------------------------------------------------------------------------------- 1 | driver = new InMemoryCachedDriver( 21 | delegate: $driver ?? $this->createDefaultMetadataDriver(), 22 | ); 23 | } 24 | 25 | protected function createDefaultMetadataDriver(): DriverInterface 26 | { 27 | return new AttributeDriver( 28 | delegate: new ReflectionDriver(), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Platform/PlatformInterface.php: -------------------------------------------------------------------------------- 1 | denormalize(0xDEAD_BEEF); 33 | * 34 | * // What does an expression like this mean 35 | * $mapper->denormalize(0xDEAD_BEEF, 'int'); 36 | * 37 | * // In the case of the 'int' type is not registered, 38 | * // the mapper will throw an exception. 39 | * ``` 40 | * 41 | * @return iterable> 42 | */ 43 | public function getTypes(): iterable; 44 | 45 | /** 46 | * Returns {@see true} in case of feature is supported. 47 | * 48 | * Each flag defines a set of language ({@link https://typelang.dev/basic-types.html}) 49 | * constructs supported by the platform. For example, you can disable 50 | * literals, nullable types or anything else. 51 | */ 52 | public function isFeatureSupported(GrammarFeature $feature): bool; 53 | } 54 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/BoolLiteralTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class BoolLiteralTypeBuilder implements TypeBuilderInterface 17 | { 18 | public function isSupported(TypeStatement $statement): bool 19 | { 20 | return $statement instanceof BoolLiteralNode; 21 | } 22 | 23 | public function build( 24 | TypeStatement $statement, 25 | TypeRepositoryInterface $types, 26 | TypeParserInterface $parser, 27 | ): BoolLiteralType { 28 | return new BoolLiteralType($statement->value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/CallableTypeBuilder.php: -------------------------------------------------------------------------------- 1 | new CustomString(), 21 | * ); 22 | * ``` 23 | * 24 | * @template-extends NamedTypeBuilder 25 | */ 26 | class CallableTypeBuilder extends NamedTypeBuilder 27 | { 28 | /** 29 | * @param non-empty-array|non-empty-string $names 30 | * @param \Closure(): TypeInterface $factory 31 | */ 32 | public function __construct( 33 | array|string $names, 34 | protected readonly \Closure $factory, 35 | ) { 36 | parent::__construct($names); 37 | } 38 | 39 | public function build( 40 | TypeStatement $statement, 41 | TypeRepositoryInterface $types, 42 | TypeParserInterface $parser, 43 | ): TypeInterface { 44 | $this->expectNoShapeFields($statement); 45 | $this->expectNoTemplateArguments($statement); 46 | 47 | try { 48 | return ($this->factory)(); 49 | } catch (\Throwable $e) { 50 | throw InternalTypeException::becauseInternalTypeErrorOccurs( 51 | type: $statement, 52 | message: 'An error occurred while trying to fetch {{type}} type from callback', 53 | previous: $e, 54 | ); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/ClassTypeBuilder.php: -------------------------------------------------------------------------------- 1 | > 25 | */ 26 | class ClassTypeBuilder extends Builder 27 | { 28 | public function __construct( 29 | protected readonly DriverInterface $driver = new ReflectionDriver(), 30 | protected readonly PropertyAccessorInterface $accessor = new ReflectionPropertyAccessor(), 31 | protected readonly ClassInstantiatorInterface $instantiator = new ReflectionClassInstantiator(), 32 | ) {} 33 | 34 | /** 35 | * Returns {@see true} if the type contains a reference to an existing class. 36 | */ 37 | public function isSupported(TypeStatement $statement): bool 38 | { 39 | if (!$statement instanceof NamedTypeNode) { 40 | return false; 41 | } 42 | 43 | /** @var non-empty-string $name */ 44 | $name = $statement->name->toString(); 45 | 46 | if (!\class_exists($name)) { 47 | return false; 48 | } 49 | 50 | $reflection = new \ReflectionClass($name); 51 | 52 | return $reflection->isInstantiable() 53 | // Allow abstract classes for discriminators 54 | || $reflection->isAbstract() 55 | // Allow interfaces for discriminators 56 | || $reflection->isInterface(); 57 | } 58 | 59 | public function build( 60 | TypeStatement $statement, 61 | TypeRepositoryInterface $types, 62 | TypeParserInterface $parser, 63 | ): ClassType { 64 | $this->expectNoShapeFields($statement); 65 | $this->expectNoTemplateArguments($statement); 66 | 67 | /** @var class-string $class */ 68 | $class = $statement->name->toString(); 69 | 70 | return new ClassType( 71 | metadata: $this->driver->getClassMetadata( 72 | class: new \ReflectionClass($class), 73 | types: $types, 74 | parser: $parser, 75 | ), 76 | accessor: $this->accessor, 77 | instantiator: $this->instantiator, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/DateTimeTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class DateTimeTypeBuilder extends Builder 20 | { 21 | public function isSupported(TypeStatement $statement): bool 22 | { 23 | return $statement instanceof NamedTypeNode 24 | && \is_a($statement->name->toLowerString(), \DateTimeInterface::class, true); 25 | } 26 | 27 | /** 28 | * @return class-string<\DateTime|\DateTimeImmutable> 29 | */ 30 | private function getDateTimeClass(string $name): string 31 | { 32 | if ($name === \DateTimeInterface::class || \interface_exists($name)) { 33 | return \DateTimeImmutable::class; 34 | } 35 | 36 | /** @var class-string<\DateTime> */ 37 | return $name; 38 | } 39 | 40 | public function build( 41 | TypeStatement $statement, 42 | TypeRepositoryInterface $types, 43 | TypeParserInterface $parser, 44 | ): DateTimeType { 45 | $this->expectNoShapeFields($statement); 46 | $this->expectTemplateArgumentsLessOrEqualThan($statement, 1, 0); 47 | 48 | if ($statement->arguments === null) { 49 | return new DateTimeType( 50 | class: $this->getDateTimeClass( 51 | name: $statement->name->toString(), 52 | ), 53 | ); 54 | } 55 | 56 | /** @var TemplateArgumentNode $formatArgument */ 57 | $formatArgument = $statement->arguments->first(); 58 | 59 | $this->expectNoTemplateArgumentHint($statement, $formatArgument); 60 | 61 | if (!$formatArgument->value instanceof StringLiteralNode) { 62 | throw InvalidTemplateArgumentException::becauseTemplateArgumentIsInvalid( 63 | expected: new NamedTypeNode('string'), 64 | argument: $formatArgument, 65 | type: $statement, 66 | ); 67 | } 68 | 69 | return new DateTimeType( 70 | class: $this->getDateTimeClass( 71 | name: $statement->name->toString(), 72 | ), 73 | format: $formatArgument->value->value, 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/FloatLiteralTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class FloatLiteralTypeBuilder implements TypeBuilderInterface 17 | { 18 | public function isSupported(TypeStatement $statement): bool 19 | { 20 | return $statement instanceof FloatLiteralNode; 21 | } 22 | 23 | public function build( 24 | TypeStatement $statement, 25 | TypeRepositoryInterface $types, 26 | TypeParserInterface $parser, 27 | ): FloatLiteralType { 28 | return new FloatLiteralType($statement->value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/IntLiteralTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class IntLiteralTypeBuilder implements TypeBuilderInterface 17 | { 18 | public function isSupported(TypeStatement $statement): bool 19 | { 20 | return $statement instanceof IntLiteralNode; 21 | } 22 | 23 | public function build( 24 | TypeStatement $statement, 25 | TypeRepositoryInterface $types, 26 | TypeParserInterface $parser, 27 | ): IntLiteralType { 28 | return new IntLiteralType($statement->value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/NamedTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class NamedTypeBuilder extends Builder 16 | { 17 | /** 18 | * @var non-empty-list 19 | */ 20 | protected readonly array $lower; 21 | 22 | /** 23 | * @param non-empty-array|non-empty-string $names 24 | */ 25 | public function __construct(array|string $names) 26 | { 27 | $this->lower = $this->formatNames($names); 28 | } 29 | 30 | /** 31 | * @param non-empty-array|non-empty-string $names 32 | * 33 | * @return non-empty-list 34 | */ 35 | private function formatNames(array|string $names): array 36 | { 37 | $result = []; 38 | 39 | foreach (\is_string($names) ? [$names] : $names as $name) { 40 | $result[] = \strtolower($name); 41 | } 42 | 43 | return $result; 44 | } 45 | 46 | public function isSupported(TypeStatement $statement): bool 47 | { 48 | return $statement instanceof NamedTypeNode 49 | && \in_array($statement->name->toLowerString(), $this->lower, true); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/NullTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class NullTypeBuilder extends Builder 20 | { 21 | public function isSupported(TypeStatement $statement): bool 22 | { 23 | if ($statement instanceof NullLiteralNode) { 24 | return true; 25 | } 26 | 27 | return $statement instanceof NamedTypeNode 28 | && $statement->name->toLowerString() === 'null'; 29 | } 30 | 31 | /** 32 | * @throws ShapeFieldsNotSupportedException 33 | * @throws TemplateArgumentsNotSupportedException 34 | */ 35 | public function build( 36 | TypeStatement $statement, 37 | TypeRepositoryInterface $types, 38 | TypeParserInterface $parser, 39 | ): NullType { 40 | if ($statement instanceof NullLiteralNode) { 41 | return new NullType(); 42 | } 43 | 44 | $this->expectNoShapeFields($statement); 45 | $this->expectNoTemplateArguments($statement); 46 | 47 | return new NullType(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/NullableTypeBuilder.php: -------------------------------------------------------------------------------- 1 | , NullableType> 17 | */ 18 | class NullableTypeBuilder implements TypeBuilderInterface 19 | { 20 | public function isSupported(TypeStatement $statement): bool 21 | { 22 | return $statement instanceof NullableTypeNode; 23 | } 24 | 25 | public function build( 26 | TypeStatement $statement, 27 | TypeRepositoryInterface $types, 28 | TypeParserInterface $parser, 29 | ): NullableType { 30 | $type = $types->getTypeByStatement($statement->type); 31 | 32 | return new NullableType($type); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/ObjectTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ObjectTypeBuilder extends NamedTypeBuilder 16 | { 17 | public function build( 18 | TypeStatement $statement, 19 | TypeRepositoryInterface $types, 20 | TypeParserInterface $parser, 21 | ): ObjectType { 22 | $this->expectNoShapeFields($statement); 23 | $this->expectNoTemplateArguments($statement); 24 | 25 | return new ObjectType(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/PsrContainerTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class PsrContainerTypeBuilder extends NamedTypeBuilder 31 | { 32 | /** 33 | * @param non-empty-array|non-empty-string $names 34 | * @param class-string $serviceId 35 | */ 36 | public function __construct( 37 | array|string $names, 38 | protected readonly string $serviceId, 39 | protected readonly ContainerInterface $container, 40 | ) { 41 | parent::__construct($names); 42 | } 43 | 44 | public function build( 45 | TypeStatement $statement, 46 | TypeRepositoryInterface $types, 47 | TypeParserInterface $parser, 48 | ): TypeInterface { 49 | $this->expectNoShapeFields($statement); 50 | $this->expectNoTemplateArguments($statement); 51 | 52 | try { 53 | $service = $this->container->get($this->serviceId); 54 | } catch (\Throwable $e) { 55 | throw InternalTypeException::becauseInternalTypeErrorOccurs( 56 | type: $statement, 57 | message: 'An error occurred while trying to fetch {{type}} type from service container', 58 | previous: $e, 59 | ); 60 | } 61 | 62 | if (!$service instanceof TypeInterface) { 63 | throw InternalTypeException::becauseInternalTypeErrorOccurs( 64 | type: $statement, 65 | message: \sprintf( 66 | 'Received service from service container defined as {{type}} must be instanceof %s, but %s given', 67 | TypeInterface::class, 68 | \get_debug_type($service), 69 | ), 70 | ); 71 | } 72 | 73 | return $service; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/SimpleTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class SimpleTypeBuilder extends NamedTypeBuilder 18 | { 19 | /** 20 | * @param non-empty-array|non-empty-string $names 21 | * @param class-string $type 22 | */ 23 | public function __construct( 24 | array|string $names, 25 | protected readonly string $type, 26 | ) { 27 | parent::__construct($names); 28 | } 29 | 30 | /** 31 | * @throws ShapeFieldsNotSupportedException 32 | * @throws TemplateArgumentsNotSupportedException 33 | */ 34 | public function build( 35 | TypeStatement $statement, 36 | TypeRepositoryInterface $types, 37 | TypeParserInterface $parser, 38 | ): TypeInterface { 39 | $this->expectNoShapeFields($statement); 40 | $this->expectNoTemplateArguments($statement); 41 | 42 | return new ($this->type)(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/TypeBuilderInterface.php: -------------------------------------------------------------------------------- 1 | , ArrayType> 17 | */ 18 | class TypesListBuilder implements TypeBuilderInterface 19 | { 20 | public function isSupported(TypeStatement $statement): bool 21 | { 22 | return $statement instanceof TypesListNode; 23 | } 24 | 25 | public function build( 26 | TypeStatement $statement, 27 | TypeRepositoryInterface $types, 28 | TypeParserInterface $parser, 29 | ): ArrayType { 30 | $type = $types->getTypeByStatement($statement->type); 31 | 32 | return new ArrayType($type); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/UnionTypeBuilder.php: -------------------------------------------------------------------------------- 1 | , TypeInterface> 18 | */ 19 | class UnionTypeBuilder implements TypeBuilderInterface 20 | { 21 | public function isSupported(TypeStatement $statement): bool 22 | { 23 | return $statement instanceof UnionTypeNode; 24 | } 25 | 26 | public function build( 27 | TypeStatement $statement, 28 | TypeRepositoryInterface $types, 29 | TypeParserInterface $parser, 30 | ): TypeInterface { 31 | $result = []; 32 | $nullable = false; 33 | 34 | foreach ($statement->statements as $leaf) { 35 | if ($leaf instanceof NullLiteralNode) { 36 | $nullable = true; 37 | } else { 38 | $result[] = $types->getTypeByStatement($leaf); 39 | } 40 | } 41 | 42 | $result = match (\count($result)) { 43 | 0 => throw new \InvalidArgumentException('Invalid union leaves'), 44 | 1 => \reset($result), 45 | default => new UnionType($result), 46 | }; 47 | 48 | if ($nullable === true) { 49 | return new NullableType($result); 50 | } 51 | 52 | return $result; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Platform/Standard/Builder/UnitEnumTypeBuilder.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class UnitEnumTypeBuilder extends Builder 18 | { 19 | /** 20 | * @var non-empty-lowercase-string 21 | */ 22 | public const DEFAULT_INNER_SCALAR_TYPE = 'string'; 23 | 24 | /** 25 | * @param non-empty-string $type 26 | */ 27 | public function __construct( 28 | protected readonly string $type = self::DEFAULT_INNER_SCALAR_TYPE, 29 | ) {} 30 | 31 | public function isSupported(TypeStatement $statement): bool 32 | { 33 | if (!$statement instanceof NamedTypeNode) { 34 | return false; 35 | } 36 | 37 | /** @var non-empty-string $enum */ 38 | $enum = $statement->name->toString(); 39 | 40 | return \enum_exists($statement->name->toString()) 41 | && !\is_subclass_of($enum, \BackedEnum::class); 42 | } 43 | 44 | public function build( 45 | TypeStatement $statement, 46 | TypeRepositoryInterface $types, 47 | TypeParserInterface $parser, 48 | ): UnitEnumType { 49 | $this->expectNoShapeFields($statement); 50 | $this->expectNoTemplateArguments($statement); 51 | 52 | $names = \iterator_to_array($this->getEnumCaseNames($statement), false); 53 | 54 | if ($names === []) { 55 | throw InternalTypeException::becauseInternalTypeErrorOccurs( 56 | type: $statement, 57 | message: 'The "{{type}}" enum requires at least one case', 58 | ); 59 | } 60 | 61 | return new UnitEnumType( 62 | // @phpstan-ignore-next-line 63 | class: $statement->name->toString(), 64 | cases: $names, 65 | type: $types->getTypeByStatement( 66 | statement: $parser->getStatementByDefinition( 67 | definition: $this->type, 68 | ), 69 | ), 70 | ); 71 | } 72 | 73 | /** 74 | * @return \Traversable 75 | */ 76 | private function getEnumCaseNames(NamedTypeNode $statement): \Traversable 77 | { 78 | /** @var class-string<\UnitEnum> $enum */ 79 | $enum = $statement->name->toString(); 80 | 81 | foreach ($enum::cases() as $case) { 82 | // @phpstan-ignore-next-line : Enum case name cannot be empty 83 | yield $case->name; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/ArrayKeyType.php: -------------------------------------------------------------------------------- 1 | int->match($value, $context) 29 | || $this->string->match($value, $context); 30 | } 31 | 32 | public function cast(mixed $value, Context $context): string|int 33 | { 34 | // PHP does not support zero ("0") string array keys, 35 | // so we need to force-cast it to the integer value. 36 | if ($value === '0') { 37 | return 0; 38 | } 39 | 40 | if (\is_string($value) || \is_int($value)) { 41 | /** @var string|int */ 42 | return $value; 43 | } 44 | 45 | if (!$context->isStrictTypesEnabled()) { 46 | try { 47 | /** @var int */ 48 | return $this->int->cast($value, $context); 49 | } catch (InvalidValueException) { 50 | // NaN, -INF and INF cannot be converted to 51 | // array-key implicitly without losses. 52 | if (\is_float($value) && !\is_finite($value)) { 53 | throw InvalidValueException::createFromContext( 54 | value: $value, 55 | context: $context, 56 | ); 57 | } 58 | 59 | /** @var string */ 60 | return $this->string->cast($value, $context); 61 | } 62 | } 63 | 64 | throw InvalidValueException::createFromContext( 65 | value: $value, 66 | context: $context, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/ArrayType.php: -------------------------------------------------------------------------------- 1 | isDenormalization()) { 26 | return \is_array($value); 27 | } 28 | 29 | return \is_iterable($value); 30 | } 31 | 32 | /** 33 | * @return array 34 | * @throws InvalidValueException in case the value is incorrect 35 | * @throws InvalidIterableKeyException in case the key of a certain element is incorrect 36 | * @throws InvalidIterableValueException in case the value of a certain element is incorrect 37 | * @throws \Throwable in case of internal error occurs 38 | */ 39 | public function cast(mixed $value, Context $context): array 40 | { 41 | if (!$this->match($value, $context)) { 42 | throw InvalidValueException::createFromContext( 43 | value: $value, 44 | context: $context, 45 | ); 46 | } 47 | 48 | $result = []; 49 | $index = 0; 50 | 51 | /** @var iterable $value */ 52 | foreach ($value as $key => $item) { 53 | try { 54 | $key = $this->key->cast($key, $context); 55 | } catch (InvalidValueException $e) { 56 | throw InvalidIterableKeyException::createFromContext( 57 | index: $index, 58 | key: $key, 59 | value: $value, 60 | context: $context, 61 | previous: $e, 62 | ); 63 | } 64 | 65 | // Not supported by PHP 66 | if (!\is_string($key) && !\is_int($key)) { 67 | throw InvalidIterableKeyException::createFromContext( 68 | index: $index, 69 | key: $key, 70 | value: $value, 71 | context: $context, 72 | ); 73 | } 74 | 75 | $entrance = $context->enter($item, new ArrayIndexEntry($key)); 76 | 77 | try { 78 | $result[$key] = $this->value->cast($item, $entrance); 79 | } catch (InvalidValueException $e) { 80 | throw InvalidIterableValueException::createFromContext( 81 | element: $item, 82 | index: $index, 83 | key: $key, 84 | value: $value, 85 | context: $entrance, 86 | previous: $e, 87 | ); 88 | } 89 | 90 | ++$index; 91 | } 92 | 93 | return $result; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/AsymmetricType.php: -------------------------------------------------------------------------------- 1 | isDenormalization()) { 27 | return $this->denormalizer->match($value, $context); 28 | } 29 | 30 | return $this->normalizer->match($value, $context); 31 | } 32 | 33 | public function cast(mixed $value, Context $context): mixed 34 | { 35 | if ($context->isDenormalization()) { 36 | return $this->denormalizer->cast($value, $context); 37 | } 38 | 39 | return $this->normalizer->cast($value, $context); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/BackedEnumType.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class BackedEnumType extends AsymmetricType 14 | { 15 | /** 16 | * @param class-string<\BackedEnum> $class 17 | */ 18 | public function __construct(string $class, TypeInterface $type) 19 | { 20 | parent::__construct( 21 | normalizer: new BackedEnumTypeNormalizer( 22 | class: $class, 23 | ), 24 | denormalizer: new BackedEnumTypeDenormalizer( 25 | class: $class, 26 | type: $type, 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/BackedEnumType/BackedEnumTypeDenormalizer.php: -------------------------------------------------------------------------------- 1 | $class 15 | */ 16 | public function __construct( 17 | protected readonly string $class, 18 | protected readonly TypeInterface $type, 19 | ) {} 20 | 21 | public function match(mixed $value, Context $context): bool 22 | { 23 | $isSupportsType = $this->type->match($value, $context); 24 | 25 | if (!$isSupportsType) { 26 | return false; 27 | } 28 | 29 | /** @var int|string $denormalized */ 30 | $denormalized = $this->type->cast($value, $context); 31 | 32 | try { 33 | return ($this->class)::tryFrom($denormalized) !== null; 34 | } catch (\Throwable) { 35 | return false; 36 | } 37 | } 38 | 39 | public function cast(mixed $value, Context $context): \BackedEnum 40 | { 41 | $denormalized = $this->type->cast($value, $context); 42 | 43 | if (!\is_string($denormalized) && !\is_int($denormalized)) { 44 | throw InvalidValueException::createFromContext( 45 | value: $value, 46 | context: $context, 47 | ); 48 | } 49 | 50 | try { 51 | $case = $this->class::tryFrom($denormalized); 52 | } catch (\TypeError $e) { 53 | throw InvalidValueException::createFromContext( 54 | value: $value, 55 | context: $context, 56 | previous: $e, 57 | ); 58 | } 59 | 60 | return $case ?? throw InvalidValueException::createFromContext( 61 | value: $value, 62 | context: $context, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/BackedEnumType/BackedEnumTypeNormalizer.php: -------------------------------------------------------------------------------- 1 | $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 | public function cast(mixed $value, Context $context): int|string 26 | { 27 | if (!$value instanceof $this->class) { 28 | throw InvalidValueException::createFromContext( 29 | value: $value, 30 | context: $context, 31 | ); 32 | } 33 | 34 | return $value->value; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/BoolLiteralType.php: -------------------------------------------------------------------------------- 1 | value; 20 | } 21 | 22 | #[\Override] 23 | public function cast(mixed $value, Context $context): bool 24 | { 25 | if ($value === $this->value) { 26 | return $value; 27 | } 28 | 29 | if (!$context->isStrictTypesEnabled() 30 | && $this->convertToBool($value) === $this->value) { 31 | return $this->value; 32 | } 33 | 34 | throw InvalidValueException::createFromContext( 35 | value: $value, 36 | context: $context, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/BoolType.php: -------------------------------------------------------------------------------- 1 | isStrictTypesEnabled()) { 29 | return $this->convertToBool($value); 30 | } 31 | 32 | throw InvalidValueException::createFromContext( 33 | value: $value, 34 | context: $context, 35 | ); 36 | } 37 | 38 | protected function convertToBool(mixed $value): bool 39 | { 40 | // 41 | // Each value should be checked EXPLICITLY, instead 42 | // of converting to a bool like `(bool) $value`. 43 | // 44 | // This will avoid implicit behavior, such as when an empty 45 | // SimpleXMLElement is cast to false, instead of being 46 | // converted to true like any other object: 47 | // 48 | // ``` 49 | // (bool) new \SimpleXMLElement(''); // -> false (WTF?) 50 | // ``` 51 | // 52 | return $value !== '' 53 | && $value !== '0' 54 | && $value !== [] 55 | && $value !== null 56 | && $value !== 0 57 | && $value !== 0.0 58 | && $value !== false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/ClassType.php: -------------------------------------------------------------------------------- 1 | , ClassTypeDenormalizer> 16 | */ 17 | class ClassType extends AsymmetricType 18 | { 19 | /** 20 | * @param ClassMetadata $metadata 21 | */ 22 | public function __construct( 23 | ClassMetadata $metadata, 24 | PropertyAccessorInterface $accessor, 25 | ClassInstantiatorInterface $instantiator, 26 | ) { 27 | parent::__construct( 28 | normalizer: new ClassTypeNormalizer( 29 | metadata: $metadata, 30 | accessor: $accessor, 31 | ), 32 | denormalizer: new ClassTypeDenormalizer( 33 | metadata: $metadata, 34 | accessor: $accessor, 35 | instantiator: $instantiator, 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/DateTimeType.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DateTimeType extends AsymmetricType 14 | { 15 | /** 16 | * @param class-string<\DateTime|\DateTimeImmutable> $class 17 | */ 18 | public function __construct(string $class, ?string $format = null) 19 | { 20 | parent::__construct( 21 | normalizer: new DateTimeTypeNormalizer( 22 | format: $format ?? DateTimeTypeNormalizer::DEFAULT_DATETIME_FORMAT, 23 | ), 24 | denormalizer: new DateTimeTypeDenormalizer( 25 | class: $class, 26 | format: $format, 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/DateTimeType/DateTimeTypeDenormalizer.php: -------------------------------------------------------------------------------- 1 | $class 15 | */ 16 | public function __construct( 17 | protected readonly string $class, 18 | protected readonly ?string $format = null, 19 | ) {} 20 | 21 | public function match(mixed $value, Context $context): bool 22 | { 23 | if (!\is_string($value)) { 24 | return false; 25 | } 26 | 27 | try { 28 | return $this->tryParseDateTime($value) !== null; 29 | } catch (\Throwable) { 30 | return false; 31 | } 32 | } 33 | 34 | public function cast(mixed $value, Context $context): \DateTimeInterface 35 | { 36 | if (!\is_string($value)) { 37 | throw InvalidValueException::createFromContext( 38 | value: $value, 39 | context: $context, 40 | ); 41 | } 42 | 43 | $result = $this->tryParseDateTime($value); 44 | 45 | if ($result instanceof \DateTimeInterface) { 46 | return $result; 47 | } 48 | 49 | throw InvalidValueException::createFromContext( 50 | value: $value, 51 | context: $context, 52 | ); 53 | } 54 | 55 | private function tryParseDateTime(string $value): ?\DateTimeInterface 56 | { 57 | if ($this->format !== null) { 58 | try { 59 | $result = ($this->class)::createFromFormat($this->format, $value); 60 | } catch (\Throwable) { 61 | return null; 62 | } 63 | 64 | return \is_bool($result) ? null : $result; 65 | } 66 | 67 | try { 68 | return new $this->class($value); 69 | } catch (\Throwable) { 70 | return null; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/DateTimeType/DateTimeTypeNormalizer.php: -------------------------------------------------------------------------------- 1 | format($this->format); 31 | } 32 | 33 | throw InvalidValueException::createFromContext( 34 | value: $value, 35 | context: $context, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/FloatLiteralType.php: -------------------------------------------------------------------------------- 1 | expected = (float) $value; 17 | } 18 | 19 | public function match(mixed $value, Context $context): bool 20 | { 21 | if (\is_int($value)) { 22 | return (float) $value === $this->expected; 23 | } 24 | 25 | return $value === $this->expected; 26 | } 27 | 28 | public function cast(mixed $value, Context $context): float 29 | { 30 | if ($this->match($value, $context)) { 31 | /** @var float|int $value */ 32 | return (float) $value; 33 | } 34 | 35 | throw InvalidValueException::createFromContext( 36 | value: $value, 37 | context: $context, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/FloatType.php: -------------------------------------------------------------------------------- 1 | value; 19 | } 20 | 21 | public function cast(mixed $value, Context $context): int 22 | { 23 | if ($value === $this->value) { 24 | /** @var int */ 25 | return $value; 26 | } 27 | 28 | throw InvalidValueException::createFromContext( 29 | value: $value, 30 | context: $context, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/IntRangeType.php: -------------------------------------------------------------------------------- 1 | = $this->min 24 | && $value <= $this->max; 25 | } 26 | 27 | public function cast(mixed $value, Context $context): int 28 | { 29 | if ($this->match($value, $context)) { 30 | /** @var int */ 31 | return $value; 32 | } 33 | 34 | throw InvalidValueException::createFromContext( 35 | value: $value, 36 | context: $context, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/IntType.php: -------------------------------------------------------------------------------- 1 | $value, 21 | !$context->isStrictTypesEnabled() => $this->convertToInt($value, $context), 22 | default => throw InvalidValueException::createFromContext( 23 | value: $value, 24 | context: $context, 25 | ), 26 | }; 27 | } 28 | 29 | /** 30 | * @throws InvalidValueException 31 | */ 32 | protected function convertToInt(mixed $value, Context $context): int 33 | { 34 | if ($value instanceof \BackedEnum && \is_int($value->value)) { 35 | $value = $value->value; 36 | } 37 | 38 | return match (true) { 39 | \is_int($value) => $value, 40 | $value === false, 41 | $value === null => 0, 42 | $value === true => 1, 43 | // Check that the conversion to int does not lose precision. 44 | \is_numeric($value) && (float) (int) $value === (float) $value => (int) $value, 45 | default => throw InvalidValueException::createFromContext( 46 | value: $value, 47 | context: $context, 48 | ), 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/ListType.php: -------------------------------------------------------------------------------- 1 | isDenormalization()) { 24 | return \is_array($value) && \array_is_list($value); 25 | } 26 | 27 | return \is_iterable($value); 28 | } 29 | 30 | /** 31 | * @return list 32 | * @throws InvalidValueException in case the value is incorrect 33 | * @throws InvalidIterableValueException in case the value of a certain element is incorrect 34 | * @throws \Throwable in case of internal error occurs 35 | */ 36 | public function cast(mixed $value, Context $context): array 37 | { 38 | if (!$this->match($value, $context)) { 39 | throw InvalidValueException::createFromContext( 40 | value: $value, 41 | context: $context, 42 | ); 43 | } 44 | 45 | $result = []; 46 | $index = 0; 47 | 48 | /** @var iterable $value */ 49 | foreach ($value as $key => $item) { 50 | $entrance = $context->enter($item, new ArrayIndexEntry($index)); 51 | 52 | try { 53 | $result[] = $this->value->cast($item, $entrance); 54 | } catch (InvalidValueException $e) { 55 | throw InvalidIterableValueException::createFromContext( 56 | element: $item, 57 | index: $index, 58 | key: $key, 59 | value: $value, 60 | context: $entrance, 61 | previous: $e, 62 | ); 63 | } 64 | 65 | ++$index; 66 | } 67 | 68 | return $result; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/MixedType.php: -------------------------------------------------------------------------------- 1 | getTypeByValue($value); 19 | 20 | return $type->cast($value, $context); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/NullType.php: -------------------------------------------------------------------------------- 1 | parent->match($value, $context); 19 | } 20 | 21 | public function cast(mixed $value, Context $context): mixed 22 | { 23 | if ($value === null) { 24 | return null; 25 | } 26 | 27 | return $this->parent->cast($value, $context); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/ObjectType.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class ObjectType extends AsymmetricType 14 | { 15 | public function __construct() 16 | { 17 | parent::__construct( 18 | normalizer: new ObjectTypeNormalizer(), 19 | denormalizer: new ObjectTypeDenormalizer(), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/ObjectType/ObjectTypeDenormalizer.php: -------------------------------------------------------------------------------- 1 | |object 20 | * @throws InvalidValueException in case the value is incorrect 21 | */ 22 | public function cast(mixed $value, Context $context): array|object 23 | { 24 | if (!\is_object($value)) { 25 | throw InvalidValueException::createFromContext( 26 | value: $value, 27 | context: $context, 28 | ); 29 | } 30 | 31 | $result = \get_object_vars($value); 32 | 33 | if ($context->isObjectsAsArrays()) { 34 | return $result; 35 | } 36 | 37 | return (object) $result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/StringType.php: -------------------------------------------------------------------------------- 1 | isStrictTypesEnabled()) { 35 | return $this->convertToString($value, $context); 36 | } 37 | 38 | throw InvalidValueException::createFromContext( 39 | value: $value, 40 | context: $context, 41 | ); 42 | } 43 | 44 | /** 45 | * @throws InvalidValueException 46 | */ 47 | protected function convertToString(mixed $value, Context $context): string 48 | { 49 | return match (true) { 50 | // Null 51 | $value === null => static::NULL_TO_STRING, 52 | // True 53 | $value === true => static::TRUE_TO_STRING, 54 | // False 55 | $value === false => static::FALSE_TO_STRING, 56 | // Float 57 | \is_float($value) => match (true) { 58 | // NaN 59 | \is_nan($value) => static::NAN_TO_STRING, 60 | // Infinity 61 | $value === \INF => static::INF_TO_STRING, 62 | $value === -\INF => '-' . static::INF_TO_STRING, 63 | // Non-zero float number 64 | \str_contains($result = (string) $value, '.') => $result, 65 | // Integer-like float number 66 | default => \number_format($value, 1, '.', ''), 67 | }, 68 | // Int 69 | \is_int($value), 70 | // Stringable 71 | $value instanceof \Stringable => (string) $value, 72 | // Enum 73 | $value instanceof \BackedEnum => (string) $value->value, 74 | default => throw InvalidValueException::createFromContext( 75 | value: $value, 76 | context: $context, 77 | ), 78 | }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/TypeInterface.php: -------------------------------------------------------------------------------- 1 | $types 15 | */ 16 | public function __construct( 17 | private readonly array $types, 18 | ) {} 19 | 20 | /** 21 | * Finds a child supported type from their {@see $types} list by value. 22 | */ 23 | protected function findType(mixed $value, Context $context): ?TypeInterface 24 | { 25 | foreach ($this->types as $index => $type) { 26 | $entrance = $context->enter($value, new UnionLeafEntry($index)); 27 | 28 | if ($type->match($value, $entrance)) { 29 | return $type; 30 | } 31 | } 32 | 33 | return null; 34 | } 35 | 36 | public function match(mixed $value, Context $context): bool 37 | { 38 | return $this->findType($value, $context) !== null; 39 | } 40 | 41 | public function cast(mixed $value, Context $context): mixed 42 | { 43 | $type = $this->findType($value, $context); 44 | 45 | if ($type !== null) { 46 | return $type->cast($value, $context); 47 | } 48 | 49 | throw InvalidValueException::createFromContext( 50 | value: $value, 51 | context: $context, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/UnitEnumType.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class UnitEnumType extends AsymmetricType 14 | { 15 | /** 16 | * @param class-string<\UnitEnum> $class 17 | * @param non-empty-list $cases 18 | */ 19 | public function __construct(string $class, array $cases, TypeInterface $type) 20 | { 21 | parent::__construct( 22 | normalizer: new UnitEnumTypeNormalizer($class), 23 | denormalizer: new UnitEnumTypeDenormalizer($class, $cases, $type), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/UnitEnumType/UnitEnumTypeDenormalizer.php: -------------------------------------------------------------------------------- 1 | $class 15 | * @param non-empty-list $cases 16 | */ 17 | public function __construct( 18 | protected readonly string $class, 19 | protected readonly array $cases, 20 | protected readonly TypeInterface $string, 21 | ) {} 22 | 23 | public function match(mixed $value, Context $context): bool 24 | { 25 | return \in_array($value, $this->cases, true); 26 | } 27 | 28 | public function cast(mixed $value, Context $context): \UnitEnum 29 | { 30 | $string = $this->string->cast($value, $context); 31 | 32 | if (!$this->match($string, $context)) { 33 | throw InvalidValueException::createFromContext( 34 | value: $value, 35 | context: $context, 36 | ); 37 | } 38 | 39 | try { 40 | // @phpstan-ignore-next-line : Handle Error manually 41 | return \constant($this->class . '::' . $string); 42 | } catch (\Error $e) { 43 | throw InvalidValueException::createFromContext( 44 | value: $value, 45 | context: $context, 46 | previous: $e, 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Platform/Standard/Type/UnitEnumType/UnitEnumTypeNormalizer.php: -------------------------------------------------------------------------------- 1 | $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 | public function cast(mixed $value, Context $context): string 26 | { 27 | if ($value instanceof $this->class) { 28 | return $value->name; 29 | } 30 | 31 | throw InvalidValueException::createFromContext( 32 | value: $value, 33 | context: $context, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Runtime/ClassInstantiator/ClassInstantiatorInterface.php: -------------------------------------------------------------------------------- 1 | $class 17 | * 18 | * @return T 19 | * @throws NonInstantiatableObjectException occurs in case of object is not instantiatable 20 | * @throws \Throwable occurs for some reason when creating an object 21 | */ 22 | public function instantiate(ClassMetadata $class, Context $context): object; 23 | } 24 | -------------------------------------------------------------------------------- /src/Runtime/ClassInstantiator/ReflectionClassInstantiator.php: -------------------------------------------------------------------------------- 1 | getName()); 16 | 17 | if (!$reflection->isInstantiable()) { 18 | throw NonInstantiatableObjectException::createFromContext( 19 | expected: $class->getTypeStatement($context), 20 | value: $reflection, 21 | context: $context, 22 | ); 23 | } 24 | 25 | return $reflection->newInstanceWithoutConstructor(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Runtime/ConfigurationInterface.php: -------------------------------------------------------------------------------- 1 | parent; 41 | } 42 | 43 | /** 44 | * @return iterable 45 | */ 46 | private function getStack(): iterable 47 | { 48 | yield $current = $this; 49 | 50 | do { 51 | yield $current = $current->parent; 52 | } while ($current instanceof self); 53 | } 54 | 55 | public function getPath(): PathInterface 56 | { 57 | $entries = []; 58 | 59 | foreach ($this->getStack() as $context) { 60 | if ($context instanceof self) { 61 | $entries[] = $context->entry; 62 | } 63 | } 64 | 65 | return new Path(\array_reverse($entries)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Runtime/Context/Direction.php: -------------------------------------------------------------------------------- 1 | isStrictTypesOptionDefined()) { 28 | $config = $config->withStrictTypes(false); 29 | } 30 | 31 | // ... 32 | } 33 | 34 | return new self( 35 | value: $value, 36 | direction: Direction::Normalize, 37 | types: $types, 38 | parser: $parser, 39 | config: $config, 40 | ); 41 | } 42 | 43 | public static function forDenormalization( 44 | mixed $value, 45 | ConfigurationInterface $config, 46 | TypeParserFacadeInterface $parser, 47 | TypeRepositoryFacadeInterface $types, 48 | ): self { 49 | if ($config instanceof Configuration) { 50 | // Enable strict-types for normalization if option is not set 51 | if (!$config->isStrictTypesOptionDefined()) { 52 | $config = $config->withStrictTypes(true); 53 | } 54 | 55 | // ... 56 | } 57 | 58 | return new self( 59 | value: $value, 60 | direction: Direction::Denormalize, 61 | types: $types, 62 | parser: $parser, 63 | config: $config, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Runtime/Parser/InMemoryTypeParser.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $types = []; 15 | 16 | public function __construct( 17 | private readonly TypeParserInterface $delegate, 18 | /** 19 | * Limit on the number of statements stored in RAM. 20 | * 21 | * @var int<0, max> 22 | */ 23 | private readonly int $typesLimit = 100, 24 | 25 | /** 26 | * Number of types cleared after GC triggering. 27 | * 28 | * @var int<0, max> 29 | */ 30 | private readonly int $typesCleanupCount = 30, 31 | ) {} 32 | 33 | public function getStatementByDefinition(string $definition): TypeStatement 34 | { 35 | $this->cleanup(); 36 | 37 | return $this->types[$definition] 38 | ??= $this->delegate->getStatementByDefinition($definition); 39 | } 40 | 41 | private function cleanup(): void 42 | { 43 | if (\count($this->types) <= $this->typesLimit) { 44 | return; 45 | } 46 | 47 | $this->types = \array_slice($this->types, $this->typesCleanupCount); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Runtime/Parser/LoggableTypeParser.php: -------------------------------------------------------------------------------- 1 | logger->debug('Fetching an AST by "{definition}"', [ 20 | 'definition' => $definition, 21 | ]); 22 | 23 | $statement = $this->delegate->getStatementByDefinition($definition); 24 | 25 | $this->logger->info('AST was fetched by "{definition}"', [ 26 | 'definition' => $definition, 27 | 'statement' => $statement, 28 | ]); 29 | 30 | return $statement; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Runtime/Parser/TraceableTypeParser.php: -------------------------------------------------------------------------------- 1 | tracer->start(\sprintf('Parse "%s"', $definition)); 20 | 21 | try { 22 | return $this->delegate->getStatementByDefinition($definition); 23 | } finally { 24 | $span->stop(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Runtime/Parser/TypeParser.php: -------------------------------------------------------------------------------- 1 | isFeatureSupported(GrammarFeature::Conditional), 28 | shapes: $platform->isFeatureSupported(GrammarFeature::Shapes), 29 | callables: $platform->isFeatureSupported(GrammarFeature::Callables), 30 | literals: $platform->isFeatureSupported(GrammarFeature::Literals), 31 | generics: $platform->isFeatureSupported(GrammarFeature::Generics), 32 | union: $platform->isFeatureSupported(GrammarFeature::Union), 33 | intersection: $platform->isFeatureSupported(GrammarFeature::Intersection), 34 | list: $platform->isFeatureSupported(GrammarFeature::List), 35 | offsets: $platform->isFeatureSupported(GrammarFeature::Offsets), 36 | hints: $platform->isFeatureSupported(GrammarFeature::Hints), 37 | attributes: $platform->isFeatureSupported(GrammarFeature::Attributes), 38 | )); 39 | } 40 | 41 | public function getStatementByDefinition(string $definition): TypeStatement 42 | { 43 | // Fast-built optimization: if the definition is "null", return a null literal. 44 | if ($definition === 'null') { 45 | return new NullLiteralNode(); 46 | } 47 | 48 | return $this->parser->parse(new Source($definition)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Runtime/Parser/TypeParserFacade.php: -------------------------------------------------------------------------------- 1 | runtime->getStatementByDefinition($definition); 20 | } 21 | 22 | public function getStatementByValue(mixed $value): TypeStatement 23 | { 24 | return match (true) { 25 | $value === null => new NullLiteralNode(), 26 | \is_bool($value) => new BoolLiteralNode($value), 27 | // @phpstan-ignore-next-line : The "get_debug_type" function always return a non-empty-string 28 | default => $this->getStatementByDefinition(\get_debug_type($value)), 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Runtime/Parser/TypeParserFacadeInterface.php: -------------------------------------------------------------------------------- 1 | index; 13 | 14 | parent::__construct($key === '' ? '0' : $key); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Runtime/Path/Entry/Entry.php: -------------------------------------------------------------------------------- 1 | value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Runtime/Path/Entry/EntryInterface.php: -------------------------------------------------------------------------------- 1 | $index 11 | */ 12 | public function __construct( 13 | public readonly int $index, 14 | ) { 15 | parent::__construct((string) $this->index); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Runtime/Path/JsonPathPrinter.php: -------------------------------------------------------------------------------- 1 | \is_numeric($entry->value) ? "[$entry]" : ".$entry", 19 | $entry instanceof ObjectPropertyEntry => ".$entry", 20 | default => '', 21 | }; 22 | } 23 | 24 | return $result; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Runtime/Path/Path.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Path implements PathInterface, \IteratorAggregate 13 | { 14 | public function __construct( 15 | /** 16 | * @var list 17 | */ 18 | protected array $entries = [], 19 | ) {} 20 | 21 | public function getIterator(): \Traversable 22 | { 23 | return new \ArrayIterator($this->entries); 24 | } 25 | 26 | public function toArray(): array 27 | { 28 | return $this->entries; 29 | } 30 | 31 | public function isEmpty(): bool 32 | { 33 | return $this->entries === []; 34 | } 35 | 36 | /** 37 | * @return int<0, max> 38 | */ 39 | public function count(): int 40 | { 41 | return \count($this->entries); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Runtime/Path/PathInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface PathInterface extends \Traversable, \Countable 13 | { 14 | /** 15 | * @return list 16 | */ 17 | public function toArray(): array; 18 | 19 | /** 20 | * Returns {@see true} in case of mapping path is empty. 21 | */ 22 | public function isEmpty(): bool; 23 | } 24 | -------------------------------------------------------------------------------- /src/Runtime/Path/PathPrinterInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private readonly \WeakMap $types; 16 | 17 | public function __construct( 18 | TypeRepositoryInterface $delegate, 19 | ) { 20 | parent::__construct($delegate); 21 | 22 | $this->types = new \WeakMap(); 23 | } 24 | 25 | #[\Override] 26 | public function getTypeByStatement(TypeStatement $statement, ?\ReflectionClass $context = null): TypeInterface 27 | { 28 | // @phpstan-ignore-next-line : PHPStan bug (array assign over readonly) 29 | return $this->types[$statement] ??= parent::getTypeByStatement($statement, $context); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Runtime/Repository/LoggableTypeRepository.php: -------------------------------------------------------------------------------- 1 | logger->debug('Fetching the type by the AST statement {statement_name}', [ 26 | 'statement' => $statement, 27 | 'statement_name' => $statement::class . '#' . \spl_object_id($statement), 28 | 'context' => $context, 29 | ]); 30 | 31 | $type = $result = parent::getTypeByStatement($statement, $context); 32 | 33 | if ($type instanceof TypeDecoratorInterface) { 34 | $type = $type->getDecoratedType(); 35 | } 36 | 37 | $this->logger->info('The {type_name} was fetched by the AST statement {statement_name}', [ 38 | 'statement' => $statement, 39 | 'statement_name' => $statement::class . '#' . \spl_object_id($statement), 40 | 'type' => $type, 41 | 'type_name' => $type::class . '#' . \spl_object_id($type), 42 | 'context' => $context, 43 | ]); 44 | 45 | if ($result instanceof LoggableType) { 46 | return $result; 47 | } 48 | 49 | return new LoggableType($this->logger, $result); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Runtime/Repository/Reference/NullReferencesReader.php: -------------------------------------------------------------------------------- 1 | $class 11 | * 12 | * @return array 13 | */ 14 | public function getUseStatements(\ReflectionClass $class): array; 15 | } 16 | -------------------------------------------------------------------------------- /src/Runtime/Repository/TraceableTypeRepository.php: -------------------------------------------------------------------------------- 1 | printer = new PrettyPrinter( 25 | wrapUnionType: false, 26 | multilineShape: \PHP_INT_MAX, 27 | ); 28 | } 29 | 30 | public function getTypeByStatement(TypeStatement $statement, ?\ReflectionClass $context = null): TypeInterface 31 | { 32 | $span = $this->tracer->start(\vsprintf('Fetch type "%s"', [ 33 | $this->printer->print($statement), 34 | ])); 35 | 36 | try { 37 | $result = parent::getTypeByStatement($statement, $context); 38 | } finally { 39 | $span->stop(); 40 | } 41 | 42 | if ($result instanceof TraceableType) { 43 | return $result; 44 | } 45 | 46 | return new TraceableType( 47 | definition: $this->printer->print($statement), 48 | tracer: $this->tracer, 49 | delegate: $result, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Runtime/Repository/TypeDecorator/LoggableType.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | private function getLoggerArguments(mixed $value, Context $context): array 28 | { 29 | $path = $context->getPath(); 30 | $delegate = $this->getDecoratedType(); 31 | 32 | return [ 33 | 'value' => $value, 34 | 'type' => $delegate, 35 | 'type_name' => $delegate::class . '#' . \spl_object_id($delegate), 36 | 'path' => $path->toArray(), 37 | ]; 38 | } 39 | 40 | public function match(mixed $value, Context $context): bool 41 | { 42 | $this->logger->debug( 43 | 'Matching by the {type_name}', 44 | $this->getLoggerArguments($value, $context), 45 | ); 46 | 47 | $result = parent::match($value, $context); 48 | 49 | $this->logger->info( 50 | $result === true 51 | ? 'Matched by the {type_name}' 52 | : 'Not matched by the {type_name}', 53 | $this->getLoggerArguments($value, $context), 54 | ); 55 | 56 | return $result; 57 | } 58 | 59 | public function cast(mixed $value, Context $context): mixed 60 | { 61 | $this->logger->debug( 62 | 'Casting by the {type_name}', 63 | $this->getLoggerArguments($value, $context), 64 | ); 65 | 66 | try { 67 | $result = parent::cast($value, $context); 68 | } catch (\Throwable $e) { 69 | $this->logger->error('Casting by the {type_name} was failed', [ 70 | ...$this->getLoggerArguments($value, $context), 71 | 'error' => $e, 72 | ]); 73 | throw $e; 74 | } 75 | 76 | $this->logger->info('Casted by the {type_name}', [ 77 | ...$this->getLoggerArguments($value, $context), 78 | 'result' => $result, 79 | ]); 80 | 81 | return $result; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Runtime/Repository/TypeDecorator/TraceableType.php: -------------------------------------------------------------------------------- 1 | name = $this->getSpanTitle(); 30 | } 31 | 32 | /** 33 | * @return non-empty-string 34 | */ 35 | private function getSpanTitle(): string 36 | { 37 | $inner = $this->getDecoratedType(); 38 | 39 | return \vsprintf('"%s" using %s#%d', [ 40 | \addcslashes($this->definition, '"'), 41 | $inner::class, 42 | \spl_object_id($inner), 43 | ]); 44 | } 45 | 46 | public function match(mixed $value, Context $context): bool 47 | { 48 | $span = $this->tracer->start(\sprintf('Match %s', $this->name)); 49 | 50 | try { 51 | return parent::match($value, $context); 52 | } finally { 53 | $span->stop(); 54 | } 55 | } 56 | 57 | public function cast(mixed $value, Context $context): mixed 58 | { 59 | $span = $this->tracer->start(\sprintf('Cast %s', $this->name)); 60 | 61 | try { 62 | return parent::cast($value, $context); 63 | } finally { 64 | $span->stop(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Runtime/Repository/TypeDecorator/TypeDecorator.php: -------------------------------------------------------------------------------- 1 | delegate instanceof TypeDecoratorInterface) { 23 | return $this->delegate->getDecoratedType(); 24 | } 25 | 26 | return $this->delegate; 27 | } 28 | 29 | public function match(mixed $value, Context $context): bool 30 | { 31 | return $this->delegate->match($value, $context); 32 | } 33 | 34 | public function cast(mixed $value, Context $context): mixed 35 | { 36 | return $this->delegate->cast($value, $context); 37 | } 38 | 39 | public function __serialize(): array 40 | { 41 | throw new \LogicException(<<<'MESSAGE' 42 | Cannot serialize a type decorator. 43 | 44 | Please disable cache in case you are using debug mode. 45 | MESSAGE); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Runtime/Repository/TypeDecorator/TypeDecoratorInterface.php: -------------------------------------------------------------------------------- 1 | > 20 | */ 21 | private array $builders = []; 22 | 23 | private TypeRepositoryInterface $context; 24 | 25 | /** 26 | * @param iterable> $types 27 | */ 28 | public function __construct( 29 | private readonly TypeParserInterface $parser, 30 | iterable $types = [], 31 | private readonly ReferencesResolver $references = new ReferencesResolver(), 32 | ) { 33 | $this->context = $this; 34 | $this->builders = self::toArrayList($types); 35 | } 36 | 37 | /** 38 | * @param iterable> $types 39 | * 40 | * @return list> 41 | */ 42 | private static function toArrayList(iterable $types): array 43 | { 44 | return match (true) { 45 | $types instanceof \Traversable => \iterator_to_array($types, false), 46 | \array_is_list($types) => $types, 47 | default => \array_values($types), 48 | }; 49 | } 50 | 51 | /** 52 | * @internal internal method for passing the root calling context 53 | */ 54 | public function setTypeRepository(TypeRepositoryInterface $parent): void 55 | { 56 | $this->context = $parent; 57 | } 58 | 59 | /** 60 | * TODO should me moved to an external factory class 61 | */ 62 | public static function createFromPlatform( 63 | PlatformInterface $platform, 64 | TypeParserInterface $parser, 65 | ReferencesResolver $references = new ReferencesResolver(), 66 | ): self { 67 | return new self( 68 | parser: $parser, 69 | types: $platform->getTypes(), 70 | references: $references, 71 | ); 72 | } 73 | 74 | public function getTypeByStatement(TypeStatement $statement, ?\ReflectionClass $context = null): TypeInterface 75 | { 76 | if ($context !== null) { 77 | $statement = $this->references->resolve($statement, $context); 78 | } 79 | 80 | foreach ($this->builders as $factory) { 81 | if ($factory->isSupported($statement)) { 82 | // @phpstan-ignore-next-line : Statement expects a bottom type (never), but TypeStatement passed 83 | return $factory->build($statement, $this->context, $this->parser); 84 | } 85 | } 86 | 87 | throw TypeNotFoundException::becauseTypeNotDefined($statement); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Runtime/Repository/TypeRepositoryDecorator.php: -------------------------------------------------------------------------------- 1 | setTypeRepository($this); 16 | } 17 | 18 | /** 19 | * @internal internal method for passing the root calling context 20 | */ 21 | public function setTypeRepository(TypeRepositoryInterface $parent): void 22 | { 23 | if ($this->delegate instanceof TypeRepositoryDecoratorInterface) { 24 | $this->delegate->setTypeRepository($parent); 25 | } 26 | } 27 | 28 | public function getTypeByStatement(TypeStatement $statement, ?\ReflectionClass $context = null): TypeInterface 29 | { 30 | return $this->delegate->getTypeByStatement($statement, $context); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Runtime/Repository/TypeRepositoryDecoratorInterface.php: -------------------------------------------------------------------------------- 1 | parser->getStatementByDefinition($definition); 21 | 22 | return $this->runtime->getTypeByStatement($statement, $context); 23 | } 24 | 25 | public function getTypeByValue(mixed $value, ?\ReflectionClass $context = null): TypeInterface 26 | { 27 | $statement = $this->parser->getStatementByValue($value); 28 | 29 | return $this->runtime->getTypeByStatement($statement, $context); 30 | } 31 | 32 | public function getTypeByStatement(TypeStatement $statement, ?\ReflectionClass $context = null): TypeInterface 33 | { 34 | return $this->runtime->getTypeByStatement($statement, $context); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Runtime/Repository/TypeRepositoryFacadeInterface.php: -------------------------------------------------------------------------------- 1 | |null $context 15 | * 16 | * @throws TypeNotFoundException in case of type cannot be loaded 17 | * @throws \Throwable in case of any internal error occurs 18 | */ 19 | public function getTypeByDefinition( 20 | string $definition, 21 | ?\ReflectionClass $context = null, 22 | ): TypeInterface; 23 | 24 | /** 25 | * @param \ReflectionClass|null $context 26 | * 27 | * @throws TypeNotFoundException in case of type cannot be loaded 28 | * @throws \Throwable in case of any internal error occurs 29 | */ 30 | public function getTypeByValue( 31 | mixed $value, 32 | ?\ReflectionClass $context = null, 33 | ): TypeInterface; 34 | } 35 | -------------------------------------------------------------------------------- /src/Runtime/Repository/TypeRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | |null $context 15 | * 16 | * @throws TypeNotFoundException in case of type cannot be loaded 17 | * @throws \Throwable in case of any internal error occurs 18 | */ 19 | public function getTypeByStatement(TypeStatement $statement, ?\ReflectionClass $context = null): TypeInterface; 20 | } 21 | -------------------------------------------------------------------------------- /src/Runtime/Tracing/SpanInterface.php: -------------------------------------------------------------------------------- 1 | stopwatch->start($name, $this->category), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Runtime/Tracing/SymfonyStopwatchTracer/SymfonyStopwatchSpan.php: -------------------------------------------------------------------------------- 1 | event->stop(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Runtime/Tracing/TracerInterface.php: -------------------------------------------------------------------------------- 1 | dumper = $dumper ?? $this->createDefaultDataDumper(); 29 | $this->cloner = $cloner ?? $this->createDefaultVarCloner(); 30 | } 31 | 32 | private function createDefaultDataDumper(): CliDumper 33 | { 34 | $dumper = new class extends CliDumper { 35 | public function dump(Data $data, $output = null): ?string 36 | { 37 | $result = parent::dump($data, $output); 38 | 39 | if ($result !== null) { 40 | return \rtrim($result, "\n"); 41 | } 42 | 43 | return null; 44 | } 45 | }; 46 | $dumper->setColors(false); 47 | 48 | return $dumper; 49 | } 50 | 51 | private function createDefaultVarCloner(): VarCloner 52 | { 53 | $cloner = new VarCloner(); 54 | $cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); 55 | 56 | return $cloner; 57 | } 58 | 59 | /** 60 | * @throws ComposerPackageRequiredException 61 | */ 62 | private static function assertKernelPackageIsInstalled(): void 63 | { 64 | if (!\interface_exists(DataDumperInterface::class)) { 65 | throw ComposerPackageRequiredException::becausePackageNotInstalled( 66 | package: 'symfony/var-dumper', 67 | purpose: 'Symfony value printer support', 68 | ); 69 | } 70 | } 71 | 72 | public function print(mixed $value): string 73 | { 74 | $result = $this->cloner->cloneVar($value); 75 | 76 | return (string) $this->dumper->dump($result, true); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Runtime/Value/ValuePrinterInterface.php: -------------------------------------------------------------------------------- 1 |