Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce mapper to map arguments of a callable
This new mapper can be used to ensure a source has the right shape before calling a function/method. The mapper builder can be configured the same way it would be with a tree mapper, for instance to customize the type strictness. ```php $someFunction = function(string $foo, int $bar): string { return "$foo / $bar"; } try { $arguments = (new \CuyZ\Valinor\MapperBuilder()) ->argumentsMapper() ->mapArguments($someFunction, [ 'foo' => 'some value', 'bar' => 42, ]); // some value / 42 echo $someFunction(...$arguments); } catch (\CuyZ\Valinor\Mapper\MappingError $error) { // Do something… } ``` Any callable can be given to the arguments mapper: ```php final class SomeController { public static function someAction(string $foo, int $bar): string { return "$foo / $bar"; } } try { $arguments = (new \CuyZ\Valinor\MapperBuilder()) ->argumentsMapper() ->mapArguments(SomeController::someAction(...), [ 'foo' => 'some value', 'bar' => 42, ]); // some value / 42 echo SomeController::someAction(...$arguments); } catch (\CuyZ\Valinor\Mapper\MappingError $error) { // Do something… } ```
- Loading branch information
Showing
19 changed files
with
591 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Mapping arguments of a callable | ||
|
||
This library can map the arguments of a callable; it can be used to ensure a | ||
source has the right shape before calling a function/method. | ||
|
||
The mapper builder can be configured the same way it would be with a tree | ||
mapper, for instance to customize the [type strictness](type-strictness.md). | ||
|
||
```php | ||
$someFunction = function(string $foo, int $bar): string { | ||
return "$foo / $bar"; | ||
} | ||
|
||
try { | ||
$arguments = (new \CuyZ\Valinor\MapperBuilder()) | ||
->argumentsMapper() | ||
->mapArguments($someFunction, [ | ||
'foo' => 'some value', | ||
'bar' => 42, | ||
]); | ||
|
||
// some value / 42 | ||
echo $someFunction(...$arguments); | ||
} catch (\CuyZ\Valinor\Mapper\MappingError $error) { | ||
// Do something… | ||
} | ||
``` | ||
|
||
Any callable can be given to the arguments mapper: | ||
|
||
```php | ||
final class SomeController | ||
{ | ||
public static function someAction(string $foo, int $bar): string | ||
{ | ||
return "$foo / $bar"; | ||
} | ||
} | ||
|
||
try { | ||
$arguments = (new \CuyZ\Valinor\MapperBuilder()) | ||
->argumentsMapper() | ||
->mapArguments(SomeController::someAction(...), [ | ||
'foo' => 'some value', | ||
'bar' => 42, | ||
]); | ||
|
||
// some value / 42 | ||
echo SomeController::someAction(...$arguments); | ||
} catch (\CuyZ\Valinor\Mapper\MappingError $error) { | ||
// Do something… | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?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\ClosureType; | ||
use PHPStan\Type\Constant\ConstantArrayType; | ||
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 instanceof ClosureType) { | ||
$parameters = $type->getParameters(); | ||
} elseif ($type instanceof ConstantArrayType) { | ||
$acceptors = $type->getCallableParametersAcceptors($scope); | ||
|
||
if (count($acceptors) !== 1) { | ||
return new MixedType(); | ||
} | ||
|
||
$parameters = $acceptors[0]->getParameters(); | ||
} else { | ||
return new MixedType(); | ||
} | ||
|
||
$builder = ConstantArrayTypeBuilder::createEmpty(); | ||
|
||
foreach ($parameters as $parameter) { | ||
$builder->setOffsetValueType(new ConstantStringType($parameter->getName()), $parameter->getType(), $parameter->isOptional()); | ||
} | ||
|
||
return $builder->getArray(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
<?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; | ||
} | ||
|
||
if (empty($type->params ?? [])) { | ||
return null; | ||
} | ||
|
||
$params = []; | ||
|
||
foreach ($type->params as $param) { | ||
$params[$param->name] = $param->type ?? new Union([new TMixed()]); | ||
} | ||
|
||
return new Union([new TKeyedArray($params)]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace CuyZ\Valinor\Mapper; | ||
|
||
/** @api */ | ||
interface ArgumentsMapper | ||
{ | ||
/** | ||
* @param mixed $source | ||
* @return array<string, mixed> | ||
* | ||
* @throws MappingError | ||
*/ | ||
public function mapArguments(callable $callable, $source): array; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace CuyZ\Valinor\Mapper; | ||
|
||
use CuyZ\Valinor\Definition\FunctionDefinition; | ||
use CuyZ\Valinor\Mapper\Tree\Message\Messages; | ||
use CuyZ\Valinor\Mapper\Tree\Node; | ||
use CuyZ\Valinor\Utility\ValueDumper; | ||
use RuntimeException; | ||
|
||
/** @internal */ | ||
final class ArgumentsMapperError extends RuntimeException implements MappingError | ||
{ | ||
private Node $node; | ||
|
||
public function __construct(FunctionDefinition $function, Node $node) | ||
{ | ||
$this->node = $node; | ||
|
||
$errors = Messages::flattenFromNode($node)->errors(); | ||
$errorsCount = count($errors); | ||
|
||
if ($errorsCount === 1) { | ||
$body = $errors | ||
->toArray()[0] | ||
->withBody("Could not map arguments of `{$function->signature()}`. An error occurred at path {node_path}: {original_message}") | ||
->toString(); | ||
} else { | ||
$source = ValueDumper::dump($node->sourceValue()); | ||
$body = "Could not map arguments of `{$function->signature()}` with value $source. A total of $errorsCount errors were encountered."; | ||
} | ||
|
||
parent::__construct($body, 1617193185); | ||
} | ||
|
||
public function node(): Node | ||
{ | ||
return $this->node; | ||
} | ||
} |
Oops, something went wrong.