Skip to content

Commit

Permalink
Merge pull request #1106: Refactor interceptors target
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk authored May 8, 2024
2 parents f6d01ec + 703275a commit e6f66b2
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 90 deletions.
2 changes: 1 addition & 1 deletion src/Console/src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function () use ($method) {

return $core instanceof HandlerInterface
? (int)$core->handle(new CallContext(
Target::fromReflection(new \ReflectionMethod(static::class, $method)),
Target::fromPair($this, $method),
$arguments,
))
: (int)$core->callAction(static::class, $method, $arguments);
Expand Down
2 changes: 1 addition & 1 deletion src/Events/src/EventDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function dispatch(object $event): object
return $this->isLegacy
? $this->core->callAction($event::class, 'dispatch', ['event' => $event])
: $this->core->handle(new CallContext(
Target::fromReflection(new \ReflectionMethod($event::class, 'dispatch')),
Target::fromPair($event, 'dispatch'),
['event' => $event],
));
}
Expand Down
16 changes: 9 additions & 7 deletions src/Hmvc/src/Core/AbstractCore.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ public function __construct(
// TODO: can we simplify this?
// resolver is usually the container itself
/** @psalm-suppress MixedAssignment */
$this->resolver = $container
->get(InvokerInterface::class)
->invoke(static fn (#[Proxy] ResolverInterface $resolver) => $resolver);
$this->resolver = $container instanceof ResolverInterface
? $container
: $container
->get(InvokerInterface::class)
->invoke(static fn (#[Proxy] ResolverInterface $resolver) => $resolver);
}

/**
Expand All @@ -49,15 +51,15 @@ public function callAction(string $controller, string $action, array $parameters
// Validate method
ActionResolver::validateControllerMethod($method);

return $this->invoke($controller, $method, $parameters);
return $this->invoke(null, $controller, $method, $parameters);
}

public function handle(CallContext $context): mixed
{
$target = $context->getTarget();
$reflection = $target->getReflection();
return $reflection instanceof \ReflectionMethod
? $this->invoke($target->getPath()[0], $reflection, $context->getArguments())
? $this->invoke($target->getObject(), $target->getPath()[0], $reflection, $context->getArguments())
: $this->callAction($target->getPath()[0], $target->getPath()[1], $context->getArguments());
}

Expand All @@ -82,7 +84,7 @@ protected function resolveArguments(\ReflectionMethod $method, array $parameters
/**
* @throws \Throwable
*/
private function invoke(string $class, \ReflectionMethod $method, array $arguments): mixed
private function invoke(?object $object, string $class, \ReflectionMethod $method, array $arguments): mixed
{
try {
$args = $this->resolveArguments($method, $arguments);
Expand All @@ -105,6 +107,6 @@ private function invoke(string $class, \ReflectionMethod $method, array $argumen
);
}

return $method->invokeArgs($this->container->get($class), $args);
return $method->invokeArgs($object ?? $this->container->get($class), $args);
}
}
134 changes: 104 additions & 30 deletions src/Hmvc/src/Interceptors/Context/Target.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,147 @@

namespace Spiral\Interceptors\Context;

/**
* @template-covariant TController of object|null
* @implements TargetInterface<TController>
*/
final class Target implements TargetInterface
{
/**
* @param list<string> $path
* @param \ReflectionFunctionAbstract|null $reflection
* @param TController|null $object
*/
private function __construct(
private ?array $path = null,
private array $path,
private ?\ReflectionFunctionAbstract $reflection = null,
private readonly string $delimiter = '.',
private readonly ?object $object = null,
private string $delimiter = '.',
) {
}

public function __toString(): string
{
return match (true) {
$this->path !== null => \implode($this->delimiter, $this->path),
$this->reflection !== null => $this->reflection->getName(),
};
return \implode($this->delimiter, $this->path);
}

public static function fromReflection(\ReflectionFunctionAbstract $reflection): self
/**
* Create a target from a method reflection.
*
* @template T of object
*
* @param \ReflectionMethod $reflection
* @param class-string<T>|T $classOrObject The original class name or object.
* It's required because the reflection may be referring to a parent class method.
* THe path will contain the original class name and the method name.
*
* @psalmif
*
* @return self<T>
*/
public static function fromReflectionMethod(
\ReflectionFunctionAbstract $reflection,
string|object $classOrObject,
): self {
/** @var self<T> $result */
$result = \is_object($classOrObject)
? new self(
path: [$classOrObject::class, $reflection->getName()],
reflection: $reflection,
object: $classOrObject,
delimiter: $reflection->isStatic() ? '::' : '->',
)
: new self(
path: [$classOrObject, $reflection->getName()],
reflection: $reflection,
delimiter: $reflection->isStatic() ? '::' : '->',
);
return $result;
}

/**
* Create a target from a function reflection.
*
* @param list<string> $path
*
* @return self<null>
*/
public static function fromReflectionFunction(\ReflectionFunction $reflection, array $path = []): self
{
/** @var self<null> $result */
$result = new self(path: $path, reflection: $reflection);
return $result;
}

/**
* Create a target from a closure.
*
* @param list<string> $path
*
* @return self<null>
*/
public static function fromClosure(\Closure $closure, array $path = []): self
{
return new self(reflection: $reflection);
return self::fromReflectionFunction(new \ReflectionFunction($closure), $path);
}

/**
* Create a target from a path string without reflection.
*
* @param non-empty-string $delimiter
*
* @return self<null>
*/
public static function fromPathString(string $path, string $delimiter = '.'): self
{
/** @psalm-suppress ArgumentTypeCoercion */
return new self(path: \explode($delimiter, $path), delimiter: $delimiter);
return self::fromPathArray(\explode($delimiter, $path), $delimiter);
}

/**
* Create a target from a path array without reflection.
*
* @param list<string> $path
* @return self<null>
*/
public static function fromPathArray(array $path, string $delimiter = '.'): self
{
return new self(path: $path, delimiter: $delimiter);
/** @var self<null> $result */
$result = new self(path: $path, delimiter: $delimiter);
return $result;
}

public static function fromPair(string $controller, string $action): self
/**
* Create a target from a controller and action pair.
* If the action is a method of the controller, the reflection will be set.
*
* @template T of object
*
* @param non-empty-string|class-string<T>|T $controller
* @param non-empty-string $action
*
* @return ($controller is class-string|T ? self<T> : self<null>)
*/
public static function fromPair(string|object $controller, string $action): self
{
$target = \method_exists($controller, $action)
? self::fromReflection(new \ReflectionMethod($controller, $action))
: self::fromPathArray([$controller, $action]);
return $target->withPath([$controller, $action]);
/** @psalm-suppress ArgumentTypeCoercion */
if (\is_object($controller) || \method_exists($controller, $action)) {
/** @var T|class-string<T> $controller */
return self::fromReflectionMethod(new \ReflectionMethod($controller, $action), $controller);
}

return self::fromPathArray([$controller, $action]);
}

public function getPath(): array
{
return match (true) {
$this->path !== null => $this->path,
$this->reflection instanceof \ReflectionMethod => [
$this->reflection->getDeclaringClass()->getName(),
$this->reflection->getName(),
],
$this->reflection instanceof \ReflectionFunction => [$this->reflection->getName()],
default => [],
};
return $this->path;
}

public function withPath(array $path): static
public function withPath(array $path, ?string $delimiter = null): static
{
$clone = clone $this;
$clone->path = $path;
$clone->delimiter = $delimiter ?? $clone->delimiter;
return $clone;
}

Expand All @@ -77,10 +153,8 @@ public function getReflection(): ?\ReflectionFunctionAbstract
return $this->reflection;
}

public function withReflection(?\ReflectionFunctionAbstract $reflection): static
public function getObject(): ?object
{
$clone = clone $this;
$clone->reflection = $reflection;
return $clone;
return $this->object;
}
}
26 changes: 20 additions & 6 deletions src/Hmvc/src/Interceptors/Context/TargetInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,46 @@
/**
* The target may be a concrete reflection or an alias.
* In both cases, you can get a path to the target.
*
* @template-covariant TController of object|null
*/
interface TargetInterface extends Stringable
{
/**
* @return list<string>
* Returns the path to the target.
* If the target was created from a method reflection, the path will contain
* the class name and the method name by default.
*
* @return list<string>|list{class-string<TController>, non-empty-string}
*/
public function getPath(): array;

/**
* @param list<string> $path
* @param string|null $delimiter The delimiter to use when converting the path to a string.
*/
public function withPath(array $path): static;
public function withPath(array $path, ?string $delimiter = null): static;

/**
* Returns the reflection of the target.
*
* It may be {@see \ReflectionFunction} or {@see \ReflectionMethod}.
* Note: {@see \ReflectionMethod::getDeclaringClass()} may return a parent class, but not the class used
* when the target was created. Use {@see Target::getPath()} to get the original target class.
*
* NOTE:
* The method {@see \ReflectionMethod::getDeclaringClass()} may return a parent class,
* but not the class used when the target was created.
* Use {@see getObject()} or {@see Target::getPath()}[0] to get the original object or class name.
*
* @psalm-pure
*/
public function getReflection(): ?\ReflectionFunctionAbstract;

/**
* @param \ReflectionFunctionAbstract|null $reflection Pass null to remove the reflection.
* Returns the object associated with the target.
*
* If the object is present, it always corresponds to the method reflection from {@see getReflection()}.
*
* @return TController|null
*/
public function withReflection(?\ReflectionFunctionAbstract $reflection): static;
public function getObject(): ?object;
}
2 changes: 1 addition & 1 deletion src/Hmvc/src/Interceptors/Handler/ReflectionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function handle(CallContext $context): mixed
throw new TargetCallException("Action not found for target `{$context->getTarget()}`.");
}

$controller = $this->container->get($path[0]);
$controller = $context->getTarget()->getObject() ?? $this->container->get($path[0]);

// Validate method and controller
ActionResolver::validateControllerMethod($method, $controller);
Expand Down
18 changes: 18 additions & 0 deletions src/Hmvc/tests/Core/CoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

namespace Spiral\Tests\Core;

use Psr\Container\ContainerInterface;
use Spiral\Core\Container;
use Spiral\Core\Exception\ControllerException;
use Spiral\Core\ResolverInterface;
use Spiral\Interceptors\Context\CallContext;
use Spiral\Interceptors\Context\Target;
use Spiral\Interceptors\Handler\ReflectionHandler;
use Spiral\Testing\Attribute\TestScope;
use Spiral\Testing\TestCase;
use Spiral\Tests\Core\Fixtures\CleanController;
Expand Down Expand Up @@ -213,4 +217,18 @@ public function testHandleReflectionMethodFromExtendedAbstractClass(): void

self::assertSame('hello', $result);
}

public function testHandleReflectionMethodWithObject(): void
{
$c = new Container();
$handler = new SampleCore($c);
// Call Context
$service = new \Spiral\Tests\Interceptors\Unit\Stub\TestService();
$ctx = (new CallContext(Target::fromPair($service, 'parentMethod')->withPath(['foo', 'bar'])))
->withArguments(['HELLO']);

$result = $handler->handle($ctx);

self::assertSame('hello', $result);
}
}
Loading

0 comments on commit e6f66b2

Please sign in to comment.