Skip to content

Commit

Permalink
[feature] access callback arguments, support PHP 8 union types (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
kbond committed Jul 16, 2021
1 parent 10ae748 commit d5d19c1
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 38 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Expand Up @@ -27,7 +27,7 @@ jobs:
uses: ramsey/composer-install@v1

- name: Test
run: vendor/bin/phpunit -v
run: vendor/bin/simple-phpunit -v

code-coverage:
name: Code Coverage
Expand All @@ -39,15 +39,15 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@2.7.0
with:
php-version: 7.4
php-version: 8.0
coverage: xdebug
ini-values: xdebug.mode=coverage

- name: Install dependencies
uses: ramsey/composer-install@v1

- name: Test with coverage
run: vendor/bin/phpunit -v --coverage-text --coverage-clover coverage.xml
run: vendor/bin/simple-phpunit -v --coverage-text --coverage-clover coverage.xml

- name: Publish coverage report to Codecov
uses: codecov/codecov-action@v1
Expand Down
3 changes: 3 additions & 0 deletions .php-cs-fixer.dist.php
Expand Up @@ -40,6 +40,9 @@
'phpdoc_to_comment' => false,
'function_declaration' => ['closure_function_spacing' => 'none'],
'nullable_type_declaration_for_default_null_value' => true,

// temporary fix for union types (ref: https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues/5495)
'binary_operator_spaces' => ['operators' => ['|' => null]],
])
->setRiskyAllowed(true)
->setFinder($finder)
Expand Down
1 change: 0 additions & 1 deletion composer.json
Expand Up @@ -15,7 +15,6 @@
"php": ">=7.2.5"
},
"require-dev": {
"phpunit/phpunit": "^8.5.0",
"symfony/phpunit-bridge": "^5.2"
},
"config": {
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Expand Up @@ -2,7 +2,7 @@

<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
Expand Down
52 changes: 40 additions & 12 deletions src/Callback.php
Expand Up @@ -2,13 +2,14 @@

namespace Zenstruck;

use Zenstruck\Callback\Argument;
use Zenstruck\Callback\Exception\UnresolveableArgument;
use Zenstruck\Callback\Parameter;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class Callback
final class Callback implements \Countable
{
/** @var \ReflectionFunction */
private $function;
Expand Down Expand Up @@ -56,16 +57,16 @@ public static function createFor($value): self
*/
public function invoke(...$arguments)
{
$parameters = $this->function->getParameters();
$functionArgs = $this->arguments();

foreach ($arguments as $key => $argument) {
if (!$argument instanceof Parameter) {
foreach ($arguments as $key => $parameter) {
if (!$parameter instanceof Parameter) {
continue;
}

if (!\array_key_exists($key, $parameters)) {
if (!$argument->isOptional()) {
throw new \ArgumentCountError(\sprintf('No argument %d for callable. Expected type: "%s".', $key + 1, $argument->type()));
if (!\array_key_exists($key, $functionArgs)) {
if (!$parameter->isOptional()) {
throw new \ArgumentCountError(\sprintf('No argument %d for callable. Expected type: "%s".', $key + 1, $parameter->type()));
}

$arguments[$key] = null;
Expand All @@ -74,9 +75,9 @@ public function invoke(...$arguments)
}

try {
$arguments[$key] = $argument->resolve($parameters[$key]);
$arguments[$key] = $parameter->resolve($functionArgs[$key]);
} catch (UnresolveableArgument $e) {
throw new UnresolveableArgument(\sprintf('Unable to resolve argument %d for callback. Expected type: "%s". (%s)', $key + 1, $argument->type(), $this), $e);
throw new UnresolveableArgument(\sprintf('Unable to resolve argument %d for callback. Expected type: "%s". (%s)', $key + 1, $parameter->type(), $this), $e);
}
}

Expand All @@ -96,12 +97,12 @@ public function invoke(...$arguments)
*/
public function invokeAll(Parameter $parameter, int $min = 0)
{
$arguments = $this->function->getParameters();

if (\count($arguments) < $min) {
if (\count($this) < $min) {
throw new \ArgumentCountError("{$min} argument(s) of type \"{$parameter->type()}\" required ({$this}).");
}

$arguments = $this->arguments();

foreach ($arguments as $key => $argument) {
try {
$arguments[$key] = $parameter->resolve($argument);
Expand All @@ -112,4 +113,31 @@ public function invokeAll(Parameter $parameter, int $min = 0)

return $this->function->invoke(...$arguments);
}

/**
* @return Argument[]
*/
public function arguments(): array
{
return \array_map(
static function(\ReflectionParameter $parameter) {
return new Argument($parameter);
},
$this->function->getParameters()
);
}

public function argument(int $index): Argument
{
if (!isset(($arguments = $this->arguments())[$index])) {
throw new \OutOfRangeException(\sprintf('Argument %d does not exist for %s.', $index + 1, $this));
}

return $arguments[$index];
}

public function count(): int
{
return $this->function->getNumberOfParameters();
}
}
67 changes: 67 additions & 0 deletions src/Callback/Argument.php
@@ -0,0 +1,67 @@
<?php

namespace Zenstruck\Callback;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class Argument
{
/** @var \ReflectionNamedType[] */
private $types = [];

public function __construct(\ReflectionParameter $parameter)
{
if (!$type = $parameter->getType()) {
return;
}

if ($type instanceof \ReflectionNamedType) {
$this->types = [$type];

return;
}

/** @var \ReflectionUnionType $type */
$this->types = $type->getTypes();
}

public function type(): ?string
{
return $this->hasType() ? \implode('|', $this->types()) : null;
}

/**
* @return string[]
*/
public function types(): array
{
return \array_map(static function(\ReflectionNamedType $type) { return $type->getName(); }, $this->types);
}

public function hasType(): bool
{
return !empty($this->types);
}

public function isUnionType(): bool
{
return \count($this->types) > 1;
}

public function supports(string $type): bool
{
if (!$this->hasType()) {
// no type-hint so any type is supported
return true;
}

foreach ($this->types() as $t) {
if ($t === $type || \is_a($t, $type, true)) {
return true;
}
}

return false;
}
}
14 changes: 4 additions & 10 deletions src/Callback/Parameter.php
Expand Up @@ -49,21 +49,15 @@ final public function optional(): self
*
* @throws UnresolveableArgument
*/
final public function resolve(\ReflectionParameter $parameter)
final public function resolve(Argument $argument)
{
$value = $this->valueFor($parameter);
$value = $this->valueFor($argument);

if (!$value instanceof ValueFactory) {
return $value;
}

$type = $parameter->getType();

if (!$type instanceof \ReflectionNamedType) {
return $value(null);
}

return $value($type->getName());
return $value($argument);
}

/**
Expand All @@ -76,5 +70,5 @@ final public function isOptional(): bool

abstract public function type(): string;

abstract protected function valueFor(\ReflectionParameter $refParameter);
abstract protected function valueFor(Argument $argument);
}
9 changes: 4 additions & 5 deletions src/Callback/Parameter/TypedParameter.php
Expand Up @@ -2,6 +2,7 @@

namespace Zenstruck\Callback\Parameter;

use Zenstruck\Callback\Argument;
use Zenstruck\Callback\Exception\UnresolveableArgument;
use Zenstruck\Callback\Parameter;

Expand All @@ -25,15 +26,13 @@ public function type(): string
return $this->type;
}

protected function valueFor(\ReflectionParameter $parameter)
protected function valueFor(Argument $argument)
{
$parameterType = $parameter->getType();

if (!$parameterType instanceof \ReflectionNamedType) {
if (!$argument->hasType()) {
throw new UnresolveableArgument('Argument has no type.');
}

if ($this->type === $parameterType->getName() || \is_a($parameterType->getName(), $this->type, true)) {
if ($argument->supports($this->type)) {
return $this->value;
}

Expand Down
5 changes: 3 additions & 2 deletions src/Callback/Parameter/UnionParameter.php
Expand Up @@ -2,6 +2,7 @@

namespace Zenstruck\Callback\Parameter;

use Zenstruck\Callback\Argument;
use Zenstruck\Callback\Exception\UnresolveableArgument;
use Zenstruck\Callback\Parameter;

Expand All @@ -27,11 +28,11 @@ public function type(): string
return \implode('|', \array_map(static function(Parameter $param) { return $param->type(); }, $this->parameters));
}

protected function valueFor(\ReflectionParameter $refParameter)
protected function valueFor(Argument $argument)
{
foreach ($this->parameters as $parameter) {
try {
return $parameter->resolve($refParameter);
return $parameter->resolve($argument);
} catch (UnresolveableArgument $e) {
continue;
}
Expand Down
5 changes: 3 additions & 2 deletions src/Callback/Parameter/UntypedParameter.php
Expand Up @@ -2,6 +2,7 @@

namespace Zenstruck\Callback\Parameter;

use Zenstruck\Callback\Argument;
use Zenstruck\Callback\Exception\UnresolveableArgument;
use Zenstruck\Callback\Parameter;

Expand All @@ -22,9 +23,9 @@ public function type(): string
return 'mixed';
}

protected function valueFor(\ReflectionParameter $parameter)
protected function valueFor(Argument $argument)
{
if ($parameter->getType()) {
if ($argument->hasType()) {
throw new UnresolveableArgument('Argument has type.');
}

Expand Down
21 changes: 19 additions & 2 deletions src/Callback/ValueFactory.php
Expand Up @@ -2,6 +2,8 @@

namespace Zenstruck\Callback;

use Zenstruck\Callback;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
Expand All @@ -15,8 +17,23 @@ public function __construct(callable $factory)
$this->factory = $factory;
}

public function __invoke(?string $type)
public function __invoke(Argument $argument)
{
return ($this->factory)($type);
$stringTypeFactory = Parameter::factory(function() use ($argument) {
if ($argument->isUnionType()) {
throw new \LogicException(\sprintf('ValueFactory does not support union types. Inject "%s" instead.', Argument::class));
}

return $argument->type();
});

return Callback::createFor($this->factory)
->invoke(Parameter::union(
Parameter::typed(Argument::class, $argument),
Parameter::typed('array', $argument->types()),
Parameter::typed('string', $stringTypeFactory),
Parameter::untyped($stringTypeFactory)
)->optional())
;
}
}

0 comments on commit d5d19c1

Please sign in to comment.