Skip to content

Commit

Permalink
WIP: user defined generics support
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/Compiler/Mapper/Object/DelegateMapperCompiler.php
  • Loading branch information
JanTvrdik committed Mar 2, 2024
1 parent 4885cf2 commit 8f7e175
Show file tree
Hide file tree
Showing 31 changed files with 1,255 additions and 56 deletions.
13 changes: 13 additions & 0 deletions src/Compiler/Mapper/GenericMapperCompiler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace ShipMonk\InputMapper\Compiler\Mapper;

interface GenericMapperCompiler extends MapperCompiler
{

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

}
33 changes: 33 additions & 0 deletions src/Compiler/Mapper/GenericMapperParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace ShipMonk\InputMapper\Compiler\Mapper;

use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class GenericMapperParameter
{

public function __construct(
public readonly string $name,
public readonly GenericTypeVariance $variance = GenericTypeVariance::Invariant,
public readonly ?TypeNode $bound = null,
public readonly ?TypeNode $default = null,
)
{
}

public function toPhpDocLine(): string
{
$tagName = match ($this->variance) {
GenericTypeVariance::Invariant => 'template',
GenericTypeVariance::Covariant => 'template-covariant',
GenericTypeVariance::Contravariant => 'template-contravariant',
};

$bound = $this->bound !== null ? " of {$this->bound}" : '';
$default = $this->default !== null ? " = {$this->default}" : '';
return trim("@{$tagName} {$this->name}{$bound}{$default}");
}

}
12 changes: 12 additions & 0 deletions src/Compiler/Mapper/GenericTypeVariance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php declare(strict_types = 1);

namespace ShipMonk\InputMapper\Compiler\Mapper;

enum GenericTypeVariance: string
{

case Invariant = 'inout';
case Covariant = 'out';
case Contravariant = 'in';

}
84 changes: 78 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,72 @@ 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 {

Check failure on line 67 in src/Compiler/Mapper/Object/DelegateMapperCompiler.php

View workflow job for this annotation

GitHub Actions / checks

Method ShipMonk\InputMapper\Compiler\Mapper\Object\DelegateMapperCompiler::compileInnerMappers() should return list<PhpParser\Node\Expr> but returns array<int<0, max>, PhpParser\Node\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');

Check failure on line 76 in src/Compiler/Mapper/Object/DelegateMapperCompiler.php

View workflow job for this annotation

GitHub Actions / checks

Parameter #1 $className of method ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder::importClass() expects class-string, string given.
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');

Check failure on line 100 in src/Compiler/Mapper/Object/DelegateMapperCompiler.php

View workflow job for this annotation

GitHub Actions / checks

Parameter #1 $className of method ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder::importClass() expects class-string, string given.
$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);
}

}
27 changes: 25 additions & 2 deletions src/Compiler/Mapper/Object/MapObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
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\GenericMapperParameter;
use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler;
use ShipMonk\InputMapper\Compiler\Mapper\UndefinedAwareMapperCompiler;
use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder;
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<GenericMapperParameter> $genericParameters
*/
public function __construct(
public readonly string $className,
public readonly array $constructorArgsMapperCompilers,
public readonly bool $allowExtraKeys = false,
private readonly array $genericParameters = [],
)
{
}
Expand Down Expand Up @@ -114,7 +120,24 @@ 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 fn (GenericMapperParameter $parameter) => $parameter->name),

Check failure on line 131 in src/Compiler/Mapper/Object/MapObject.php

View workflow job for this annotation

GitHub Actions / checks

Parameter #2 $genericTypes of class PHPStan\PhpDocParser\Ast\Type\GenericTypeNode constructor expects array<PHPStan\PhpDocParser\Ast\Type\TypeNode>, array<int<0, max>, string> given.
);
}

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

/**
Expand Down
Loading

0 comments on commit 8f7e175

Please sign in to comment.