Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
JanTvrdik committed Mar 14, 2024
1 parent c7f289f commit 1cfdf63
Show file tree
Hide file tree
Showing 29 changed files with 1,353 additions and 49 deletions.
15 changes: 15 additions & 0 deletions src/Compiler/Mapper/GenericMapperCompiler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types = 1);

namespace ShipMonk\InputMapper\Compiler\Mapper;

use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter;

interface GenericMapperCompiler extends MapperCompiler
{

/**
* @return list<GenericTypeParameter>
*/
public function getGenericParameters(): array;

}
83 changes: 77 additions & 6 deletions src/Compiler/Mapper/Object/DelegateMapperCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,40 @@

namespace ShipMonk\InputMapper\Compiler\Mapper\Object;

use Nette\Utils\Arrays;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\VariadicPlaceholder;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use ShipMonk\InputMapper\Compiler\CompiledExpr;
use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler;
use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder;
use ShipMonk\InputMapper\Runtime\CallbackMapper;
use function count;

class DelegateMapperCompiler implements MapperCompiler
{

/**
* @param class-string $className
* @param list<MapperCompiler> $innerMapperCompilers
*/
public function __construct(
public readonly string $className,
public readonly array $innerMapperCompilers = [],
)
{
}

public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): CompiledExpr
{
$shortName = $builder->importClass($this->className);
$provider = $builder->propertyFetch($builder->var('this'), 'provider');
$mapper = $builder->methodCall($provider, 'get', [$builder->classConstFetch($shortName, 'class')]);
$compilerMapper = $this->compileMapperExpr($builder);
$mapper = $compilerMapper->expr;
$statements = $compilerMapper->statements;
$mapped = $builder->methodCall($mapper, 'map', [$value, $path]);

return new CompiledExpr($mapped);
return new CompiledExpr($mapped, $statements);
}

public function getInputType(): TypeNode
Expand All @@ -38,7 +45,71 @@ public function getInputType(): TypeNode

public function getOutputType(): TypeNode
{
return new IdentifierTypeNode($this->className);
$outputType = new IdentifierTypeNode($this->className);

if (count($this->innerMapperCompilers) === 0) {
return $outputType;
}

return new GenericTypeNode($outputType, Arrays::map(
$this->innerMapperCompilers,
static function (MapperCompiler $innerMapperCompiler): TypeNode {
return $innerMapperCompiler->getOutputType();
},
));
}

/**
* @return list<Expr>
*/
private function compileInnerMappers(PhpCodeBuilder $builder): array
{
return Arrays::map($this->innerMapperCompilers, function (MapperCompiler $innerMapperCompiler, int $key) use ($builder): Expr {
return $this->compileInnerMapper($innerMapperCompiler, $key, $builder);
});
}

private function compileInnerMapper(MapperCompiler $innerMapperCompiler, int $key, PhpCodeBuilder $builder): Expr
{
if ($innerMapperCompiler instanceof self && count($innerMapperCompiler->innerMapperCompilers) === 0) {
$provider = $builder->propertyFetch($builder->var('this'), 'provider');
$innerClassExpr = $builder->classConstFetch($builder->importClass($innerMapperCompiler->className), 'class');
return $builder->methodCall($provider, 'get', [$innerClassExpr]);
}

$innerMapperMethodName = $builder->uniqMethodName("mapInner{$key}");
$innerMapperMethod = $builder->mapperMethod($innerMapperMethodName, $innerMapperCompiler)->makePrivate()->getNode();
$builder->addMethod($innerMapperMethod);

$innerMapperMethodCallback = new MethodCall($builder->var('this'), $innerMapperMethodName, [new VariadicPlaceholder()]);
return $builder->new($builder->importClass(CallbackMapper::class), [$innerMapperMethodCallback]);
}

private function compileMapperExpr(PhpCodeBuilder $builder): CompiledExpr
{
foreach ($builder->getGenericParameters() as $offset => $genericParameter) {
if ($this->className === $genericParameter->name) {
$innerMappers = $builder->propertyFetch($builder->var('this'), 'innerMappers');
$innerMapper = $builder->arrayDimFetch($innerMappers, $builder->val($offset));
return new CompiledExpr($innerMapper);
}
}

$statements = [];
$classNameExpr = $builder->classConstFetch($builder->importClass($this->className), 'class');
$provider = $builder->propertyFetch($builder->var('this'), 'provider');
$innerMappers = $this->compileInnerMappers($builder);

if (count($innerMappers) === 0) {
$mapper = $builder->methodCall($provider, 'get', [$classNameExpr]);

} else {
$innerMappersVarName = $builder->uniqVariableName('innerMappers');
$statements[] = $builder->assign($builder->var($innerMappersVarName), $builder->val($innerMappers));
$mapper = $builder->methodCall($provider, 'getGeneric', [$classNameExpr, $builder->var($innerMappersVarName)]);
}

return new CompiledExpr($mapper, $statements);
}

}
29 changes: 27 additions & 2 deletions src/Compiler/Mapper/Object/MapObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
namespace ShipMonk\InputMapper\Compiler\Mapper\Object;

