From 70e658039366304e9f598e4ac5e6cb44f218b3ee Mon Sep 17 00:00:00 2001 From: "Mohammed Taha [BMT]" Date: Thu, 29 Jun 2023 18:58:01 +0200 Subject: [PATCH] init version 2.0 --- LICENSE | 7 ++ composer.json | 2 +- src/Controller.php | 163 ++++++++++++++++++++++++++++++++++ src/Dispatcher.php | 214 ++++++++++++++++++++++++++++----------------- src/Middleware.php | 11 +-- src/Register.php | 2 +- src/Route.php | 6 +- src/Utils.php | 41 ++++++++- 8 files changed, 357 insertions(+), 89 deletions(-) create mode 100644 LICENSE create mode 100644 src/Controller.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f7f7438 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/composer.json b/composer.json index 93bc172..35ff54d 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", "bmt/plural-converter": "1.0", - "effectra/http-server-handler":"1.0", + "effectra/http-server-handler":"2.0", "effectra/http-message": "1.0" }, "minimum-stability": "stable" diff --git a/src/Controller.php b/src/Controller.php new file mode 100644 index 0000000..906b454 --- /dev/null +++ b/src/Controller.php @@ -0,0 +1,163 @@ +request = $request; + $this->response = $response; + $this->args = $args; + $this->callback = $callback; + } + + /** + * Get the server request. + * + * @return ServerRequestInterface The server request. + */ + public function getRequest(): ServerRequestInterface + { + return $this->request; + } + + /** + * Set the server request. + * + * @param ServerRequestInterface $request The server request. + * + * @return void + */ + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } + + /** + * Get the response. + * + * @return ResponseInterface The response. + */ + public function getResponse(): ResponseInterface + { + return $this->response; + } + + /** + * Set the response. + * + * @param ResponseInterface $response The response. + * + * @return void + */ + public function setResponse(ResponseInterface $response): void + { + $this->response = $response; + } + + /** + * Get the arguments. + * + * @return array The arguments. + */ + public function getArgs(): array + { + return $this->args; + } + + /** + * Set the arguments. + * + * @param array $args The arguments. + * + * @return void + */ + public function setArgs(array $args): void + { + $this->args = $args; + } + + /** + * Convert the controller to an array. + * + * @return array The controller as an array. + */ + public function toArray(): array + { + return [ + $this->request, + $this->response, + (object) $this->args + ]; + } + + /** + * Set the callback function to handle the request. + * + * @param callable $callback The callback function. + * + * @return void + */ + public function setCallback(callable $callback) + { + $this->callback = $callback; + } + + /** + * Get the callback function. + * + * @return callable|null The callback function or null if not set. + */ + public function getCallback(): callable|null + { + return $this->callback; + } + + /** + * Handle the request and return the response. + * + * @throws InvalidCallbackException If the callback is not set. + * + * @return ResponseInterface The response. + */ + public function handle(): ResponseInterface + { + $this->args = array_merge($this->args, $this->request->getQueryParams()); + + if (!$this->callback) { + throw new InvalidCallbackException("Error Processing Response"); + } + + $response = call_user_func_array($this->callback, $this->toArray()); + + return $response; + } +} diff --git a/src/Dispatcher.php b/src/Dispatcher.php index 336a257..0311e99 100644 --- a/src/Dispatcher.php +++ b/src/Dispatcher.php @@ -7,22 +7,20 @@ use Effectra\Http\Message\Stream; use Effectra\Http\Server\RequestHandler; use Effectra\Router\Exception\InvalidCallbackException; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; trait Dispatcher { - /** - * @var Callback The callback instance. + * @var ServerRequestInterface The request object. */ - protected Callback $callback; + private ServerRequestInterface $request; /** - * @var RequestInterface The request object. + * @var ResponseInterface The response object. */ - private RequestInterface $request; + private ResponseInterface $response; /** * @var array The route arguments. @@ -34,18 +32,20 @@ trait Dispatcher */ protected $notFound; - - public function __construct( - ) { + /** + * Dispatcher constructor. + */ + public function __construct() + { $this->callback = new Callback(); } /** - * Dispatches the current request to the appropriate controller action and returns the HTTP response. - * - * @param ServerRequestInterface $request The HTTP request to dispatch. + * Dispatches the server request and returns a response. * - * @return ResponseInterface The HTTP response returned by the controller action. + * @param ServerRequestInterface $request The server request. + * @return ResponseInterface The response. + * @throws InvalidCallbackException If there is an error processing the response. */ public function dispatch(ServerRequestInterface $request): ResponseInterface { @@ -56,38 +56,71 @@ public function dispatch(ServerRequestInterface $request): ResponseInterface // Determine the appropriate controller action for the request. $action = $this->getAction($uri_path, $method); - // Add any query string arguments to the internal argument list. - $this->addArguments($request->getQueryParams()); - // Get the callback function for the selected controller action. $callback = isset($action['callback']) ? $this->callback->getCallback($action['callback']) : null; - // Pass the request, response, and arguments to the controller action. - $pass = $this->pass((object) $this->args); - // If no valid callback was found, return a 404 Not Found response. + $controller = new Controller($request, $this->response, $this->args, $callback); + + // send 404 response if no callback if (!$callback) { + if (!$this->notFound) { - $content = new Stream(HtmlRender::notFoundHTML()); - return $this->response - ->withStatus(404) - ->withBody($content); + return $this->notFoundResponse(); + } - return call_user_func($this->notFound, $pass); - } - // Execute the controller action and return the resulting response. - $response = $this->process($callback, $pass); + $controller->setCallback($this->notFound); + } + // handle router middlewares if (!empty($action['middleware'])) { - // handle middleware - $response = $this->runMiddleware($request, new RequestHandler($response, $action['middleware'])); + + $handler = new RequestHandler($this->response, $action['middleware']); + + $responseMiddleware = $this->runMiddleware($this->request, $handler); + + $controller->setRequest($handler->getLastRequest()); + + $response = $controller->handle(); + + $response = $this->compareResponses($this->response, $responseMiddleware) ? $response : $responseMiddleware; + } else { + $response = $controller->handle(); } + // regenerate response if its string + if (is_string($response)) { + $response = $this->stringResponse($response); + } + + return $response; } + /** + * Sets the HTTP request to be sent to the controller. + * + * @param ServerRequestInterface $request The HTTP request to send to the client. + * @return void + */ + public function addRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } + + /** + * Sets the HTTP response to be sent to the controller. + * + * @param ResponseInterface $response The HTTP response to send to the client. + * @return void + */ + public function addResponse(ResponseInterface $response): void + { + $this->response = $response; + } + /** * Adds the specified arguments to the internal argument list. * @@ -100,91 +133,116 @@ public function addArguments(array $args): void // Merge the specified arguments with the existing arguments. $this->args = array_merge($this->args, $args); } + /** - * Sets the HTTP request to be sent to the controller. - * - * @param RequestInterface $response The HTTP response to send to the client. + * Sets the specified callback as the response returned when a route is not found. * + * @param callable $response The callback to set as the not found response. * @return void */ - public function addRequest(RequestInterface $request): void + public function setNotFound(callable $response): void { - $this->request = $request; + $this->notFound = $response; } + /** - * Sets the HTTP response to be sent to the controller. - * - * @param ResponseInterface $response The HTTP response to send to the client. + * Sets the specified callback as the response returned when an internal server error occurs. * + * @param callable $response The callback to set as the internal server error response. * @return void */ - public function addResponse(ResponseInterface $response): void + public function setInternalServerError(callable $response): void { - $this->response = $response; + $this->internalServerError = $response; } + /** - * Passes the server request to another part of the application for further processing. - * - * @param ServerRequestInterface $server_request The incoming server request object. - * @param ResponseInterface $response The response object to use for generating a response. - * @param array $args An array of route parameters extracted from the request URI. + * Converts the request object. * - * @return array An array containing the new request object, the original response object, and the route parameters. + * @param ServerRequestInterface $request The request object to convert. + * @param ServerRequestInterface $NewRequest The new request object to update. + * @return ServerRequestInterface The updated new request object. */ - public function pass(array|object $args) + public function convertRequest(ServerRequestInterface $request, ServerRequestInterface $NewRequest): ServerRequestInterface { - return [ - $this->request, - $this->response, - $args - ]; + + $NewRequest = $NewRequest->withMethod($request->getMethod()); + $NewRequest = $NewRequest->withUri($request->getUri()); + + foreach ($request->getHeaders() as $key => $value) { + $NewRequest = $NewRequest->withHeader($key, $value); + } + + $NewRequest = $NewRequest->withBody($request->getBody()); + $NewRequest = $NewRequest->withProtocolVersion($request->getProtocolVersion()); + $NewRequest = $NewRequest->withQueryParams($request->getQueryParams()); + $NewRequest = $NewRequest->withParsedBody($request->getParsedBody()); + + foreach ($request->getAttributes() as $key => $value) { + $NewRequest = $NewRequest->withAttribute($key, $value); + } + + return $NewRequest; } /** - * Calls the specified callback with the given parameters and returns the response. - * - * @param callable $callback The callback to call. - * @param array $pass The parameters to pass to the callback. + * Compares two response objects. * - * @return ResponseInterface The response returned by the callback. + * @param ResponseInterface $response1 The first response object. + * @param ResponseInterface $response2 The second response object. + * @return bool True if the responses are equal, false otherwise. */ - public function process(callable $callback, array $pass): ResponseInterface + public function compareResponses(ResponseInterface $response1, ResponseInterface $response2): bool { - $response = call_user_func_array($callback, $pass); + // Compare status codes + if ($response1->getStatusCode() !== $response2->getStatusCode()) { + return false; + } - if (!$response) { - throw new InvalidCallbackException("Error Processing Response"); + // Compare headers + $headers1 = $response1->getHeaders(); + $headers2 = $response2->getHeaders(); + + if (count($headers1) !== count($headers2)) { + return false; } - if (is_string($response)) { - $response = $this->response - ->withStatus(200) - ->withBody(new Stream($response)) - ->withHeader('Content-type', ['text/html; charset=UTF-8']); + + foreach ($headers1 as $name => $values) { + if (!isset($headers2[$name]) || $headers1[$name] !== $headers2[$name]) { + return false; + } } - return $response; + + // Compare bodies + $body1 = (string) $response1->getBody(); + $body2 = (string) $response2->getBody(); + + return $body1 === $body2; } /** - * Sets the specified callback as the response returned when a route is not found. - * - * @param callable $response The callback to set as the not found response. + * Generate a response with a string body. * - * @return void + * @param string $response The response string. + * @return ResponseInterface The generated response. */ - public function setNotFound(callable $response): void + public function stringResponse(string $response): ResponseInterface { - $this->notFound = $response; + return $response = $this->response + ->withStatus(200) + ->withBody(new Stream($response)) + ->withHeader('Content-type', ['text/html; charset=UTF-8']); } /** - * Sets the specified callback as the response returned when an internal server error occurs. + * Generate a "Not Found" response. * - * @param callable $response The callback to set as the internal server error response. - * - * @return void + * @return ResponseInterface The "Not Found" response. */ - public function setInternalServerError(callable $response): void + public function notFoundResponse(): ResponseInterface { - $this->internalServerError = $response; + $content = new Stream(HtmlRender::notFoundHTML()); + + return $this->response->withStatus(404)->withBody($content); } } diff --git a/src/Middleware.php b/src/Middleware.php index c5715d8..4c6bec3 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -18,6 +18,7 @@ trait Middleware protected array $middleware; + /** * Set the middleware to be applied to all routes. * @@ -31,7 +32,7 @@ public function middleware(string|MiddlewareInterface $middlewareClass): self throw new InvalidArgumentException("{$middlewareClass} is not a valid middleware class."); } - $this->routes[count($this->routes) - 1]['middleware'][] = $middlewareClass;; + $this->routes[count($this->routes) - 1]['middleware'][] = $middlewareClass; return $this; } @@ -39,13 +40,13 @@ public function middleware(string|MiddlewareInterface $middlewareClass): self /** * Run the middleware stack for a given request and handler. * - * @param ServerRequestInterface $request The incoming HTTP request. - * @param RequestHandlerInterface $handler The request handler for the route. + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler * @return ResponseInterface The response from the middleware stack. */ - protected function runMiddleware(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + protected function runMiddleware(ServerRequestInterface $request,RequestHandlerInterface $handler): ResponseInterface { - return $handler->handle($request); + return $handler->handle($request); } } diff --git a/src/Register.php b/src/Register.php index 6100e4d..14d6263 100644 --- a/src/Register.php +++ b/src/Register.php @@ -354,7 +354,7 @@ public function matchRoutePattern($path, $pattern) $pattern = preg_quote($pattern, '/'); // Replace "{id}" in the pattern with a regular expression to match any number - $pattern = str_replace('\{id\}', '(\d+|\w+)', $pattern); + $pattern = str_replace('\{id\}', '(\d+)', $pattern); // Replace "{type}" in the pattern with a regular expression to match any word characters $pattern = str_replace('\{type\}', '(\w+)', $pattern); diff --git a/src/Route.php b/src/Route.php index 2405318..099b97e 100644 --- a/src/Route.php +++ b/src/Route.php @@ -10,7 +10,7 @@ * @method \Effectra\Router\Middleware middleware(string|MiddlewareInterface $middlewareClass): self * * @method \Effectra\Router\Utils group(string $common_route, $controller, array $methods): self - * @method \Effectra\Router\Utils crud(string $route, $controller, string $actions): self + * @method \Effectra\Router\Utils crud(string $route, $controller, string $actions, ?MiddlewareInterface|array $middleware = null): self * @method \Effectra\Router\Utils auth(string $pattern, $controller): self * * @method \Effectra\Router\Register setPreRoute(string $preRoute): void @@ -27,13 +27,13 @@ * @method \Effectra\Router\Register getArguments(string $route_parts, string $pattern_parts): array * * @method \Effectra\Router\Dispatcher addArguments(array $args): void - * @method \Effectra\Router\Dispatcher addRequest(RequestInterface $request): void + * @method \Effectra\Router\Dispatcher addRequest(ServerRequestInterface $request): void * @method \Effectra\Router\Dispatcher addResponse(ResponseInterface $response): void * @method \Effectra\Router\Dispatcher setNotFound(callable $response): void * @method \Effectra\Router\Dispatcher setInternalServerError(callable $response): void */ -class Route implements RouterDispatcher +class Route { use Dispatcher, Register, Middleware, Utils; } diff --git a/src/Utils.php b/src/Utils.php index 32d0fe8..74b35cb 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -6,6 +6,7 @@ use Bmt\PluralConverter\PluralConverter; use Exception; +use Psr\Http\Server\MiddlewareInterface; trait Utils { @@ -49,6 +50,7 @@ public function auth(string $pattern, $controller): self } /** * Define a group of routes that share a common URL prefix. + *example `['get|info/{id}' => 'info']` * * @param string $common_route The common URL prefix for the group of routes. * @param mixed $controller The controller for the group of routes. @@ -102,7 +104,7 @@ public function group(string $common_route, $controller, array $methods): self *@return self Returns the instance of the Router. */ - public function crud(string $route, $controller, string $actions): self + public function crud(string $route, $controller, string $actions, MiddlewareInterface|array $middlewares = []): self { $route = $this->remakeRoute($route); @@ -115,30 +117,67 @@ public function crud(string $route, $controller, string $actions): self $converter = new PluralConverter(); if (method_exists($controller, $action) && $action === 'read') { + $this->get($converter->convertToPlural($route), [$controller, 'read']); + if (!empty($middlewares)) { + foreach ($middlewares as $middleware) { + $this->middleware($middleware); + } + } } if (method_exists($controller, $action) && $action === 'readOne') { $this->get($route . '/{id}', [$controller, 'readOne']); + if (!empty($middlewares)) { + foreach ($middlewares as $middleware) { + $this->middleware($middleware); + } + } } if (method_exists($controller, $action) && $action === 'create') { $this->post($route . '/create', [$controller, 'create']); + if (!empty($middlewares)) { + foreach ($middlewares as $middleware) { + $this->middleware($middleware); + } + } } if (method_exists($controller, $action) && $action === 'delete') { $this->delete($route . '/delete/{id}', [$controller, 'delete']); + if (!empty($middlewares)) { + foreach ($middlewares as $middleware) { + $this->middleware($middleware); + } + } } if (method_exists($controller, $action) && $action === 'deleteAll') { $this->delete($converter->convertToPlural($route) . '/delete-all', [$controller, 'deleteAll']); + if (!empty($middlewares)) { + foreach ($middlewares as $middleware) { + $this->middleware($middleware); + } + } } if (method_exists($controller, $action) && $action === 'update') { $this->put($route . '/update/{id}', [$controller, 'update']); + if (!empty($middlewares)) { + foreach ($middlewares as $middleware) { + $this->middleware($middleware); + } + } } if (method_exists($controller, $action) && $action === 'search') { + $this->get($converter->convertToPlural($route) . '/search', [$controller, 'search']); + if (!empty($middlewares)) { + foreach ($middlewares as $middleware) { + $this->middleware($middleware); + } + } } } }