Skip to content

Commit

Permalink
Add instantiation.
Browse files Browse the repository at this point in the history
  • Loading branch information
serge-kvashnin committed Oct 10, 2023
1 parent eeb7e31 commit 6b907a5
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 11 deletions.
20 changes: 20 additions & 0 deletions src/Exception/NonInstantiatableClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/*
* This file is part of norvica/invoker.
*
* (c) Siarhei Kvashnin <serge@norvica.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Norvica\Invoker\Exception;

use LogicException;

final class NonInstantiatableClass extends LogicException
{
}
91 changes: 81 additions & 10 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,35 @@

use Closure;
use Norvica\Invoker\Exception\InvalidCallableException;
use Norvica\Invoker\Exception\NonInstantiatableClass;
use Norvica\Invoker\Exception\UnresolvedParameterException;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use TypeError;
use ReflectionParameter;

if (!function_exists('Norvica\Invoker\call')) {
/**
* @throws ReflectionException
*/
function call(callable|array|string $callable, array $arguments, ?Resolver $resolver = null): mixed
{
$reflection = reflection($callable);
$parameters = $reflection->getParameters();
[$parameters, $callable] = prepare($callable, $resolver);
$canonical = resolve($parameters, $arguments, $resolver);

return call_user_func_array($callable, $canonical);
}
}

if (!function_exists('Norvica\Invoker\resolve')) {
/**
* @internal
*
* @param ReflectionParameter[] $parameters
* @param array<string, mixed> $arguments
*/
function resolve(array $parameters, array $arguments = [], ?Resolver $resolver = null): array {
$canonical = [];

foreach ($parameters as $parameter) {
Expand All @@ -50,20 +65,34 @@ function call(callable|array|string $callable, array $arguments, ?Resolver $reso
}
}

return call_user_func_array($callable, $canonical);
return $canonical;
}
}

if (!function_exists('Norvica\Invoker\reflection')) {
if (!function_exists('Norvica\Invoker\prepare')) {
/**
* @internal
*
* @return array{0: ReflectionParameter[], 1: Closure}
* @throws ReflectionException
*/
function reflection(callable|array|string $callable): ReflectionMethod|ReflectionFunction
function prepare(callable|array|string $callable, ?Resolver $resolver = null): array
{
if (is_string($callable)) {
if (function_exists($callable)) {
return new ReflectionFunction($callable);
return [
(new ReflectionFunction($callable))->getParameters(),
$callable(...),
];
}

if (class_exists($callable)) {
$instance = instantiate($callable, [], $resolver);

return [
(new ReflectionMethod($callable, '__invoke'))->getParameters(),
[$instance, '__invoke'](...)
];
}

$callable = match(true) {
Expand All @@ -74,15 +103,57 @@ function reflection(callable|array|string $callable): ReflectionMethod|Reflectio

if (is_array($callable)) {
// object method or class method
return new ReflectionMethod($callable[0], $callable[1]);
$method = new ReflectionMethod($callable[0], $callable[1]);
if ($method->isStatic()) {
return [
$method->getParameters(),
$callable(...),
];
}

$instance = is_string($callable[0])
? instantiate($callable[0], [], $resolver)
: $callable[0];

return [
$method->getParameters(),
[$instance, $callable[1]](...),
];
}

if (is_object($callable) && !$callable instanceof Closure) {
// object's __invoke method
return new ReflectionMethod($callable, '__invoke');
return [
(new ReflectionMethod($callable, '__invoke'))->getParameters(),
[$callable, '__invoke'](...)
];
}

// standalone function or closure
return new ReflectionFunction($callable);
return [
(new ReflectionFunction($callable))->getParameters(),
$callable,
];
}
}

if (!function_exists('Norvica\Invoker\instantiate')) {
/**
* @throws ReflectionException
*/
function instantiate(string $class, array $arguments = [], ?Resolver $resolver = null): object {
$reflection = new ReflectionClass($class);
if (!$reflection->hasMethod('__construct')) {
return new $class();
}

$constructor = $reflection->getMethod('__construct');
if (!$constructor->isPublic()) {
throw new NonInstantiatableClass("Constructor of a class '{$class}' is not public, therefore class cannot be instantiated.");
}

$parameters = resolve($constructor->getParameters(), $arguments, $resolver);

return $reflection->newInstanceArgs($parameters);
}
}
5 changes: 5 additions & 0 deletions tests/Fixtures/ClassWithInvokeMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@

final class ClassWithInvokeMethod
{
public function __construct(
public DateTimeImmutable $timestamp,
) {
}

public function __invoke(
Closure $assertion,
int $__special,
Expand Down
5 changes: 5 additions & 0 deletions tests/Fixtures/ClassWithPublicMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@

final class ClassWithPublicMethod
{
public function __construct(
public readonly string $foo = 'bar',
) {
}

public function bar(
Closure $assertion,
bool $boolean,
Expand Down
5 changes: 4 additions & 1 deletion tests/InvokerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,11 @@ public function supports(ReflectionParameter $parameter): bool
$default,
...$variadic,
), $resolver];
yield 'class instance with __invoke method' => [new ClassWithInvokeMethod(), $resolver];
yield 'class instance with __invoke method' => [new ClassWithInvokeMethod(new DateTimeImmutable()), $resolver];
yield 'non-instantiated class with __invoke method' => [ClassWithInvokeMethod::class, $resolver];
yield 'class instance with public method' => [[new ClassWithPublicMethod(), 'bar'], $resolver];
yield 'non-instantiated class with public method' => [[ClassWithPublicMethod::class, 'bar'], $resolver];
yield 'non-instantiated class with public method (alternative notation)' => [ClassWithPublicMethod::class . '::bar', $resolver];
yield 'class with static method' => [[ClassWithStaticMethod::class, 'foo'], $resolver];
yield 'class with static method (alternative notation)' => [ClassWithStaticMethod::class . '::foo', $resolver];
}
Expand Down

0 comments on commit 6b907a5

Please sign in to comment.