use Attribute;
use Nette\Utils\Arrays;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use ShipMonk\InputMapper\Compiler\CompiledExpr;
use ShipMonk\InputMapper\Compiler\Mapper\GenericMapperCompiler;
use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler;
use ShipMonk\InputMapper\Compiler\Mapper\UndefinedAwareMapperCompiler;
use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder;
use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter;
use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException;
use function array_fill_keys;
use function array_keys;
Expand All @@ -22,17 +26,19 @@
* @template T of object
*/
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
class MapObject implements MapperCompiler
class MapObject implements GenericMapperCompiler
{

/**
* @param class-string<T> $className
* @param array<string, MapperCompiler> $constructorArgsMapperCompilers
* @param list<GenericTypeParameter> $genericParameters
*/
public function __construct(
public readonly string $className,
public readonly array $constructorArgsMapperCompilers,
public readonly bool $allowExtraKeys = false,
public readonly array $genericParameters = [],
)
{
}
Expand Down Expand Up @@ -114,7 +120,26 @@ public function getInputType(): TypeNode

public function getOutputType(): TypeNode
{
return new IdentifierTypeNode($this->className);
$outputType = new IdentifierTypeNode($this->className);

if (count($this->genericParameters) === 0) {
return $outputType;
}

return new GenericTypeNode(
$outputType,
Arrays::map($this->genericParameters, static function (GenericTypeParameter $parameter): TypeNode {
return new IdentifierTypeNode($parameter->name);
}),
);
}

/**
* @return list<GenericTypeParameter>
*/
public function getGenericParameters(): array
{
return $this->genericParameters;
}

/**
Expand Down
90 changes: 83 additions & 7 deletions src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
use DateTimeImmutable;
use DateTimeInterface;
use LogicException;
use Nette\Utils\Arrays;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
Expand Down Expand Up @@ -48,6 +50,8 @@
use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapNullable;
use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapOptional;
use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\ValidatedMapperCompiler;
use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter;
use ShipMonk\InputMapper\Compiler\Type\GenericTypeVariance;
use ShipMonk\InputMapper\Compiler\Type\PhpDocTypeUtils;
use ShipMonk\InputMapper\Compiler\Validator\Array\AssertListLength;
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertIntRange;
Expand All @@ -57,12 +61,17 @@
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertPositiveInt;
use ShipMonk\InputMapper\Compiler\Validator\ValidatorCompiler;
use ShipMonk\InputMapper\Runtime\Optional;
use function array_column;
use function array_fill_keys;
use function array_keys;
use function array_values;
use function class_exists;
use function class_implements;
use function class_parents;
use function count;
use function enum_exists;
use function interface_exists;
use function str_ends_with;
use function strcasecmp;
use function strtolower;
use function substr;
Expand All @@ -71,6 +80,7 @@ class DefaultMapperCompilerFactory implements MapperCompilerFactory
{

final public const DELEGATE_OBJECT_MAPPING = 'delegateObjectMapping';
final public const GENERIC_PARAMETERS = 'genericParameters';

/**
* @param array<class-string, callable(class-string, array<string, mixed>): MapperCompiler> $mapperCompilerFactories
Expand Down Expand Up @@ -102,7 +112,7 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
{
if ($type instanceof IdentifierTypeNode) {
if (!PhpDocTypeUtils::isKeyword($type)) {
if (!class_exists($type->name) && !interface_exists($type->name) && !enum_exists($type->name)) {
if (!class_exists($type->name) && !interface_exists($type->name) && !enum_exists($type->name) && !isset($options[self::GENERIC_PARAMETERS][$type->name])) {
throw CannotCreateMapperCompilerException::fromType($type, 'there is no class, interface or enum with this name');
}

Expand Down Expand Up @@ -164,7 +174,7 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
1 => new MapOptional($this->createInner($type->genericTypes[0], $options)),
default => throw CannotCreateMapperCompilerException::fromType($type),
},
default => throw CannotCreateMapperCompilerException::fromType($type),
default => $this->createFromGenericType($type, $options),
},
};
}
Expand Down Expand Up @@ -218,6 +228,27 @@ protected function createInner(TypeNode $type, array $options): MapperCompiler
return $this->create($type, $options);
}

/**
* @param array<string, mixed> $options
*/
protected function createFromGenericType(GenericTypeNode $type, array $options): MapperCompiler
{
$genericParameters = $this->getGenericParameters($type->type->name);

return new DelegateMapperCompiler(
$type->type->name,
Arrays::map(array_values($type->genericTypes), function (TypeNode $genericType, int $index) use ($type, $options, $genericParameters): MapperCompiler {
$genericParameter = $genericParameters[$index] ?? throw CannotCreateMapperCompilerException::fromType($type, "generic parameter at index {$index} does not exist");

if ($genericParameter->bound !== null && !PhpDocTypeUtils::isSubTypeOf($genericType, $genericParameter->bound)) {
throw CannotCreateMapperCompilerException::fromType($type, "type {$genericType} is not a subtype of {$genericParameter->bound}");
}

return $this->createInner($genericType, $options);
}),
);
}

/**
* @param class-string $inputClassName
* @param array<string, mixed> $options
Expand Down Expand Up @@ -257,8 +288,12 @@ protected function createObjectMappingByConstructorInvocation(
throw CannotCreateMapperCompilerException::fromType(new IdentifierTypeNode($inputClassName), 'class has a non-public constructor');
}

$genericParameters = $this->getGenericParameters($inputClassName);
$genericParameterNames = array_column($genericParameters, 'name');
$options[self::GENERIC_PARAMETERS] = array_fill_keys($genericParameterNames, true);

$constructorParameterMapperCompilers = [];
$constructorParameterTypes = $this->getConstructorParameterTypes($constructor);
$constructorParameterTypes = $this->getConstructorParameterTypes($constructor, $genericParameterNames);

foreach ($constructor->getParameters() as $parameter) {
$name = $parameter->getName();
Expand All @@ -267,13 +302,54 @@ protected function createObjectMappingByConstructorInvocation(
}

$allowExtraKeys = count($classReflection->getAttributes(AllowExtraKeys::class)) > 0;
return new MapObject($classReflection->getName(), $constructorParameterMapperCompilers, $allowExtraKeys);
return new MapObject($classReflection->getName(), $constructorParameterMapperCompilers, $allowExtraKeys, $genericParameters);
}

/**
* @param class-string $inputClassName
* @return list<GenericTypeParameter>
*/
protected function getGenericParameters(string $inputClassName): array
{
$classReflection = new ReflectionClass($inputClassName);
$classPhpDoc = $classReflection->getDocComment();

if ($classPhpDoc === false) {
return [];
}

$genericParameters = [];

foreach ($this->parsePhpDoc($classPhpDoc)->children as $node) {
if ($node instanceof PhpDocTagNode && $node->value instanceof TemplateTagValueNode) {
$variance = match (true) {
str_ends_with($node->name, '-covariant') => GenericTypeVariance::Covariant,
str_ends_with($node->name, '-contravariant') => GenericTypeVariance::Contravariant,
default => GenericTypeVariance::Invariant,
};

$genericParameters[$node->value->name] = new GenericTypeParameter(
name: $node->value->name,
variance: $variance,
bound: $node->value->bound,
default: $node->value->default,
);
}
}

foreach ($genericParameters as $genericParameter) {
PhpDocTypeUtils::resolve($genericParameter->bound, $classReflection, array_keys($genericParameters));
PhpDocTypeUtils::resolve($genericParameter->default, $classReflection, array_keys($genericParameters));
}

return array_values($genericParameters);
}

/**
* @param list<string> $genericParameterNames
* @return array<string, TypeNode>
*/
protected function getConstructorParameterTypes(ReflectionMethod $constructor): array
protected function getConstructorParameterTypes(ReflectionMethod $constructor, array $genericParameterNames): array
{
$class = $constructor->getDeclaringClass();
$parameterTypes = [];
Expand All @@ -290,7 +366,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor):
if ($constructorDocComment !== false) {
foreach ($this->parsePhpDoc($constructorDocComment)->children as $node) {
if ($node instanceof PhpDocTagNode && $node->value instanceof ParamTagValueNode) {
PhpDocTypeUtils::resolve($node->value->type, $class);
PhpDocTypeUtils::resolve($node->value->type, $class, $genericParameterNames);
$parameterName = substr($node->value->parameterName, 1);
$parameterTypes[$parameterName] = $node->value->type;
}
Expand All @@ -312,7 +388,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor):
&& $node->value instanceof VarTagValueNode
&& ($node->value->variableName === '' || substr($node->value->variableName, 1) === $parameterName)
) {
PhpDocTypeUtils::resolve($node->value->type, $class);
PhpDocTypeUtils::resolve($node->value->type, $class, $genericParameterNames);
$parameterTypes[$parameterName] = $node->value->type;
}
}
Expand Down
Loading

0 comments on commit 1cfdf63

Please sign in to comment.