Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor interceptors target #1106

Merged
merged 6 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading