Skip to content

Commit

Permalink
Mapping to generic input classes (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
JanTvrdik authored Mar 15, 2024
1 parent e2c2149 commit 0ef4f61
Show file tree
Hide file tree
Showing 37 changed files with 1,947 additions and 192 deletions.
6 changes: 6 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ parameters:
forbidCheckedExceptionInCallable:
immediatelyCalledCallables:
'ShipMonkTests\InputMapper\InputMapperTestCase::assertException': 2
allowedCheckedExceptionCallables:
'ShipMonk\InputMapper\Runtime\CallbackMapper::__construct': 0

ignoreErrors:
-
Expand All @@ -53,3 +55,7 @@ parameters:
message: "#^Method ShipMonkTests\\\\InputMapper\\\\Compiler\\\\Validator\\\\Array\\\\Data\\\\ListItemValidatorWithMultipleValidatorsMapper\\:\\:map\\(\\) should return list\\<int\\<1, max\\>\\> but returns list\\<int\\>\\.$#"
count: 1
path: tests/Compiler/Validator/Array/Data/ListItemValidatorWithMultipleValidatorsMapper.php
-
message: "#^Throwing checked exception ShipMonk\\\\InputMapper\\\\Runtime\\\\Exception\\\\MappingFailedException in first-class-callable!$#"
count: 1
path: tests/Compiler/Mapper/Object/Data/DelegateToIntCollectionMapper.php
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;

}
88 changes: 82 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,76 @@ 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
{
$innerMappers = [];

foreach ($this->innerMapperCompilers as $key => $innerMapperCompiler) {
$innerMappers[] = $this->compileInnerMapper($innerMapperCompiler, $key, $builder);
}

return $innerMappers;
}

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) {
$innerMappersVarName = $builder->uniqVariableName('innerMappers');
$statements[] = $builder->assign($builder->var($innerMappersVarName), $builder->val($innerMappers));
$getArguments = [$classNameExpr, $builder->var($innerMappersVarName)];

} else {
$getArguments = [$classNameExpr];
}

$mapper = $builder->methodCall($provider, 'get', $getArguments);
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
Loading

0 comments on commit 0ef4f61

Please sign in to comment.