diff --git a/src/Attributes/Precondition.php b/src/Attributes/Precondition.php index 9c2b7eb..655d02f 100644 --- a/src/Attributes/Precondition.php +++ b/src/Attributes/Precondition.php @@ -14,22 +14,23 @@ #[Attribute(Attribute::TARGET_METHOD)] class Precondition implements AttributeContract { - public function __construct(private readonly string $validator) + private PreconditionValidator $validator; + + public function __construct(string $validator) { + $this->validator = new $validator(); } public function validate(Request $request): bool { - /** @var PreconditionValidator $validator */ - $validator = new $this->validator(); - if (empty($validator->parameter($request))) { - $validator->preProcess(); + if (empty($this->validator->parameter($request))) { + $this->validator->preProcess(); - throw new PreconditionRequiredException($validator->getRequiredMessage()); + throw new PreconditionRequiredException($this->validator->getRequiredMessage()); } - if (! $validator($request)) { - throw new PreconditionFailedException($validator->getFailedMessage()); + if (! ($this->validator)($request)) { + throw new PreconditionFailedException($this->validator->getFailedMessage()); } return true; diff --git a/src/Middleware/PreconditionRequest.php b/src/Middleware/PreconditionRequest.php index 5854946..26b7806 100644 --- a/src/Middleware/PreconditionRequest.php +++ b/src/Middleware/PreconditionRequest.php @@ -6,15 +6,19 @@ use Closure; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Route; +use Illuminate\Routing\Router; use OnurSimsek\Precondition\Reflection; use Symfony\Component\HttpFoundation\Response; class PreconditionRequest { - public function handle(Request $request, Closure $next): Response + public function __construct(private readonly Router $router, private readonly Reflection $reflection) { - $route = Route::getRoutes()->match($request); + } + + public function handle(Request $request, Closure $next) + { + $route = $this->router->getRoutes()->match($request); if (! $route->getControllerClass()) { return $next($request); } @@ -24,7 +28,7 @@ public function handle(Request $request, Closure $next): Response private function handleRequestUsingAttribute(Request $request, Closure $next, string $controller, string $action): Response { - $reflection = new Reflection($controller, $action); + $reflection = $this->reflection->reflect($controller, $action); if (! $reflection->hasPreconditionAttribute()) { return $next($request); } diff --git a/src/Reflection.php b/src/Reflection.php index 869d2d3..06053da 100644 --- a/src/Reflection.php +++ b/src/Reflection.php @@ -9,19 +9,26 @@ use ReflectionAttribute; use ReflectionMethod; -class Reflection extends ReflectionMethod +class Reflection { - /** @var ReflectionAttribute[] */ - private array $attributes; + private ReflectionMethod $reflectionMethod; + private ReflectionAttribute $attribute; - public function __construct(string $controller, string $action) + public function reflect(string $controller, string $action): static { - parent::__construct($controller, $action); + $this->reflectionMethod = new ReflectionMethod($controller, $action); + unset($this->attribute); + + return $this; } - private function getPreconditionAttribute(): array + private function getPreconditionAttribute(): ?ReflectionAttribute { - return $this->attributes ?? $this->attributes = $this->getAttributes(Precondition::class); + if (!$attributes = $this->reflectionMethod->getAttributes(Precondition::class)) { + return null; + } + + return $this->attribute ??= $attributes[0]; } public function hasPreconditionAttribute(): bool @@ -31,6 +38,6 @@ public function hasPreconditionAttribute(): bool public function getPreconditionInstance(): Attribute { - return $this->getPreconditionAttribute()[0]->newInstance(); + return $this->getPreconditionAttribute()->newInstance(); } } diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..abd1f65 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,19 @@ +precondition = new Precondition(LostUpdateValidator::class); + } + + #[Test] + public function throw_precondition_required_exception() + { + $request = $this->getMockBuilder(Request::class)->getMock(); + $request->expects($this->once()) + ->method('header') + ->with('If-Unmodified-Since') + ->willReturn(null); + + self::expectException(PreconditionRequiredException::class); + $this->precondition->validate($request); + } + + #[Test] + public function throw_precondition_failed_exception() + { + $updatedAt = Date::create(2024, 1, 15, 10, 00, 00, 'utc'); + + $article = new \stdClass(); + $article->updated_at = $updatedAt; + + $request = $this->getMockBuilder(Request::class)->getMock(); + $request->expects($this->exactly(2)) + ->method('header') + ->with('If-Unmodified-Since') + ->willReturn((clone $updatedAt)->addYear()); + + $request->expects($this->once()) + ->method('route') + ->with('article') + ->willReturn($article); + + self::expectException(PreconditionFailedException::class); + $this->precondition->validate($request); + } + + #[Test] + public function it_can_be_validate_a_proper_request() + { + $updatedAt = Date::create(2024, 1, 15, 10, 00, 00, 'utc'); + + $article = new \stdClass(); + $article->updated_at = $updatedAt; + + $request = $this->getMockBuilder(Request::class)->getMock(); + $request->expects($this->exactly(2)) + ->method('header') + ->with('If-Unmodified-Since') + ->willReturn($updatedAt); + + $request->expects($this->once()) + ->method('route') + ->with('article') + ->willReturn($article); + + self::assertTrue($this->precondition->validate($request)); + } +} diff --git a/tests/Unit/Fixtures/Controller.php b/tests/Unit/Fixtures/Controller.php new file mode 100644 index 0000000..ea43ac2 --- /dev/null +++ b/tests/Unit/Fixtures/Controller.php @@ -0,0 +1,20 @@ +json(); + } + + public function withoutPreconditionMethod(): JsonResponse + { + return response()->json(); + } +} diff --git a/tests/Unit/Fixtures/LostUpdateValidator.php b/tests/Unit/Fixtures/LostUpdateValidator.php new file mode 100644 index 0000000..535f8cd --- /dev/null +++ b/tests/Unit/Fixtures/LostUpdateValidator.php @@ -0,0 +1,19 @@ +header('If-Unmodified-Since'); + } + + public function __invoke(Request $request): bool + { + return $this->parameter($request) == $request->route('article')->updated_at; + } +} diff --git a/tests/Unit/Middleware/PreconditionRequestTest.php b/tests/Unit/Middleware/PreconditionRequestTest.php new file mode 100644 index 0000000..cc2d1a6 --- /dev/null +++ b/tests/Unit/Middleware/PreconditionRequestTest.php @@ -0,0 +1,134 @@ +getMockBuilder(Route::class)->disableOriginalConstructor()->getMock(); + $route->expects($this->once()) + ->method('getControllerClass') + ->willReturn(null); + + $routeCollection = $this->getMockBuilder(RouteCollectionInterface::class)->getMock(); + $routeCollection->expects($this->once()) + ->method('match') + ->willReturn($route); + + $router = $this->getMockBuilder(Router::class)->disableOriginalConstructor()->getMock(); + $router->expects($this->once()) + ->method('getRoutes') + ->willReturn($routeCollection); + + $reflection = $this->getReflectionMock(); + $middleware = new PreconditionRequest($router, $reflection); + + self::assertInstanceOf(Response::class, $middleware->handle(new Request(), fn (Request $request) => new Response())); + } + + #[Test] + public function it_should_not_be_touch_when_the_action_doesnt_have_the_attribute() + { + $route = $this->getMockBuilder(Route::class)->disableOriginalConstructor()->getMock(); + $route->expects($this->exactly(2)) + ->method('getControllerClass') + ->willReturn('UserController'); + + $route->expects($this->once()) + ->method('getActionMethod') + ->willReturn('index'); + + $routeCollection = $this->getMockBuilder(RouteCollectionInterface::class)->getMock(); + $routeCollection->expects($this->once()) + ->method('match') + ->willReturn($route); + + $router = $this->getMockBuilder(Router::class)->disableOriginalConstructor()->getMock(); + $router->expects($this->once()) + ->method('getRoutes') + ->willReturn($routeCollection); + + $reflection = $this->getReflectionMock(); + $reflection->expects($this->once()) + ->method('reflect') + ->willReturnSelf(); + + $reflection->expects($this->once()) + ->method('hasPreconditionAttribute') + ->willReturn(false); + + $middleware = new PreconditionRequest($router, $reflection); + + self::assertInstanceOf(Response::class, $middleware->handle(new Request(), fn (Request $request) => new Response())); + } + + #[Test] + public function it_can_be_validate_precondition_request() + { + $route = $this->getMockBuilder(Route::class)->disableOriginalConstructor()->getMock(); + $route->expects($this->exactly(2)) + ->method('getControllerClass') + ->willReturn('UserController'); + + $route->expects($this->once()) + ->method('getActionMethod') + ->willReturn('index'); + + $routeCollection = $this->getMockBuilder(RouteCollectionInterface::class)->getMock(); + $routeCollection->expects($this->once()) + ->method('match') + ->willReturn($route); + + $router = $this->getMockBuilder(Router::class)->disableOriginalConstructor()->getMock(); + $router->expects($this->once()) + ->method('getRoutes') + ->willReturn($routeCollection); + + $precondition = $this->getPreconditionAttributeMock(); + $precondition->expects($this->once()) + ->method('validate') + ->willReturn(true); + + $reflection = $this->getReflectionMock(); + $reflection->expects($this->once()) + ->method('reflect') + ->willReturnSelf(); + + $reflection->expects($this->once()) + ->method('hasPreconditionAttribute') + ->willReturn(true); + + $reflection->expects($this->once()) + ->method('getPreconditionInstance') + ->willReturn($precondition); + + $middleware = new PreconditionRequest($router, $reflection); + + self::assertInstanceOf(Response::class, $middleware->handle(new Request(), fn (Request $request) => new Response())); + } + + private function getReflectionMock(): \PHPUnit\Framework\MockObject\MockObject|Reflection + { + return $this->getMockBuilder(Reflection::class)->disableOriginalConstructor()->getMock(); + } + + private function getPreconditionAttributeMock(): \PHPUnit\Framework\MockObject\MockObject + { + return $this->getMockBuilder(Precondition::class)->disableOriginalConstructor()->getMock(); + } +} diff --git a/tests/Unit/PreconditionServiceProviderTest.php b/tests/Unit/PreconditionServiceProviderTest.php new file mode 100644 index 0000000..fae91f8 --- /dev/null +++ b/tests/Unit/PreconditionServiceProviderTest.php @@ -0,0 +1,17 @@ +reflection = new Reflection(); + } + + #[Test] + public function it_can_be_reflect_a_method() + { + $actual = $this->reflection->reflect(Controller::class, 'withPreconditionMethod'); + + self::assertInstanceOf(Reflection::class, $actual); + } + + #[Test] + public function it_can_be_found_the_attribute() + { + $reflection = $this->reflection->reflect(Controller::class, 'withPreconditionMethod'); + self::assertTrue($reflection->hasPreconditionAttribute()); + + $reflection = $this->reflection->reflect(Controller::class, 'withoutPreconditionMethod'); + self::assertFalse($reflection->hasPreconditionAttribute()); + } + + #[Test] + public function it_can_be_create_the_attribute_instance() + { + $reflection = $this->reflection->reflect(Controller::class, 'withPreconditionMethod'); + + self::assertInstanceOf(Precondition::class, $reflection->getPreconditionInstance()); + } +}