allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s

This commit is contained in:
2026-03-30 14:54:57 +07:00
parent 66aed7c4e8
commit b5e3a778ce
21316 changed files with 2777892 additions and 13 deletions

18
vendor/cuyz/valinor/LICENSE.md vendored Normal file
View File

@@ -0,0 +1,18 @@
Copyright (c) 2021 Romain Canon
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

129
vendor/cuyz/valinor/README.md vendored Normal file
View File

@@ -0,0 +1,129 @@
<div align="center">
![Valinor banner](docs/pages/img/valinor-banner.svg)
<div>— From boring old arrays to shiny typed objects —</div>
<br>
[![Latest Stable Version](https://poser.pugx.org/cuyz/valinor/v)][link-packagist]
[![PHP Version Require](https://poser.pugx.org/cuyz/valinor/require/php)][link-packagist]
[![Total Downloads](https://poser.pugx.org/cuyz/valinor/downloads)][link-packagist]
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FCuyZ%2FValinor%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/CuyZ/Valinor/master)
</div>
---
Valinor takes care of the construction and validation of raw inputs (JSON, plain
arrays, etc.) into objects, ensuring a perfectly valid state. It allows the
objects to be used without having to worry about their integrity during the
whole application lifecycle.
The validation system will detect any incorrect value and help the developers by
providing precise and human-readable error messages.
The mapper can handle native PHP types as well as other advanced types supported
by [PHPStan] and [Psalm] like shaped arrays, generics, integer ranges and more.
The library also provides a normalization mechanism that can help transform any
input into a data format (JSON, CSV, …), while preserving the original
structure.
## Installation
```bash
composer require cuyz/valinor
```
**📔 Read more on the [online documentation](https://valinor.cuyz.io)**
## Example
```php
final class Country
{
public function __construct(
/** @var non-empty-string */
public readonly string $name,
/** @var list<City> */
public readonly array $cities,
) {}
}
final class City
{
public function __construct(
/** @var non-empty-string */
public readonly string $name,
public readonly DateTimeZone $timeZone,
) {}
}
$json = <<<JSON
{
"name": "France",
"cities": [
{"name": "Paris", "timeZone": "Europe/Paris"},
{"name": "Lyon", "timeZone": "Europe/Paris"}
]
}
JSON;
try {
$country = (new \CuyZ\Valinor\MapperBuilder())
->mapper()
->map(Country::class, \CuyZ\Valinor\Mapper\Source\Source::json($json));
echo $country->name; // France
echo $country->cities[0]->name; // Paris
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
// Handle the error…
}
```
## Documentation
The full documentation is available on [valinor.cuyz.io].
## Credits & thank you
The development of this library is mainly motivated by the kind words and the
help of many people. I am grateful to everyone, especially to the contributors
of this repository who directly help to push the project forward:
[![](https://contrib.rocks/image?repo=cuyz/valinor)](https://github.com/cuyz/valinor/graphs/contributors)
### Powered by
[![PhpStorm logo](https://resources.jetbrains.com/storage/products/company/brand/logos/PhpStorm.svg)](https://jb.gg/OpenSourceSupport)
I have to give [JetBrains] credits for providing [a free PhpStorm license] for
the development of this open-source package.
### Special thanks
I also want to thank
[![blackfire-logo] Blackfire](https://www.blackfire.io/?utm_source=valinor&utm_medium=readme&utm_campaign=free-open-source)
for providing a license of their awesome tool, leading to notable performance
gains when using this library.
[link-packagist]: https://packagist.org/packages/cuyz/valinor
[contributors]: https://github.com/CuyZ/Valinor/graphs/contributors
[PHPStan]: https://phpstan.org/
[Psalm]: https://psalm.dev/
[Jetbrains]: https://www.jetbrains.com/
[a free PhpStorm license]: https://jb.gg/OpenSourceSupport
[Blackfire]: https://www.blackfire.io/?utm_source=valinor&utm_medium=readme&utm_campaign=free-open-source
[blackfire-logo]: docs/pages/img/blackfire-logo.svg "Blackfire logo"
[valinor.cuyz.io]: https://valinor.cuyz.io

97
vendor/cuyz/valinor/composer.json vendored Normal file
View File

@@ -0,0 +1,97 @@
{
"name": "cuyz/valinor",
"type": "library",
"description": "Library that helps to map any input into a strongly-typed value object structure.",
"keywords": [
"object", "tree", "mapper", "mapping", "hydrator", "array", "conversion", "json", "yaml"
],
"homepage": "https://github.com/CuyZ/Valinor",
"license": "MIT",
"authors": [
{
"name": "Romain Canon",
"email": "romain.hydrocanon@gmail.com",
"homepage": "https://github.com/romm"
}
],
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"composer-runtime-api": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"infection/infection": "^0.31",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"friendsofphp/php-cs-fixer": "^3.4",
"marcocesarato/php-conventional-changelog": "^1.12",
"vimeo/psalm": "^6.0",
"mikey179/vfsstream": "^1.6.10",
"rector/rector": "^2.0",
"phpbench/phpbench": "^1.3"
},
"autoload": {
"psr-4": {
"CuyZ\\Valinor\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"CuyZ\\Valinor\\Tests\\": "tests",
"CuyZ\\Valinor\\QA\\": "qa"
}
},
"scripts": {
"check": [
"@putenv XDEBUG_MODE=off",
"phpunit",
"phpstan",
"psalm --config=tests/StaticAnalysis/psalm-without-plugin.xml",
"psalm --config=tests/StaticAnalysis/psalm-with-plugin.xml",
"phpstan --configuration=tests/StaticAnalysis/phpstan-without-extension.neon.dist",
"phpstan --configuration=tests/StaticAnalysis/phpstan-with-extension.neon.dist",
"php-cs-fixer fix --dry-run",
"rector --dry-run",
"@check-todo"
],
"check-todo": [
"! git --no-pager grep --extended-regexp --ignore-case 'todo|fixme' -- ':!composer.json' ':!*/quality-assurance.yml'"
],
"fix": [
"@putenv XDEBUG_MODE=off",
"php-cs-fixer fix",
"rector"
],
"test": [
"@putenv XDEBUG_MODE=off",
"phpunit"
],
"mutation": [
"infection --threads=max --git-diff-lines --with-uncovered --min-msi=100"
],
"doc": [
"Composer\\Config::disableProcessTimeout",
"docker build -t valinor-doc ./docs",
"docker run --name valinor-doc --rm -it -p 8000:8000 -v ${PWD}/docs:/docs valinor-doc"
],
"benchmark-baseline": [
"phpbench run --tag=original --retry-threshold=5 --iterations=10"
],
"benchmark-compare": [
"phpbench run --report=aggregate --ref=original --retry-threshold=5 --iterations=10 --assert='mode(variant.time.avg) <= mode(baseline.time.avg) +/- 10%'"
],
"benchmark": [
"phpbench run --report=aggregate --iterations=10"
]
},
"config": {
"allow-plugins": {
"infection/extension-installer": false
}
},
"conflict": {
"vimeo/psalm": "<5.0 || >=7.0",
"phpstan/phpstan": "<1.0 || >= 3.0"
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\PHPStan\Extension;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function str_contains;
use function str_starts_with;
/**
* @implements Rule<InClassNode>
*/
final class ApiAndInternalAnnotationCheck implements Rule
{
public function getNodeType(): string
{
return InClassNode::class;
}
public function processNode(Node $node, Scope $scope): array
{
$reflection = $scope->getClassReflection();
if (! $reflection) {
return [];
}
if ($reflection->isAnonymous()) {
return [];
}
if (str_contains($reflection->getFileName() ?? '', '/tests/')) {
return [];
}
if (str_starts_with($reflection->getName(), 'CuyZ\Valinor\QA')) {
return [];
}
if (! preg_match('/@(api|internal)\s+/', $reflection->getResolvedPhpDoc()?->getPhpDocString() ?? '')) {
return [
RuleErrorBuilder::message(
'Missing annotation `@api` or `@internal`.'
)->identifier('valinor.apiOrInternalAnnotation')->build(),
];
}
return [];
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\PHPStan\Extension;
use CuyZ\Valinor\Mapper\ArgumentsMapper;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use function count;
final class ArgumentsMapperPHPStanExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return ArgumentsMapper::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'mapArguments';
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$arguments = $methodCall->getArgs();
if (count($arguments) === 0) {
return new MixedType();
}
$type = $scope->getType($arguments[0]->value);
if (! $type->isCallable()->yes()) {
return new MixedType();
}
$acceptors = $type->getCallableParametersAcceptors($scope);
if (count($acceptors) !== 1) {
return new MixedType();
}
$parameters = $acceptors[0]->getParameters();
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($parameters as $parameter) {
$builder->setOffsetValueType(new ConstantStringType($parameter->getName()), $parameter->getType(), $parameter->isOptional());
}
return $builder->getArray();
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\PHPStan\Extension;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\NormalizerBuilder;
use CuyZ\Valinor\Utility\Polyfill;
use PhpParser\Node;
use PHPStan\Analyser\Error;
use PHPStan\Analyser\IgnoreErrorExtension;
use PHPStan\Analyser\Scope;
use function in_array;
/**
* This extension should not be used by default, as it suppresses errors that
* are typically indicative of purity issues in the codebase. It is advised to
* first address the root causes of these errors before enabling this extension.
*
* When enabled, it will ignore all purity-related errors of method calls in the
* context of `MapperBuilder` and `NormalizerBuilder`.
*/
final class SuppressPureErrors implements IgnoreErrorExtension
{
public function shouldIgnore(Error $error, Node $node, Scope $scope): bool
{
if ($error->getIdentifier() !== 'argument.type') {
return false;
}
if (! $node instanceof Node\Expr\MethodCall) {
return false;
}
$type = $scope->getType($node->var);
if (! $type->isObject()->yes()) {
return false;
}
if (! Polyfill::array_find($type->getObjectClassNames(), fn (string $className) => $className === MapperBuilder::class || $className === NormalizerBuilder::class)) {
return false;
}
if (! $node->name instanceof Node\Identifier) {
return false;
}
$methodName = $node->name->toString();
if (! in_array($methodName, [
'infer',
'registerConstructor',
'registerConverter',
'registerTransformer',
], true)) {
return false;
}
if ($node->args === []) {
return false;
}
$callableArgumentIndex = [
'infer' => 1,
'registerConstructor' => 0,
'registerConverter' => 0,
'registerTransformer' => 0,
][$methodName];
$argument = $node->args[$callableArgumentIndex];
if (! $argument instanceof Node\Arg) {
return false;
}
$argumentType = $scope->getType($argument->value);
if ($argumentType->isCallable()->yes()) {
return true;
}
if ($argumentType->isArray()->yes() && $argumentType->getIterableValueType()->isCallable()->yes()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\PHPStan\Extension;
use CuyZ\Valinor\Mapper\TreeMapper;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\PhpDoc\TypeStringResolver;
use PHPStan\PhpDocParser\Parser\ParserException;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use function implode;
use function method_exists;
final class TreeMapperPHPStanExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(private TypeStringResolver $resolver) {}
public function getClass(): string
{
return TreeMapper::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'map';
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$arguments = $methodCall->getArgs();
if (count($arguments) === 0) {
return new MixedType();
}
$type = $scope->getType($arguments[0]->value);
if ($type instanceof UnionType) {
return $type->traverse(fn (Type $type) => $this->type($type));
}
try {
return $this->type($type);
} catch (ParserException) {
// Fallback to `mixed` type if the type cannot be resolved. This can
// occur with a type that is not understood/supported by PHPStan. If
// that happens, returning a mixed type is the safest option, as it
// will not make the analysis fail.
return new MixedType();
}
}
private function type(Type $type): Type
{
if ($type->isConstantValue()->yes()) {
$value = implode('', $type->getConstantScalarValues());
return $this->resolver->resolve($value);
}
// @phpstan-ignore function.alreadyNarrowedType (support for PHPStan v1)
if (method_exists($type, 'isClassString') && $type->isClassString()->yes()) {
return $type->getClassStringObjectType();
}
// @phpstan-ignore method.nonObject (support for PHPStan v1)
if (method_exists($type, 'isClassStringType') && $type->isClassStringType()->yes()) {
return $type->getClassStringObjectType();
}
return new MixedType();
}
}

View File

@@ -0,0 +1,19 @@
<?php
use CuyZ\Valinor\QA\PHPStan\Extension\ArgumentsMapperPHPStanExtension;
use CuyZ\Valinor\QA\PHPStan\Extension\TreeMapperPHPStanExtension;
require_once 'Extension/ArgumentsMapperPHPStanExtension.php';
require_once 'Extension/TreeMapperPHPStanExtension.php';
return [
'services' => [
[
'class' => TreeMapperPHPStanExtension::class,
'tags' => ['phpstan.broker.dynamicMethodReturnTypeExtension']
], [
'class' => ArgumentsMapperPHPStanExtension::class,
'tags' => ['phpstan.broker.dynamicMethodReturnTypeExtension']
],
],
];

View File

@@ -0,0 +1,14 @@
<?php
use CuyZ\Valinor\QA\PHPStan\Extension\SuppressPureErrors;
require_once 'Extension/SuppressPureErrors.php';
return [
'services' => [
[
'class' => SuppressPureErrors::class,
'tags' => ['phpstan.ignoreErrorExtension'],
],
],
];

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\Psalm\Plugin;
use CuyZ\Valinor\Mapper\ArgumentsMapper;
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface;
use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Union;
use function count;
use function reset;
final class ArgumentsMapperPsalmPlugin implements MethodReturnTypeProviderInterface
{
public static function getClassLikeNames(): array
{
return [ArgumentsMapper::class];
}
public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union
{
if ($event->getMethodNameLowercase() !== 'maparguments') {
return null;
}
$arguments = $event->getCallArgs();
if (count($arguments) === 0) {
return null;
}
$types = $event->getSource()->getNodeTypeProvider()->getType($arguments[0]->value);
if ($types === null) {
return null;
}
$types = $types->getAtomicTypes();
if (count($types) !== 1) {
return null;
}
$type = reset($types);
if ($type instanceof TKeyedArray) {
// Internal class usage, see https://github.com/vimeo/psalm/issues/8726
$type = CallableTypeComparator::getCallableFromAtomic($event->getSource()->getCodebase(), $type);
}
if (! $type instanceof TClosure) {
return null;
}
$typeParams = $type->params ?? [];
if ($typeParams === []) {
return null;
}
$params = [];
foreach ($typeParams as $param) {
$params[$param->name] = $param->type ?? new Union([new TMixed()]);
}
return new Union([new TKeyedArray($params)]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\Psalm\Plugin;
use CuyZ\Valinor\Mapper\TreeMapper;
use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TDependentGetClass;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Union;
final class TreeMapperPsalmPlugin implements MethodReturnTypeProviderInterface
{
public static function getClassLikeNames(): array
{
return [TreeMapper::class];
}
public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union
{
if ($event->getMethodNameLowercase() !== 'map') {
return null;
}
$arguments = $event->getCallArgs();
if (count($arguments) === 0) {
return null;
}
$type = $event->getSource()->getNodeTypeProvider()->getType($arguments[0]->value);
if (! $type) {
return null;
}
$types = [];
foreach ($type->getAtomicTypes() as $node) {
$inferred = self::type($node);
if ($inferred === null) {
return null;
}
$types[] = $inferred;
}
return Type::combineUnionTypeArray($types, $event->getSource()->getCodebase());
}
private static function type(Atomic $node): ?Union
{
return match (true) {
$node instanceof TLiteralString => Type::parseString($node->value),
$node instanceof TDependentGetClass => $node->as_type,
$node instanceof TClassString && $node->as_type => new Union([$node->as_type]),
default => null,
};
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace CuyZ\Valinor\QA\Psalm;
use CuyZ\Valinor\QA\Psalm\Plugin\ArgumentsMapperPsalmPlugin;
use CuyZ\Valinor\QA\Psalm\Plugin\TreeMapperPsalmPlugin;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
use SimpleXMLElement;
class ValinorPsalmPlugin implements PluginEntryPointInterface
{
public function __invoke(RegistrationInterface $api, ?SimpleXMLElement $config = null): void
{
require_once __DIR__ . '/Plugin/TreeMapperPsalmPlugin.php';
require_once __DIR__ . '/Plugin/ArgumentsMapperPsalmPlugin.php';
$api->registerHooksFromClass(TreeMapperPsalmPlugin::class);
$api->registerHooksFromClass(ArgumentsMapperPsalmPlugin::class);
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<issueHandlers xmlns="https://getpsalm.org/schema/config">
<InvalidArgument>
<errorLevel type="suppress">
<referencedFunction name="CuyZ\Valinor\NormalizerBuilder::registerTransformer"/>
<referencedFunction name="\CuyZ\Valinor\MapperBuilder::infer"/>
<referencedFunction name="\CuyZ\Valinor\MapperBuilder::registerConstructor"/>
<referencedFunction name="\CuyZ\Valinor\MapperBuilder::registerConverter"/>
</errorLevel>
</InvalidArgument>
</issueHandlers>

26
vendor/cuyz/valinor/src/Cache/Cache.php vendored Normal file
View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
/**
* @internal
*
* @template T = mixed
*/
interface Cache
{
/**
* @param non-empty-string $key
* @return null|T
*/
public function get(string $key, mixed ...$arguments): mixed;
/**
* @param non-empty-string $key
*/
public function set(string $key, CacheEntry $entry): void;
public function clear(): void;
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
/** @internal */
final class CacheEntry
{
public function __construct(
public readonly string $code,
/** @var list<non-empty-string> */
public readonly array $filesToWatch = [],
) {}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Exception;
use RuntimeException;
/** @internal */
final class CacheDirectoryNotWritable extends RuntimeException
{
public function __construct(string $directory)
{
parent::__construct("Provided directory `$directory` is not writable.");
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Exception;
use RuntimeException;
/** @internal */
final class CompiledPhpCacheFileNotWritten extends RuntimeException
{
public function __construct(string $file)
{
parent::__construct("File `$file` could not be written.");
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Exception;
use RuntimeException;
/** @internal */
final class CorruptedCompiledPhpCacheFile extends RuntimeException
{
public function __construct(string $filename)
{
parent::__construct("Compiled php cache file `$filename` has corrupted value.");
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Exception;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use RuntimeException;
use function lcfirst;
/** @internal */
final class InvalidSignatureToWarmup extends RuntimeException
{
public function __construct(UnresolvableType $unresolvableType)
{
parent::__construct(
"Cannot warm up invalid signature `{$unresolvableType->toString()}`: " . lcfirst($unresolvableType->message()),
);
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use CuyZ\Valinor\Cache\Exception\CacheDirectoryNotWritable;
use CuyZ\Valinor\Cache\Exception\CompiledPhpCacheFileNotWritten;
use CuyZ\Valinor\Cache\Exception\CorruptedCompiledPhpCacheFile;
use Error;
use FilesystemIterator;
use function bin2hex;
use function file_exists;
use function file_put_contents;
use function is_dir;
use function mkdir;
use function random_bytes;
use function rename;
use function rmdir;
use function str_contains;
use function umask;
use function unlink;
/**
* @api
*
* @template EntryType
* @implements Cache<EntryType>
*/
final class FileSystemCache implements Cache
{
private const GENERATED_MESSAGE = 'Generated by ' . self::class;
public function __construct(
private string $cacheDir,
) {}
/** @internal */
public function get(string $key, mixed ...$arguments): mixed
{
$filename = $this->cacheDir . DIRECTORY_SEPARATOR . $key . '.php';
if (! file_exists($filename)) {
return null;
}
try {
return (require $filename)(...$arguments); // @phpstan-ignore callable.nonCallable
} catch (Error) {
throw new CorruptedCompiledPhpCacheFile($filename);
}
}
/** @internal */
public function set(string $key, CacheEntry $entry): void
{
$tmpDir = $this->cacheDir . DIRECTORY_SEPARATOR . '.valinor.tmp';
$filename = $this->cacheDir . DIRECTORY_SEPARATOR . $key . '.php';
if (! is_dir($tmpDir) && ! @mkdir($tmpDir, 0777, true)) {
throw new CacheDirectoryNotWritable($this->cacheDir);
}
/** @infection-ignore-all */
$tmpFilename = $tmpDir . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16));
try {
$code = '<?php // ' . self::GENERATED_MESSAGE . PHP_EOL . "return $entry->code;" . PHP_EOL;
if (! @file_put_contents($tmpFilename, $code)) {
throw new CompiledPhpCacheFileNotWritten($tmpFilename);
}
if (! @rename($tmpFilename, $filename)) {
throw new CompiledPhpCacheFileNotWritten($filename);
}
@chmod($filename, 0666 & ~umask());
} finally {
@unlink($tmpFilename);
}
}
public function clear(): void
{
if (! is_dir($this->cacheDir)) {
return;
}
$shouldDeleteRootDir = true;
/** @var FilesystemIterator $file */
foreach (new FilesystemIterator($this->cacheDir) as $file) {
if ($file->getFilename() === '.valinor.tmp') {
@rmdir($file->getPathname());
continue;
}
if (! $file->isFile()) {
$shouldDeleteRootDir = false;
continue;
}
$line = $file->openFile()->getCurrentLine();
if (! $line || ! str_contains($line, self::GENERATED_MESSAGE)) {
$shouldDeleteRootDir = false;
continue;
}
@unlink($file->getPathname());
}
if ($shouldDeleteRootDir) {
@rmdir($this->cacheDir);
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use function file_exists;
use function filemtime;
use function var_export;
/**
* This cache implementation will watch the files of the application and
* invalidate cache entries when a PHP file is modified preventing the library
* not behaving as expected when the signature of a property or a method
* changes.
*
* This is especially useful when the application runs in a development
* environment, where source files are often modified by developers.
*
* It should decorate the original cache implementation and should be given to
* the mapper builder: @see \CuyZ\Valinor\MapperBuilder::withCache
*
* @api
*
* @template EntryType
* @implements Cache<EntryType>
*/
final class FileWatchingCache implements Cache
{
/** @var array<string, array<string, int>> */
private array $timestamps = [];
public function __construct(
/** @var Cache<EntryType> */
private Cache $delegate,
) {}
/** @internal */
public function get(string $key, mixed ...$arguments): mixed
{
$this->timestamps[$key] ??= $this->delegate->get("$key.timestamps"); // @phpstan-ignore assign.propertyType
if ($this->timestamps[$key] === null) {
return null;
}
assert(is_array($this->timestamps[$key]));
foreach ($this->timestamps[$key] as $fileName => $timestamp) {
if (! file_exists($fileName)) {
return null;
}
if (filemtime($fileName) !== $timestamp) {
return null;
}
}
return $this->delegate->get($key, ...$arguments);
}
/** @internal */
public function set(string $key, CacheEntry $entry): void
{
$this->delegate->set($key, $entry);
$this->timestamps[$key] = [];
foreach ($entry->filesToWatch as $fileName) {
$time = @filemtime($fileName);
if (false === $time) {
continue;
}
$this->timestamps[$key][$fileName] = $time;
}
$code = 'fn () => ' . var_export($this->timestamps[$key], true);
$this->delegate->set("$key.timestamps", new CacheEntry($code));
}
public function clear(): void
{
$this->timestamps = [];
$this->delegate->clear();
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use CuyZ\Valinor\Library\Settings;
use CuyZ\Valinor\Utility\Package;
use function hash;
use function strstr;
/**
* @internal
*
* @template EntryType
* @implements Cache<EntryType>
*/
final class KeySanitizerCache implements Cache
{
private static string $version;
public function __construct(
/** @var Cache<EntryType> */
private Cache $delegate,
private Settings $settings,
) {}
public function get(string $key, mixed ...$arguments): mixed
{
return $this->delegate->get($this->sanitize($key), ...$arguments);
}
public function set(string $key, CacheEntry $entry): void
{
$this->delegate->set($this->sanitize($key), $entry);
}
public function clear(): void
{
$this->delegate->clear();
}
/**
* @return non-empty-string
*/
private function sanitize(string $key): string
{
// @infection-ignore-all
self::$version ??= PHP_VERSION . '/' . Package::version();
$firstPart = strstr($key, "\0", before_needle: true);
// @infection-ignore-all
return $firstPart . hash('xxh128', $key . $this->settings->hash() . self::$version);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
/**
* @internal
*
* @template EntryType
* @implements Cache<EntryType>
*/
final class RuntimeCache implements Cache
{
/** @var array<string, EntryType|null> */
private array $entries = [];
public function __construct(
/** @var Cache<EntryType> */
private Cache $delegate,
) {}
public function get(string $key, mixed ...$arguments): mixed
{
return $this->entries[$key] ??= $this->delegate->get($key, ...$arguments);
}
public function set(string $key, CacheEntry $entry): void
{
$this->delegate->set($key, $entry);
}
public function clear(): void
{
$this->entries = [];
$this->delegate->clear();
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use Closure;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Library\Settings;
use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Utility\Reflection\Reflection;
use CuyZ\Valinor\Utility\TypeHelper;
use ReflectionFunction;
use function array_filter;
use function array_unique;
use function array_values;
/** @internal */
final class TypeFilesWatcher
{
public function __construct(
private Settings $settings,
private ClassDefinitionRepository $classDefinitionRepository,
) {}
/**
* This method returns a list of files in which are declared all types that
* compose the given type. If the given type is composed of other types,
* they are recursively resolved as well. If the type is a class, all
* properties are also resolved.
*
* In addition, all callable that were given to the global settings are
* checked, and their file names are added to the list.
*
* This file list can then be used by the `FileWatchingCache` to detect
* changes to these files and invalidate the cache entries if needed.
*
* Example of returned value:
*
* ```php
* [
* // An entity representing a user
* '/root/path/src/Domain/User/User.php',
*
* // The user has an `$email` property and a `$phoneNumber` property
* '/root/path/src/Domain/Shared/Email.php',
* '/root/path/src/Domain/Shared/PhoneNumber.php',
*
* // The settings contain a custom constructor defined in a file
* '/root/path/src/Infrastructure/Mapper/CustomConstructor.php',
*
* // The settings contain a custom exception filter
* '/root/path/src/Infrastructure/Mapper/CustomExceptionFilter.php',
*
* // And maybe more…
* ];
* ```
*
* @return list<non-empty-string>
*/
public function for(Type $type): array
{
// Merging the files bound to settings and the files bound to the type
$files = [
...array_map(
fn (callable $callable) => (new ReflectionFunction(Closure::fromCallable($callable)))->getFileName(),
$this->settings->callables(),
),
...$this->filesToWatch($type),
];
// Removing the duplicates
$files = array_unique($files);
// Filtering the empty/invalid file names
$files = array_filter($files, fn ($value) => is_string($value));
/** @var list<non-empty-string> */
return array_values($files);
}
/**
* @param array<non-empty-string> $files
* @return array<non-empty-string>
*/
private function filesToWatch(Type $type, array $files = []): array
{
if (isset($files[$type->toString()])) {
// Prevents infinite loop in case of circular references
return [];
}
foreach (TypeHelper::traverseRecursively($type) as $subType) {
$files = [...$files, ...$this->filesToWatch($subType, $files)];
}
if ($type instanceof ObjectType) {
$fileName = Reflection::class($type->className())->getFileName();
if (! $fileName) {
return [];
}
$files[$type->toString()] = $fileName;
$class = $this->classDefinitionRepository->for($type);
foreach ($class->properties as $property) {
$files = [...$files, ...$this->filesToWatch($property->type, $files)];
}
}
return $files;
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Cache\Warmup;
use CuyZ\Valinor\Cache\Exception\InvalidSignatureToWarmup;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
use CuyZ\Valinor\Mapper\Tree\Builder\InterfaceInferringContainer;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\Parser\TypeParser;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\InterfaceType;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use CuyZ\Valinor\Utility\TypeHelper;
use function in_array;
/** @internal */
final class RecursiveCacheWarmupService
{
/** @var list<class-string> */
private array $classesWarmedUp = [];
public function __construct(
private TypeParser $parser,
private InterfaceInferringContainer $interfaceInferringContainer,
private ClassDefinitionRepository $classDefinitionRepository,
private ObjectBuilderFactory $objectBuilderFactory
) {}
public function warmup(string ...$signatures): void
{
foreach ($signatures as $signature) {
$type = $this->parser->parse($signature);
if ($type instanceof UnresolvableType) {
throw new InvalidSignatureToWarmup($type);
}
$this->warmupType($type);
}
}
private function warmupType(Type $type): void
{
if ($type instanceof InterfaceType) {
$this->warmupInterfaceType($type);
}
if ($type instanceof ClassType) {
$this->warmupClassType($type);
}
foreach (TypeHelper::traverseRecursively($type) as $subType) {
$this->warmupType($subType);
}
}
private function warmupInterfaceType(InterfaceType $type): void
{
$interfaceName = $type->className();
if (! $this->interfaceInferringContainer->has($interfaceName)) {
return;
}
$function = $this->interfaceInferringContainer->inferFunctionFor($interfaceName);
$this->warmupType($function->returnType);
foreach ($function->parameters as $parameter) {
$this->warmupType($parameter->type);
}
}
private function warmupClassType(ClassType $type): void
{
if (in_array($type->className(), $this->classesWarmedUp, true)) {
return;
}
$this->classesWarmedUp[] = $type->className();
$classDefinition = $this->classDefinitionRepository->for($type);
$objectBuilders = $this->objectBuilderFactory->for($classDefinition);
foreach ($objectBuilders as $builder) {
foreach ($builder->describeArguments() as $argument) {
$this->warmupType($argument->type());
}
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler;
use function str_repeat;
use function str_replace;
/** @internal */
final class Compiler
{
private string $code = '';
/** @var non-negative-int */
private int $indentation = 0;
public function compile(Node ...$nodes): self
{
$compiler = $this;
while ($current = array_shift($nodes)) {
$compiler = $current->compile($compiler);
if ($nodes !== []) {
$compiler = $compiler->write("\n");
}
}
return $compiler;
}
public function sub(): self
{
return new self();
}
public function write(string $code): self
{
$self = clone $this;
$self->code .= $code;
return $self;
}
public function indent(): self
{
$self = clone $this;
$self->indentation++;
return $self;
}
public function code(): string
{
$indent = str_repeat(' ', $this->indentation);
return $indent . str_replace("\n", "\n" . $indent, $this->code);
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Library;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
use CuyZ\Valinor\Definition\AttributeDefinition;
use ReflectionClass;
use ReflectionProperty;
use function array_map;
/** @internal */
final class NewAttributeNode extends Node
{
public function __construct(private AttributeDefinition $attribute) {}
public function compile(Compiler $compiler): Compiler
{
if ($this->attribute->arguments !== null) {
return $compiler->compile(
Node::newClass(
$this->attribute->class->name,
...array_map(Node::value(...), $this->attribute->arguments),
),
);
}
// @phpstan-ignore match.unhandled (for now only those two cases can be handled here anyway)
$node = match ($this->attribute->reflectionParts[0]) {
'class' => Node::newClass(ReflectionClass::class, Node::className($this->attribute->reflectionParts[1])),
'property' => Node::newClass(ReflectionProperty::class, Node::className($this->attribute->reflectionParts[1]), Node::value($this->attribute->reflectionParts[2])),
};
return $compiler->compile(
$node->wrap()
->callMethod('getAttributes')
->key(Node::value($this->attribute->attributeIndex))
->callMethod('newInstance'),
);
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Library;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
use CuyZ\Valinor\Compiler\Node;
use CuyZ\Valinor\Type\Type;
/** @internal */
final class TypeAcceptNode extends Node
{
public function __construct(
private ComplianceNode $node,
private Type $type,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->compile($this->type->compiledAccept($this->node));
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
use function array_map;
use function implode;
/** @internal */
final class AnonymousClassNode extends Node
{
/** @var array<Node> */
private array $arguments = [];
/** @var array<interface-string> */
private array $interfaces = [];
/** @var array<PropertyDeclarationNode> */
private array $properties = [];
/** @var array<non-empty-string, MethodNode> */
private array $methods = [];
public function withArguments(Node ...$arguments): self
{
$self = clone $this;
$self->arguments = $arguments;
return $self;
}
/**
* @param interface-string ...$interfaces
*/
public function implements(string ...$interfaces): self
{
$self = clone $this;
$self->interfaces = $interfaces;
return $self;
}
public function withProperties(PropertyDeclarationNode ...$properties): self
{
$self = clone $this;
$self->properties = $properties;
return $self;
}
public function withMethods(MethodNode ...$methods): self
{
$self = clone $this;
foreach ($methods as $method) {
$self->methods[$method->name()] = $method;
}
return $self;
}
public function hasMethod(string $name): bool
{
return isset($this->methods[$name]);
}
public function compile(Compiler $compiler): Compiler
{
$arguments = implode(', ', array_map(
fn (Node $argument) => $compiler->sub()->compile($argument)->code(),
$this->arguments,
));
$compiler = $compiler->write("new class ($arguments)");
if ($this->interfaces !== []) {
$compiler = $compiler->write(
' implements ' . implode(', ', $this->interfaces),
);
}
$body = [
...array_map(
fn (PropertyDeclarationNode $property) => $compiler->sub()->indent()->compile($property)->code(),
$this->properties,
),
...array_map(
fn (MethodNode $method) => $compiler->sub()->indent()->compile($method)->code(),
$this->methods,
),
];
$compiler = $compiler->write(' {');
if ($body !== []) {
$compiler = $compiler->write(PHP_EOL . implode(PHP_EOL . PHP_EOL, $body) . PHP_EOL);
}
return $compiler->write('}');
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ArrayKeyAccessNode extends Node
{
public function __construct(
private Node $node,
private Node $key,
) {}
public function compile(Compiler $compiler): Compiler
{
$key = $compiler->sub()->compile($this->key)->code();
return $compiler
->compile($this->node)
->write('[' . $key . ']');
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
use function var_export;
/** @internal */
final class ArrayNode extends Node
{
public function __construct(
/** @var array<Node> */
private array $assignments
) {}
public function compile(Compiler $compiler): Compiler
{
if ($this->assignments === []) {
return $compiler->write('[]');
}
$sub = [];
foreach ($this->assignments as $key => $assignment) {
$sub[] = ' ' . var_export($key, true) . ' => ' . $compiler->sub()->compile($assignment)->code() . ",";
}
$sub = implode("\n", $sub);
return $compiler->write("[\n$sub\n]");
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class AssignNode extends Node
{
public function __construct(
private Node $node,
private Node $value,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->node)
->write(' = ')
->compile($this->value);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
use function array_map;
use function implode;
/** @internal */
final class CallNode extends Node
{
public function __construct(
private Node $node,
/** @var array<Node> */
private array $arguments = [],
) {}
public function compile(Compiler $compiler): Compiler
{
$compiler = $compiler
->compile($this->node)
->write('(');
if ($this->arguments !== []) {
$arguments = array_map(
fn (Node $argument) => $compiler->sub()->compile($argument)->code(),
$this->arguments,
);
$compiler = $compiler->write(implode(', ', $arguments));
}
return $compiler->write(')');
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class CastNode extends Node
{
private function __construct(
private string $type,
private Node $node,
) {}
public static function toArray(Node $node): self
{
return new self('array', $node);
}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->write('(' . $this->type . ')')
->compile($this->node);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ClassNameNode extends Node
{
public function __construct(
/** @var class-string */
private string $className,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->write($this->className . '::class');
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ClassNode extends Node
{
public function __construct(
/** @var class-string */
private string $name,
) {}
/**
* @param non-empty-string $method
* @param array<Node> $arguments
*/
public function callStaticMethod(
string $method,
array $arguments = [],
): Node {
return new StaticMethodCallNode($this, $method, $arguments);
}
public function compile(Compiler $compiler): Compiler
{
return $compiler->write($this->name);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class CloneNode extends Node
{
public function __construct(
private Node $node,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->write('clone ')
->compile($this->node);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
use function array_map;
use function implode;
/** @internal */
final class ClosureNode extends Node
{
/** @var list<string> */
private array $use = [];
/** @var array<Node> */
private array $nodes;
public function __construct(Node ...$nodes)
{
$this->nodes = $nodes;
}
/**
* @no-named-arguments
* @param non-empty-string ...$names
*/
public function uses(string ...$names): self
{
$self = clone $this;
$self->use = array_map(fn (string $name) => '$' . $name, $names);
return $self;
}
public function compile(Compiler $compiler): Compiler
{
$use = $this->use !== [] ? ' use (' . implode(', ', $this->use) . ')' : '';
$body = $compiler->sub()->indent()->compile(...$this->nodes)->code();
$code = <<<PHP
function ()$use {
$body
}
PHP;
return $compiler->write($code);
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ComplianceNode extends Node
{
public function __construct(private Node $node) {}
/**
* @param non-empty-string $value
*/
public function access(string $value): self
{
return new self(new VariableAccessNode($this, $value));
}
/**
* @no-named-arguments
*/
public function and(Node ...$nodes): self
{
return new self(new LogicalAndNode($this, ...$nodes));
}
public function castToArray(): self
{
return new self(CastNode::toArray($this));
}
public function clone(): self
{
return new self(new CloneNode($this));
}
public function key(Node $key): self
{
return new self(new ArrayKeyAccessNode($this, $key));
}
public function assign(Node $value): self
{
return new self(new AssignNode($this, $value));
}
/**
* @param array<Node> $arguments
*/
public function call(array $arguments = []): self
{
return new self(new CallNode($this, $arguments));
}
/**
* @param non-empty-string $method
* @param array<Node> $arguments
*/
public function callMethod(string $method, array $arguments = []): self
{
return new self(new MethodCallNode($this, $method, $arguments));
}
public function different(Node $right): self
{
return new self(new DifferentNode($this, $right));
}
public function equals(Node $right): self
{
return new self(new EqualsNode($this, $right));
}
/**
* @no-named-arguments
*/
public function or(Node ...$nodes): self
{
return new self(new LogicalOrNode($this, ...$nodes));
}
/**
* @param class-string $className
*/
public function instanceOf(string $className): self
{
return new self(new InstanceOfNode($this, $className));
}
public function isLessThan(Node $right): self
{
return new self(new LessThanNode($this, $right));
}
public function isLessOrEqualsTo(Node $right): self
{
return new self(new LessOrEqualsToNode($this, $right));
}
public function isGreaterThan(Node $right): self
{
return new self(new GreaterThanNode($this, $right));
}
public function isGreaterOrEqualsTo(Node $right): self
{
return new self(new GreaterOrEqualsToNode($this, $right));
}
public function compile(Compiler $compiler): Compiler
{
return $compiler->compile($this->node);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class DifferentNode extends Node
{
public function __construct(
private Node $left,
private Node $right,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->left)
->write(' !== ')
->compile($this->right);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class EqualsNode extends Node
{
public function __construct(
private Node $left,
private Node $right,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->left)
->write(' === ')
->compile($this->right);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ExpressionNode extends Node
{
public function __construct(private Node $node) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->write($compiler->sub()->compile($this->node)->code() . ';');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ForEachNode extends Node
{
public function __construct(
private Node $value,
/** @var non-empty-string */
private string $key,
/** @var non-empty-string */
private string $item,
private Node $body,
) {}
public function compile(Compiler $compiler): Compiler
{
$value = $compiler->sub()->compile($this->value)->code();
$body = $compiler->sub()->indent()->compile($this->body)->code();
return $compiler->write(
<<<PHP
foreach ($value as $$this->key => $$this->item) {
$body
}
PHP
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class FunctionCallNode extends Node
{
public function __construct(
/** @var non-empty-string */
private string $name,
/** @var array<Node> */
private array $arguments = [],
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile(new CallNode(new FunctionNameNode($this->name), $this->arguments));
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
use function in_array;
/** @internal */
final class FunctionNameNode extends Node
{
private const RESERVED_FUNCTIONS = [
'isset',
];
public function __construct(
/** @var non-empty-string */
private string $name
) {}
public function compile(Compiler $compiler): Compiler
{
$function = in_array($this->name, self::RESERVED_FUNCTIONS, true)
? $this->name
: '\\' . $this->name;
return $compiler->write($function);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class GreaterOrEqualsToNode extends Node
{
public function __construct(
private Node $left,
private Node $right,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->left)
->write(' >= ')
->compile($this->right);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class GreaterThanNode extends Node
{
public function __construct(
private Node $left,
private Node $right,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->left)
->write(' > ')
->compile($this->right);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class IfNode extends Node
{
public function __construct(
private Node $condition,
private Node $body,
) {}
public function compile(Compiler $compiler): Compiler
{
$condition = $compiler->sub()->compile($this->condition)->code();
$body = $compiler->sub()->indent()->compile($this->body)->code();
return $compiler->write(
<<<PHP
if ($condition) {
$body
}
PHP,
);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class InstanceOfNode extends Node
{
public function __construct(
private Node $node,
/** @var class-string */
private string $className,
) {}
public function compile(Compiler $compiler): Compiler
{
$className = $this->className;
return $compiler
->compile($this->node)
->write(' instanceof ')
->write($className);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class LessOrEqualsToNode extends Node
{
public function __construct(
private Node $left,
private Node $right,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->left)
->write(' <= ')
->compile($this->right);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class LessThanNode extends Node
{
public function __construct(
private Node $left,
private Node $right,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->left)
->write(' < ')
->compile($this->right);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class LogicalAndNode extends Node
{
/** @var list<Node> */
private array $nodes;
/**
* @no-named-arguments
*/
public function __construct(Node ...$nodes)
{
$this->nodes = $nodes;
}
public function compile(Compiler $compiler): Compiler
{
$nodes = $this->nodes;
while ($node = array_shift($nodes)) {
$compiler = $compiler->compile($node);
if ($nodes !== []) {
$compiler = $compiler->write(' && ');
}
}
return $compiler;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class LogicalOrNode extends Node
{
/** @var list<Node> */
private array $nodes;
/**
* @no-named-arguments
*/
public function __construct(Node ...$nodes)
{
$this->nodes = $nodes;
}
public function compile(Compiler $compiler): Compiler
{
$nodes = $this->nodes;
while ($node = array_shift($nodes)) {
$compiler = $compiler->compile($node);
if ($nodes !== []) {
$compiler = $compiler->write(' || ');
}
}
return $compiler;
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class MatchNode extends Node
{
/** @var list<array{condition: Node, body: Node}> */
private array $cases = [];
private Node $defaultCase;
public function __construct(private Node $value) {}
public function withCase(Node $condition, Node $body): self
{
$self = clone $this;
$self->cases[] = ['condition' => $condition, 'body' => $body];
return $self;
}
public function withDefaultCase(Node $defaultCase): self
{
$self = clone $this;
$self->defaultCase = $defaultCase;
return $self;
}
public function compile(Compiler $compiler): Compiler
{
$value = $compiler->sub()->compile($this->value)->code();
$body = [];
foreach ($this->cases as $case) {
$body[] = $compiler->sub()->indent()->compile($case['condition'])->code() .
' => ' .
$compiler->sub()->compile($case['body'])->code() .
',';
}
if (isset($this->defaultCase)) {
$body[] = $compiler->sub()->indent()->write('default')->code() .
' => ' .
$compiler->sub()->compile($this->defaultCase)->code() .
',';
}
$body = implode("\n", $body);
return $compiler->write(
<<<PHP
match ($value) {
$body
}
PHP,
);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class MethodCallNode extends Node
{
public function __construct(
private Node $node,
/** @var non-empty-string */
private string $method,
/** @var array<Node> */
private array $arguments = [],
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->compile(
new CallNode(new VariableAccessNode($this->node, $this->method), $this->arguments)
);
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
use function array_map;
/** @internal */
final class MethodNode extends Node
{
/** @var 'public'|'private' */
private string $visibility = 'private';
/** @var non-empty-string */
private string $name;
private string $returnType;
/** @var array<ParameterDeclarationNode> */
private array $parameters = [];
/** @var array<Node> */
private array $nodes = [];
/**
* @param non-empty-string $name
*/
public function __construct(string $name)
{
$this->name = $name;
}
public static function constructor(): self
{
return new self('__construct');
}
/**
* @return non-empty-string
*/
public function name(): string
{
return $this->name;
}
public function witParameters(ParameterDeclarationNode ...$parameters): self
{
$self = clone $this;
$self->parameters = $parameters;
return $self;
}
/**
* @param 'public'|'private' $visibility
*/
public function withVisibility(string $visibility): self
{
$self = clone $this;
$self->visibility = $visibility;
return $self;
}
/**
* @param non-empty-string $type
*/
public function withReturnType(string $type): self
{
$self = clone $this;
$self->returnType = $type;
return $self;
}
public function withBody(Node ...$nodes): self
{
$self = clone $this;
$self->nodes = $nodes;
return $self;
}
public function compile(Compiler $compiler): Compiler
{
$parameters = implode(', ', array_map(
fn (ParameterDeclarationNode $parameter) => $compiler->sub()->compile($parameter)->code(),
$this->parameters,
));
$compiler = $compiler->write("$this->visibility function $this->name($parameters)");
if ($this->name !== '__construct') {
$compiler = $compiler->write(': ' . ($this->returnType ?? 'void'));
}
if ($this->nodes === []) {
return $compiler->write(' {}');
}
$body = $compiler->sub()->indent()->compile(...$this->nodes)->code();
return $compiler->write(PHP_EOL . '{' . PHP_EOL . $body . PHP_EOL . '}');
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class NegateNode extends Node
{
public function __construct(private Node $node) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->write('! ')->compile($this->node);
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
use function array_map;
/** @internal */
final class NewClassNode extends Node
{
/** @var class-string */
private string $className;
/** @var array<Node> */
private array $arguments;
/**
* @param class-string $className
*/
public function __construct(string $className, Node ...$arguments)
{
$this->className = $className;
$this->arguments = $arguments;
}
public function compile(Compiler $compiler): Compiler
{
$arguments = array_map(
fn (Node $argument) => $compiler->sub()->compile($argument)->code(),
$this->arguments,
);
$arguments = implode(', ', $arguments);
return $compiler->write("new {$this->className}($arguments)");
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ParameterDeclarationNode extends Node
{
public function __construct(
/** @var non-empty-string */
private string $name,
private string $type,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->write($this->type . ' $' . $this->name);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class PhpFileNode extends Node
{
/** @var array<Node> */
private array $nodes;
public function __construct(Node ...$nodes)
{
$this->nodes = $nodes;
}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->write("<?php\n\ndeclare(strict_types=1);\n\n")
->compile(...$this->nodes);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class PropertyDeclarationNode extends Node
{
public function __construct(
/** @var non-empty-string */
private string $name,
private string $type,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->write('private ' . $this->type . ' $' . $this->name . ';');
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class PropertyNode extends Node
{
public function __construct(private string $name) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->write('$this->' . $this->name);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ReturnNode extends Node
{
public function __construct(private ?Node $node = null) {}
public function compile(Compiler $compiler): Compiler
{
$code = $this->node ? ' ' . $compiler->sub()->compile($this->node)->code() : '';
return $compiler->write("return$code;");
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ShortClosureNode extends Node
{
private Node $returnNode;
/** @var array<ParameterDeclarationNode> */
private array $parameters = [];
public function __construct(Node $returnNode)
{
$this->returnNode = $returnNode;
}
public function witParameters(ParameterDeclarationNode ...$parameters): self
{
$self = clone $this;
$self->parameters = $parameters;
return $self;
}
public function compile(Compiler $compiler): Compiler
{
$parameters = implode(', ', array_map(
fn (ParameterDeclarationNode $parameter) => $compiler->sub()->compile($parameter)->code(),
$this->parameters,
));
$return = $compiler->sub()->compile($this->returnNode)->code();
return $compiler->write(
<<<PHP
fn ($parameters) => $return
PHP,
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class StaticAccessNode extends Node
{
public function __construct(
private Node $left,
/** @var non-empty-string */
private string $name,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->left)
->write("::$this->name");
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class StaticMethodCallNode extends Node
{
public function __construct(
private Node $node,
/** @var non-empty-string */
private string $method,
/** @var array<Node> */
private array $arguments = [],
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->compile(
new CallNode(new StaticAccessNode($this->node, $this->method), $this->arguments)
);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class TernaryNode extends Node
{
public function __construct(
private Node $condition,
private Node $ifTrue,
private Node $ifFalse,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->condition)
->write(' ? ')
->compile($this->ifTrue)
->write(' : ')
->compile($this->ifFalse);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class ThrowNode extends Node
{
public function __construct(
private Node $node,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->write('throw ')
->compile($this->node);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
use function is_array;
use function var_export;
/** @internal */
final class ValueNode extends Node
{
public function __construct(
/** @var array<mixed>|bool|float|int|string|null */
private array|bool|float|int|string|null $value,
) {}
public function compile(Compiler $compiler): Compiler
{
return $this->compileValue($this->value, $compiler);
}
private function compileValue(mixed $value, Compiler $compiler): Compiler
{
if (is_array($value)) {
$compiler = $compiler->write('[');
$i = 0;
$numItems = count($value);
foreach ($value as $key => $item) {
$compiler = $compiler->write(var_export($key, true) . ' => ');
$compiler = $this->compileValue($item, $compiler);
if (++$i !== $numItems) {
$compiler = $compiler->write(', ');
}
}
$compiler = $compiler->write(']');
} else {
$compiler = $compiler->write(var_export($value, true));
}
return $compiler;
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class VariableAccessNode extends Node
{
public function __construct(
private Node $node,
/** @var non-empty-string */
private string $value
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->compile($this->node)
->write('->' . $this->value);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class VariableNode extends Node
{
public function __construct(private string $name) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler->write('$' . $this->name);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class WrapNode extends Node
{
public function __construct(private Node $node) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->write('(')
->compile($this->node)
->write(')');
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler\Native;
use CuyZ\Valinor\Compiler\Compiler;
use CuyZ\Valinor\Compiler\Node;
/** @internal */
final class YieldNode extends Node
{
public function __construct(
private Node $key,
private Node $value,
) {}
public function compile(Compiler $compiler): Compiler
{
return $compiler
->write('yield ')
->compile($this->key)
->write(' => ')
->compile($this->value);
}
}

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Compiler;
use CuyZ\Valinor\Compiler\Native\AnonymousClassNode;
use CuyZ\Valinor\Compiler\Native\ArrayNode;
use CuyZ\Valinor\Compiler\Native\ClassNameNode;
use CuyZ\Valinor\Compiler\Native\ClassNode;
use CuyZ\Valinor\Compiler\Native\ClosureNode;
use CuyZ\Valinor\Compiler\Native\ComplianceNode;
use CuyZ\Valinor\Compiler\Native\ExpressionNode;
use CuyZ\Valinor\Compiler\Native\ForEachNode;
use CuyZ\Valinor\Compiler\Native\FunctionCallNode;
use CuyZ\Valinor\Compiler\Native\IfNode;
use CuyZ\Valinor\Compiler\Native\LogicalAndNode;
use CuyZ\Valinor\Compiler\Native\LogicalOrNode;
use CuyZ\Valinor\Compiler\Native\MatchNode;
use CuyZ\Valinor\Compiler\Native\MethodNode;
use CuyZ\Valinor\Compiler\Native\NegateNode;
use CuyZ\Valinor\Compiler\Native\NewClassNode;
use CuyZ\Valinor\Compiler\Native\ParameterDeclarationNode;
use CuyZ\Valinor\Compiler\Native\PropertyDeclarationNode;
use CuyZ\Valinor\Compiler\Native\PropertyNode;
use CuyZ\Valinor\Compiler\Native\ReturnNode;
use CuyZ\Valinor\Compiler\Native\ShortClosureNode;
use CuyZ\Valinor\Compiler\Native\TernaryNode;
use CuyZ\Valinor\Compiler\Native\ThrowNode;
use CuyZ\Valinor\Compiler\Native\ValueNode;
use CuyZ\Valinor\Compiler\Native\VariableNode;
use CuyZ\Valinor\Compiler\Native\WrapNode;
use CuyZ\Valinor\Compiler\Native\YieldNode;
/** @internal */
abstract class Node
{
abstract public function compile(Compiler $compiler): Compiler;
public function asExpression(): ExpressionNode
{
return new ExpressionNode($this);
}
public function wrap(): ComplianceNode
{
return new ComplianceNode(new WrapNode($this));
}
/**
* @param array<Node> $assignments
*/
public static function array(array $assignments = []): ArrayNode
{
return new ArrayNode($assignments);
}
public static function anonymousClass(): AnonymousClassNode
{
return new AnonymousClassNode();
}
/**
* @param class-string $name
*/
public static function class(string $name): ClassNode
{
return new ClassNode($name);
}
/**
* @param class-string $className
*/
public static function className(string $className): ClassNameNode
{
return new ClassNameNode($className);
}
public static function closure(Node ...$nodes): ClosureNode
{
return new ClosureNode(...$nodes);
}
/**
* @param non-empty-string $key
* @param non-empty-string $item
*/
public static function forEach(Node $value, string $key, string $item, Node $body): ForEachNode
{
return new ForEachNode($value, $key, $item, $body);
}
/**
* @param non-empty-string $name
* @param array<Node> $arguments
*/
public static function functionCall(string $name, array $arguments = []): ComplianceNode
{
return new ComplianceNode(new FunctionCallNode($name, $arguments));
}
public static function if(Node $condition, Node $body): IfNode
{
return new IfNode($condition, $body);
}
/**
* @no-named-arguments
*/
public static function logicalAnd(Node ...$nodes): ComplianceNode
{
return new ComplianceNode(new LogicalAndNode(...$nodes));
}
/**
* @no-named-arguments
*/
public static function logicalOr(Node ...$nodes): ComplianceNode
{
return new ComplianceNode(new LogicalOrNode(...$nodes));
}
public static function match(Node $value): MatchNode
{
return new MatchNode($value);
}
/**
* @param non-empty-string $name
*/
public static function method(string $name): MethodNode
{
return new MethodNode($name);
}
public static function negate(Node $node): NegateNode
{
return new NegateNode($node);
}
/**
* @param class-string $className
*/
public static function newClass(string $className, Node ...$arguments): NewClassNode
{
return new NewClassNode($className, ...$arguments);
}
/**
* @param non-empty-string $name
*/
public static function parameterDeclaration(string $name, string $type): ParameterDeclarationNode
{
return new ParameterDeclarationNode($name, $type);
}
public static function property(string $name): ComplianceNode
{
return new ComplianceNode(new PropertyNode($name));
}
/**
* @param non-empty-string $name
*/
public static function propertyDeclaration(string $name, string $type): PropertyDeclarationNode
{
return new PropertyDeclarationNode($name, $type);
}
public static function return(Node $node): ReturnNode
{
return new ReturnNode($node);
}
public static function shortClosure(Node $return): ShortClosureNode
{
return new ShortClosureNode($return);
}
public static function ternary(Node $condition, Node $ifTrue, Node $ifFalse): TernaryNode
{
return new TernaryNode($condition, $ifTrue, $ifFalse);
}
public static function this(): ComplianceNode
{
return self::variable('this');
}
public static function throw(Node $node): ThrowNode
{
return new ThrowNode($node);
}
/**
* @param array<mixed>|bool|float|int|string|null $value
*/
public static function value(array|bool|float|int|string|null $value): ComplianceNode
{
return new ComplianceNode(new ValueNode($value));
}
public static function variable(string $name): ComplianceNode
{
return new ComplianceNode(new VariableNode($name));
}
public static function yield(Node $key, Node $value): YieldNode
{
return new YieldNode($key, $value);
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use Closure;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
/** @internal */
final class AttributeDefinition
{
/** @var callable */
private mixed $callable;
public function __construct(
public readonly ClassDefinition $class,
/** @var null|list<array<scalar>|scalar> */
public readonly ?array $arguments,
/** @var array{'closure'}
* | array{'class', class-string}
* | array{'property', class-string, string}
* | array{'method', class-string, string}
* | array{'methodParameter', class-string, string, int}
* | array{'closureParameter', int}
*/
public readonly array $reflectionParts,
public readonly int $attributeIndex,
) {}
public function forCallable(callable $callable): self
{
$self = clone $this;
$self->callable = $callable;
return $self;
}
/**
* There are two ways of instantiating an attribute:
*
* 1. If the attribute's arguments contain only scalar, we can directly
* instantiate the attribute using its class and arguments.
* 2. If the attribute arguments contain any object or callable, we are
* forced to use reflection to instantiate it, as it is not possible to
* properly compile it.
*
* The first solution is by far more performant, so we prefer using it when
* possible.
*/
public function instantiate(): object
{
if ($this->arguments !== null) {
return new ($this->class->type->className())(...$this->arguments);
}
$reflection = match ($this->reflectionParts[0]) {
'class' => new ReflectionClass($this->reflectionParts[1]),
'property' => new ReflectionProperty($this->reflectionParts[1], $this->reflectionParts[2]),
'method' => new ReflectionMethod($this->reflectionParts[1], $this->reflectionParts[2]),
'methodParameter' => (new ReflectionMethod($this->reflectionParts[1], $this->reflectionParts[2]))->getParameters()[$this->reflectionParts[3]],
'closure' => new ReflectionFunction(Closure::fromCallable($this->callable)),
'closureParameter' => new ReflectionParameter(Closure::fromCallable($this->callable), $this->reflectionParts[1]),
};
return $reflection->getAttributes()[$this->attributeIndex]->newInstance();
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use Countable;
use IteratorAggregate;
use Traversable;
use function array_filter;
use function array_map;
use function count;
use function is_a;
/**
* @internal
*
* @implements IteratorAggregate<AttributeDefinition>
*/
final class Attributes implements IteratorAggregate, Countable
{
private static self $empty;
/** @var list<AttributeDefinition> */
private array $attributes;
/**
* @no-named-arguments
*/
public function __construct(AttributeDefinition ...$attributes)
{
$this->attributes = $attributes;
}
public static function empty(): self
{
return self::$empty ??= new self();
}
public function forCallable(callable $callable): self
{
return new self(...array_map(
fn (AttributeDefinition $attribute) => $attribute->forCallable($callable),
$this->attributes,
));
}
public function has(string $className): bool
{
foreach ($this->attributes as $attribute) {
if (is_a($attribute->class->type->className(), $className, true)) {
return true;
}
}
return false;
}
/**
* @param callable(AttributeDefinition): bool $callback
*/
public function filter(callable $callback): self
{
return new self(
...array_filter($this->attributes, $callback)
);
}
public function merge(self $other): self
{
$clone = clone $this;
$clone->attributes = [...$this->attributes, ...$other->attributes];
return $clone;
}
public function count(): int
{
return count($this->attributes);
}
/**
* @return list<AttributeDefinition>
*/
public function toArray(): array
{
return $this->attributes;
}
/**
* @return Traversable<AttributeDefinition>
*/
public function getIterator(): Traversable
{
yield from $this->attributes;
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use CuyZ\Valinor\Type\ObjectType;
/** @internal */
final class ClassDefinition
{
public function __construct(
/** @var class-string */
public readonly string $name,
public readonly ObjectType $type,
public readonly Attributes $attributes,
public readonly Properties $properties,
public readonly Methods $methods,
public readonly bool $isFinal,
public readonly bool $isAbstract,
) {}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\Generics;
use CuyZ\Valinor\Utility\TypeHelper;
/** @internal */
final class FunctionDefinition
{
public function __construct(
/** @var non-empty-string */
public readonly string $name,
/** @var non-empty-string */
public readonly string $signature,
public readonly Attributes $attributes,
/** @var non-empty-string|null */
public readonly ?string $fileName,
/** @var class-string|null */
public readonly ?string $class,
public readonly bool $isStatic,
public readonly bool $isClosure,
public readonly Parameters $parameters,
public readonly Type $returnType,
) {}
public function forCallable(callable $callable): self
{
return new self(
$this->name,
$this->signature,
$this->attributes->forCallable($callable),
$this->fileName,
$this->class,
$this->isStatic,
$this->isClosure,
$this->parameters->forCallable($callable),
$this->returnType
);
}
public function assignGenerics(Generics $generics): self
{
if ($generics->items === []) {
return $this;
}
return new self(
$this->name,
$this->signature,
$this->attributes,
$this->fileName,
$this->class,
$this->isStatic,
$this->isClosure,
$this->parameters->assignGenerics($generics),
TypeHelper::assignVacantTypes($this->returnType, $generics->items),
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
/** @internal */
final class FunctionObject
{
public readonly FunctionDefinition $definition;
/** @var callable */
public readonly mixed $callback;
public function __construct(FunctionDefinition $definition, callable $callback)
{
$this->definition = $definition;
$this->callback = $callback;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use Countable;
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
use IteratorAggregate;
use Traversable;
use function array_keys;
use function count;
use function iterator_to_array;
/**
* @internal
*
* @implements IteratorAggregate<string|int, FunctionObject>
*/
final class FunctionsContainer implements IteratorAggregate, Countable
{
/** @var array<FunctionObject> */
private array $functions = [];
public function __construct(
private FunctionDefinitionRepository $functionDefinitionRepository,
/** @var array<callable> */
private array $callables
) {}
public function has(string|int $key): bool
{
return isset($this->callables[$key]);
}
public function get(string|int $key): FunctionObject
{
return $this->function($key);
}
/**
* @return array<FunctionObject>
*/
public function toArray(): array
{
return iterator_to_array($this);
}
public function getIterator(): Traversable
{
foreach (array_keys($this->callables) as $key) {
yield $key => $this->function($key);
}
}
private function function(string|int $key): FunctionObject
{
return $this->functions[$key] ??= new FunctionObject(
$this->functionDefinitionRepository->for($this->callables[$key]),
$this->callables[$key]
);
}
public function count(): int
{
return count($this->callables);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use CuyZ\Valinor\Type\Type;
/** @internal */
final class MethodDefinition
{
public function __construct(
/** @var non-empty-string */
public readonly string $name,
/** @var non-empty-string */
public readonly string $signature,
public readonly Attributes $attributes,
public readonly Parameters $parameters,
public readonly bool $isStatic,
public readonly bool $isPublic,
public readonly Type $returnType
) {}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use Countable;
use IteratorAggregate;
use Traversable;
/**
* @internal
*
* @implements IteratorAggregate<string, MethodDefinition>
*/
final class Methods implements IteratorAggregate, Countable
{
/** @var MethodDefinition[] */
private array $methods = [];
public function __construct(MethodDefinition ...$methods)
{
foreach ($methods as $method) {
$this->methods[$method->name] = $method;
}
}
public function has(string $name): bool
{
return isset($this->methods[$name]);
}
public function get(string $name): MethodDefinition
{
return $this->methods[$name];
}
public function hasConstructor(): bool
{
return $this->has('__construct');
}
public function constructor(): MethodDefinition
{
return $this->get('__construct');
}
public function count(): int
{
return count($this->methods);
}
/**
* @return Traversable<string, MethodDefinition>
*/
public function getIterator(): Traversable
{
yield from $this->methods;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\Generics;
use CuyZ\Valinor\Utility\TypeHelper;
/** @internal */
final class ParameterDefinition
{
public function __construct(
/** @var non-empty-string */
public readonly string $name,
/** @var non-empty-string */
public readonly string $signature,
public readonly Type $type,
public readonly Type $nativeType,
public readonly bool $isOptional,
public readonly bool $isVariadic,
public readonly mixed $defaultValue,
public readonly Attributes $attributes
) {}
public function forCallable(callable $callable): self
{
return new self(
$this->name,
$this->signature,
$this->type,
$this->nativeType,
$this->isOptional,
$this->isVariadic,
$this->defaultValue,
$this->attributes->forCallable($callable)
);
}
public function assignGenerics(Generics $generics): self
{
assert($generics->items !== []);
return new self(
$this->name,
$this->signature,
TypeHelper::assignVacantTypes($this->type, $generics->items),
$this->nativeType,
$this->isOptional,
$this->isVariadic,
$this->defaultValue,
$this->attributes
);
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use Countable;
use CuyZ\Valinor\Type\Types\Generics;
use IteratorAggregate;
use Traversable;
use function array_map;
use function array_values;
/**
* @internal
*
* @implements IteratorAggregate<string, ParameterDefinition>
*/
final class Parameters implements IteratorAggregate, Countable
{
/** @var array<non-empty-string, ParameterDefinition> */
private array $parameters = [];
public function __construct(ParameterDefinition ...$parameters)
{
foreach ($parameters as $parameter) {
$this->parameters[$parameter->name] = $parameter;
}
}
public function has(string $name): bool
{
return isset($this->parameters[$name]);
}
public function get(string $name): ParameterDefinition
{
return $this->parameters[$name];
}
/**
* @param int<0, max> $index
*/
public function at(int $index): ParameterDefinition
{
return array_values($this->parameters)[$index];
}
public function assignGenerics(Generics $generics): self
{
return new self(
...array_map(
static fn (ParameterDefinition $parameter) => $parameter->assignGenerics($generics),
$this->parameters,
),
);
}
/**
* @return array<non-empty-string, ParameterDefinition>
*/
public function toArray(): array
{
return $this->parameters;
}
public function count(): int
{
return count($this->parameters);
}
public function forCallable(callable $callable): self
{
return new self(...array_map(
fn (ParameterDefinition $parameter) => $parameter->forCallable($callable),
$this->parameters
));
}
/**
* @return Traversable<string, ParameterDefinition>
*/
public function getIterator(): Traversable
{
yield from $this->parameters;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use Countable;
use IteratorAggregate;
use Traversable;
/**
* @internal
*
* @implements IteratorAggregate<string, PropertyDefinition>
*/
final class Properties implements IteratorAggregate, Countable
{
/** @var PropertyDefinition[] */
private array $properties = [];
public function __construct(PropertyDefinition ...$properties)
{
foreach ($properties as $property) {
$this->properties[$property->name] = $property;
}
}
public function has(string $name): bool
{
return isset($this->properties[$name]);
}
public function get(string $name): PropertyDefinition
{
return $this->properties[$name];
}
public function count(): int
{
return count($this->properties);
}
/**
* @return Traversable<string, PropertyDefinition>
*/
public function getIterator(): Traversable
{
yield from $this->properties;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition;
use CuyZ\Valinor\Type\Type;
/** @internal */
final class PropertyDefinition
{
public function __construct(
/** @var non-empty-string */
public readonly string $name,
/** @var non-empty-string */
public readonly string $signature,
public readonly Type $type,
public readonly Type $nativeType,
public readonly bool $hasDefaultValue,
public readonly mixed $defaultValue,
public readonly bool $isPublic,
public readonly Attributes $attributes
) {}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository;
use CuyZ\Valinor\Definition\AttributeDefinition;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use Reflector;
/** @internal */
interface AttributesRepository
{
/**
* @param ReflectionClass<covariant object>|ReflectionProperty|ReflectionMethod|ReflectionFunction|ReflectionParameter $reflection
* @return list<AttributeDefinition>
*/
public function for(Reflector $reflection): array;
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache;
use CuyZ\Valinor\Cache\Cache;
use CuyZ\Valinor\Cache\CacheEntry;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Definition\Repository\Cache\Compiler\ClassDefinitionCompiler;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Utility\Reflection\Reflection;
use function is_string;
/** @internal */
final class CompiledClassDefinitionRepository implements ClassDefinitionRepository
{
public function __construct(
private ClassDefinitionRepository $delegate,
/** @var Cache<ClassDefinition> */
private Cache $cache,
private ClassDefinitionCompiler $compiler,
) {}
public function for(ObjectType $type): ClassDefinition
{
// @infection-ignore-all
$key = "class-definition-\0" . $type->toString();
$entry = $this->cache->get($key);
if ($entry) {
return $entry;
}
$class = $this->delegate->for($type);
$code = 'fn () => ' . $this->compiler->compile($class);
$filesToWatch = $this->filesToWatch($type);
$this->cache->set($key, new CacheEntry($code, $filesToWatch));
/** @var ClassDefinition */
return $this->cache->get($key);
}
/**
* @return list<non-empty-string>
*/
private function filesToWatch(ObjectType $type): array
{
$reflection = Reflection::class($type->className());
$fileNames = [];
do {
$fileName = $reflection->getFileName();
if (is_string($fileName)) {
$fileNames[] = $fileName;
}
} while ($reflection = $reflection->getParentClass());
return $fileNames;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache;
use CuyZ\Valinor\Cache\Cache;
use CuyZ\Valinor\Cache\CacheEntry;
use CuyZ\Valinor\Definition\FunctionDefinition;
use CuyZ\Valinor\Definition\Repository\Cache\Compiler\FunctionDefinitionCompiler;
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
use CuyZ\Valinor\Utility\Reflection\Reflection;
/** @internal */
final class CompiledFunctionDefinitionRepository implements FunctionDefinitionRepository
{
public function __construct(
private FunctionDefinitionRepository $delegate,
/** @var Cache<FunctionDefinition> */
private Cache $cache,
private FunctionDefinitionCompiler $compiler,
) {}
public function for(callable $function): FunctionDefinition
{
$reflection = Reflection::function($function);
// @infection-ignore-all
$key = "function-definition-\0" . $reflection->getFileName() . ':' . $reflection->getStartLine() . '-' . $reflection->getEndLine();
$entry = $this->cache->get($key);
if ($entry) {
return $entry->forCallable($function);
}
$definition = $this->delegate->for($function);
$code = 'fn () => ' . $this->compiler->compile($definition);
$filesToWatch = $definition->fileName ? [$definition->fileName] : [];
$this->cache->set($key, new CacheEntry($code, $filesToWatch));
/** @var FunctionDefinition */
return $this->cache->get($key)?->forCallable($function);
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
use CuyZ\Valinor\Definition\Attributes;
use function array_map;
use function count;
use function implode;
use function var_export;
/** @internal */
final class AttributesCompiler
{
public function __construct(private ClassDefinitionCompiler $classDefinitionCompiler) {}
public function compile(Attributes $attributes): string
{
if (count($attributes) === 0) {
return Attributes::class . '::empty()';
}
$attributesListCode = $this->compileAttributes($attributes);
return <<<PHP
new \CuyZ\Valinor\Definition\Attributes($attributesListCode)
PHP;
}
private function compileAttributes(Attributes $attributes): string
{
$attributesListCode = [];
foreach ($attributes as $attribute) {
$class = $this->classDefinitionCompiler->compile($attribute->class);
$argumentsCode = $this->compileAttributeArguments($attribute->arguments);
$reflectionParts = var_export($attribute->reflectionParts, true);
$attributeIndex = var_export($attribute->attributeIndex, true);
$attributesListCode[] = <<<PHP
new \CuyZ\Valinor\Definition\AttributeDefinition(
$class,
$argumentsCode,
$reflectionParts,
$attributeIndex,
)
PHP;
}
return implode(', ', $attributesListCode);
}
/**
* @param null|list<array<scalar>|scalar> $arguments
*/
private function compileAttributeArguments(?array $arguments): string
{
if ($arguments === null) {
return 'null';
}
$code = array_map(static fn ($value) => var_export($value, true), $arguments);
return '[' . implode(', ', $code) . ']';
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Definition\MethodDefinition;
use CuyZ\Valinor\Definition\PropertyDefinition;
use function array_map;
use function implode;
use function iterator_to_array;
use function var_export;
/** @internal */
final class ClassDefinitionCompiler
{
private TypeCompiler $typeCompiler;
private AttributesCompiler $attributesCompiler;
private MethodDefinitionCompiler $methodCompiler;
private PropertyDefinitionCompiler $propertyCompiler;
public function __construct()
{
$this->attributesCompiler = new AttributesCompiler($this);
$this->typeCompiler = new TypeCompiler($this->attributesCompiler);
$this->methodCompiler = new MethodDefinitionCompiler($this->typeCompiler, $this->attributesCompiler);
$this->propertyCompiler = new PropertyDefinitionCompiler($this->typeCompiler, $this->attributesCompiler);
}
public function compile(ClassDefinition $value): string
{
$name = var_export($value->name, true);
$type = $this->typeCompiler->compile($value->type);
$properties = array_map(
fn (PropertyDefinition $property) => $this->propertyCompiler->compile($property),
iterator_to_array($value->properties)
);
$properties = implode(', ', $properties);
$methods = array_map(
fn (MethodDefinition $method) => $this->methodCompiler->compile($method),
iterator_to_array($value->methods)
);
$methods = implode(', ', $methods);
$attributes = $this->attributesCompiler->compile($value->attributes);
$isFinal = var_export($value->isFinal, true);
$isAbstract = var_export($value->isAbstract, true);
return <<<PHP
new \CuyZ\Valinor\Definition\ClassDefinition(
$name,
$type,
$attributes,
new \CuyZ\Valinor\Definition\Properties($properties),
new \CuyZ\Valinor\Definition\Methods($methods),
$isFinal,
$isAbstract,
)
PHP;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler\Exception;
use CuyZ\Valinor\Type\Type;
use LogicException;
/** @internal */
final class TypeCannotBeCompiled extends LogicException
{
public function __construct(Type $type)
{
$class = $type::class;
parent::__construct("The type `$class` cannot be compiled.");
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
use CuyZ\Valinor\Definition\FunctionDefinition;
use CuyZ\Valinor\Definition\ParameterDefinition;
use function var_export;
/** @internal */
final class FunctionDefinitionCompiler
{
private TypeCompiler $typeCompiler;
private AttributesCompiler $attributesCompiler;
private ParameterDefinitionCompiler $parameterCompiler;
public function __construct()
{
$this->attributesCompiler = new AttributesCompiler(new ClassDefinitionCompiler());
$this->typeCompiler = new TypeCompiler($this->attributesCompiler);
$this->parameterCompiler = new ParameterDefinitionCompiler($this->typeCompiler, $this->attributesCompiler);
}
public function compile(FunctionDefinition $value): string
{
$parameters = array_map(
fn (ParameterDefinition $parameter) => $this->parameterCompiler->compile($parameter),
iterator_to_array($value->parameters)
);
$attributes = $this->attributesCompiler->compile($value->attributes);
$fileName = var_export($value->fileName, true);
$class = var_export($value->class, true);
$isStatic = var_export($value->isStatic, true);
$isClosure = var_export($value->isClosure, true);
$parameters = implode(', ', $parameters);
$returnType = $this->typeCompiler->compile($value->returnType);
return <<<PHP
new \CuyZ\Valinor\Definition\FunctionDefinition(
'{$value->name}',
'{$value->signature}',
$attributes,
$fileName,
$class,
$isStatic,
$isClosure,
new \CuyZ\Valinor\Definition\Parameters($parameters),
$returnType
)
PHP;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
use CuyZ\Valinor\Definition\MethodDefinition;
use CuyZ\Valinor\Definition\ParameterDefinition;
use function var_export;
/** @internal */
final class MethodDefinitionCompiler
{
private TypeCompiler $typeCompiler;
private AttributesCompiler $attributesCompiler;
private ParameterDefinitionCompiler $parameterCompiler;
public function __construct(TypeCompiler $typeCompiler, AttributesCompiler $attributesCompiler)
{
$this->typeCompiler = $typeCompiler;
$this->attributesCompiler = $attributesCompiler;
$this->parameterCompiler = new ParameterDefinitionCompiler($typeCompiler, $attributesCompiler);
}
public function compile(MethodDefinition $method): string
{
$attributes = $this->attributesCompiler->compile($method->attributes);
$parameters = array_map(
fn (ParameterDefinition $parameter) => $this->parameterCompiler->compile($parameter),
iterator_to_array($method->parameters)
);
$parameters = implode(', ', $parameters);
$isStatic = var_export($method->isStatic, true);
$isPublic = var_export($method->isPublic, true);
$returnType = $this->typeCompiler->compile($method->returnType);
return <<<PHP
new \CuyZ\Valinor\Definition\MethodDefinition(
'{$method->name}',
'{$method->signature}',
$attributes,
new \CuyZ\Valinor\Definition\Parameters($parameters),
$isStatic,
$isPublic,
$returnType
)
PHP;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
use CuyZ\Valinor\Definition\ParameterDefinition;
/** @internal */
final class ParameterDefinitionCompiler
{
public function __construct(
private TypeCompiler $typeCompiler,
private AttributesCompiler $attributesCompiler
) {}
public function compile(ParameterDefinition $parameter): string
{
$isOptional = var_export($parameter->isOptional, true);
$isVariadic = var_export($parameter->isVariadic, true);
$defaultValue = $this->defaultValue($parameter);
$type = $this->typeCompiler->compile($parameter->type);
$nativeType = $this->typeCompiler->compile($parameter->nativeType);
$attributes = $this->attributesCompiler->compile($parameter->attributes);
return <<<PHP
new \CuyZ\Valinor\Definition\ParameterDefinition(
'{$parameter->name}',
'{$parameter->signature}',
$type,
$nativeType,
$isOptional,
$isVariadic,
$defaultValue,
$attributes
)
PHP;
}
private function defaultValue(ParameterDefinition $parameter): string
{
$defaultValue = $parameter->defaultValue;
return is_object($defaultValue)
? 'unserialize(' . var_export(serialize($defaultValue), true) . ')'
: var_export($defaultValue, true);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
use CuyZ\Valinor\Definition\PropertyDefinition;
/** @internal */
final class PropertyDefinitionCompiler
{
public function __construct(
private TypeCompiler $typeCompiler,
private AttributesCompiler $attributesCompiler
) {}
public function compile(PropertyDefinition $property): string
{
$type = $this->typeCompiler->compile($property->type);
$nativeType = $this->typeCompiler->compile($property->nativeType);
$hasDefaultValue = var_export($property->hasDefaultValue, true);
$defaultValue = var_export($property->defaultValue, true);
$isPublic = var_export($property->isPublic, true);
$attributes = $this->attributesCompiler->compile($property->attributes);
return <<<PHP
new \CuyZ\Valinor\Definition\PropertyDefinition(
'{$property->name}',
'{$property->signature}',
$type,
$nativeType,
$hasDefaultValue,
$defaultValue,
$isPublic,
$attributes
)
PHP;
}
}

View File

@@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache\Compiler;
use CuyZ\Valinor\Definition\Repository\Cache\Compiler\Exception\TypeCannotBeCompiled;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\ArrayKeyType;
use CuyZ\Valinor\Type\Types\ArrayType;
use CuyZ\Valinor\Type\Types\BooleanValueType;
use CuyZ\Valinor\Type\Types\CallableType;
use CuyZ\Valinor\Type\Types\ClassStringType;
use CuyZ\Valinor\Type\Types\EnumType;
use CuyZ\Valinor\Type\Types\FloatValueType;
use CuyZ\Valinor\Type\Types\GenericType;
use CuyZ\Valinor\Type\Types\IntegerRangeType;
use CuyZ\Valinor\Type\Types\IntegerValueType;
use CuyZ\Valinor\Type\Types\InterfaceType;
use CuyZ\Valinor\Type\Types\IntersectionType;
use CuyZ\Valinor\Type\Types\IterableType;
use CuyZ\Valinor\Type\Types\ListType;
use CuyZ\Valinor\Type\Types\MixedType;
use CuyZ\Valinor\Type\Types\NativeBooleanType;
use CuyZ\Valinor\Type\Types\NativeClassType;
use CuyZ\Valinor\Type\Types\NativeFloatType;
use CuyZ\Valinor\Type\Types\NativeIntegerType;
use CuyZ\Valinor\Type\Types\NativeStringType;
use CuyZ\Valinor\Type\Types\NegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonEmptyArrayType;
use CuyZ\Valinor\Type\Types\NonEmptyListType;
use CuyZ\Valinor\Type\Types\NonEmptyStringType;
use CuyZ\Valinor\Type\Types\NonNegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonPositiveIntegerType;
use CuyZ\Valinor\Type\Types\NullType;
use CuyZ\Valinor\Type\Types\NumericStringType;
use CuyZ\Valinor\Type\Types\PositiveIntegerType;
use CuyZ\Valinor\Type\Types\ScalarConcreteType;
use CuyZ\Valinor\Type\Types\ShapedArrayElement;
use CuyZ\Valinor\Type\Types\ShapedArrayType;
use CuyZ\Valinor\Type\Types\StringValueType;
use CuyZ\Valinor\Type\Types\UndefinedObjectType;
use CuyZ\Valinor\Type\Types\UnionType;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use UnitEnum;
use function array_keys;
use function array_map;
use function implode;
use function var_export;
/** @internal */
final class TypeCompiler
{
public function __construct(
private AttributesCompiler $attributesCompiler,
) {}
public function compile(Type $type): string
{
$class = $type::class;
switch (true) {
case $type instanceof NullType:
case $type instanceof NativeBooleanType:
case $type instanceof NativeFloatType:
case $type instanceof NativeIntegerType:
case $type instanceof PositiveIntegerType:
case $type instanceof NegativeIntegerType:
case $type instanceof NonPositiveIntegerType:
case $type instanceof NonNegativeIntegerType:
case $type instanceof NativeStringType:
case $type instanceof NonEmptyStringType:
case $type instanceof NumericStringType:
case $type instanceof UndefinedObjectType:
case $type instanceof MixedType:
case $type instanceof ScalarConcreteType:
return "$class::get()";
case $type instanceof BooleanValueType:
return $type->value() === true
? "$class::true()"
: "$class::false()";
case $type instanceof IntegerRangeType:
return "new $class({$type->min()}, {$type->max()})";
case $type instanceof StringValueType:
$value = var_export($type->toString(), true);
return "$class::from($value)";
case $type instanceof IntegerValueType:
case $type instanceof FloatValueType:
$value = var_export($type->value(), true);
return "new $class($value)";
case $type instanceof IntersectionType:
case $type instanceof UnionType:
$subTypes = array_map(
fn (Type $subType) => $this->compile($subType),
$type->types()
);
return "new $class(" . implode(', ', $subTypes) . ')';
case $type instanceof ArrayKeyType:
return match ($type->toString()) {
'array-key' => "$class::default()",
'string' => "$class::string()",
'int' => "$class::integer()",
default => (function () use ($type, $class) {
$types = array_map(
fn (Type $subType) => $this->compile($subType),
$type->types,
);
return "new $class([" . implode(', ', $types) . '])';
})(),
};
case $type instanceof ShapedArrayType:
$elements = [];
foreach ($type->elements as $key => $element) {
$subkey = $this->compile($element->key());
$subtype = $this->compile($element->type());
$optional = var_export($element->isOptional(), true);
$attributes = $this->attributesCompiler->compile($element->attributes());
$elements[] = var_export($key, true) . ' => new ' . ShapedArrayElement::class . "($subkey, $subtype, $optional, $attributes)";
}
$elements = implode(', ', $elements);
$isUnsealed = var_export($type->isUnsealed, true);
$unsealedType = $type->hasUnsealedType() ? $this->compile($type->unsealedType()) : 'null';
return "new $class([$elements], $isUnsealed, $unsealedType)";
case $type instanceof ArrayType:
case $type instanceof NonEmptyArrayType:
if ($type->toString() === 'array' || $type->toString() === 'non-empty-array') {
return "$class::native()";
}
$subType = $this->compile($type->subType());
if (str_ends_with($type->toString(), '[]')) {
return "$class::simple($subType)";
}
$keyType = $this->compile($type->keyType());
return "new $class($keyType, $subType)";
case $type instanceof ListType:
case $type instanceof NonEmptyListType:
if ($type->toString() === 'list' || $type->toString() === 'non-empty-list') {
return "$class::native()";
}
$subType = $this->compile($type->subType());
return "new $class($subType)";
case $type instanceof IterableType:
$keyType = $this->compile($type->keyType());
$subType = $this->compile($type->subType());
return "new $class($keyType, $subType)";
case $type instanceof NativeClassType:
case $type instanceof InterfaceType:
$generics = [];
foreach ($type->generics() as $key => $generic) {
$generics[] = var_export($key, true) . ' => ' . $this->compile($generic);
}
$generics = implode(', ', $generics);
return "new $class('{$type->className()}', [$generics])";
case $type instanceof ClassStringType:
$subTypes = implode(', ', array_map(
fn (Type $subType) => $this->compile($subType),
$type->subTypes(),
));
return "new $class([$subTypes])";
case $type instanceof EnumType:
$enumName = var_export($type->className(), true);
$pattern = var_export($type->pattern(), true);
$cases = array_map(
fn (string|int $key, UnitEnum $case) => var_export($key, true) . ' => ' . var_export($case, true),
array_keys($type->cases()),
$type->cases()
);
$cases = implode(', ', $cases);
return "new $class($enumName, $pattern, [$cases])";
case $type instanceof CallableType:
$returnType = $this->compile($type->returnType);
$parameters = implode(', ', array_map(
fn (Type $subType) => $this->compile($subType),
$type->parameters,
));
return "new $class([$parameters], $returnType)";
case $type instanceof GenericType:
$symbol = var_export($type->symbol, true);
$innerType = $this->compile($type->innerType);
return "new $class($symbol, $innerType)";
case $type instanceof UnresolvableType:
$raw = var_export($type->toString(), true);
$message = var_export($type->message(), true);
return "new $class($raw, $message)";
default:
throw new TypeCannotBeCompiled($type);
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Type\ObjectType;
/** @internal */
final class InMemoryClassDefinitionRepository implements ClassDefinitionRepository
{
/** @var array<string, ClassDefinition> */
private array $classDefinitions = [];
public function __construct(
private ClassDefinitionRepository $delegate,
) {}
public function for(ObjectType $type): ClassDefinition
{
return $this->classDefinitions[$type->toString()] ??= $this->delegate->for($type);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository\Cache;
use CuyZ\Valinor\Definition\FunctionDefinition;
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
use CuyZ\Valinor\Utility\Reflection\Reflection;
/** @internal */
final class InMemoryFunctionDefinitionRepository implements FunctionDefinitionRepository
{
/** @var array<string, FunctionDefinition> */
private array $functionDefinitions = [];
public function __construct(
private FunctionDefinitionRepository $delegate,
) {}
public function for(callable $function): FunctionDefinition
{
$reflection = Reflection::function($function);
// @infection-ignore-all
$key = $reflection->getFileName() . ':' . $reflection->getStartLine() . '-' . $reflection->getEndLine();
return ($this->functionDefinitions[$key] ??= $this->delegate->for($function))->forCallable($function);
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Definition\Repository;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Type\ObjectType;
/** @internal */
interface ClassDefinitionRepository
{
public function for(ObjectType $type): ClassDefinition;
}

Some files were not shown because too many files have changed in this diff Show More