Skip to content

Commit

Permalink
api: back to traditional api
Browse files Browse the repository at this point in the history
This new api is more in line with traditional routing apis in PHP. The previous api was extremely verbose and unnecesary.

BREAKING CHANGE: This removes a lot of methods and complexity.

- The ProtocolError exception was removed and replaced by case-by-case exceptions
- The error handler does not log errors anymore nor attempt to id requests
- Route now requires a handler
- FinalHandler now throws proper exceptions
  • Loading branch information
mnavarrocarter committed Dec 11, 2021
1 parent 7eac20e commit d81256a
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 292 deletions.
1 change: 1 addition & 0 deletions .castor/docker/main/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ RUN apk add --no-cache wget php8 \
php8-mbstring \
php8-openssl \
php8-pcntl \
php8-posix \
php8-pecl-xdebug

# Link PHP
Expand Down
34 changes: 18 additions & 16 deletions .castor/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Creating a router is extremely simple:
$router = Castor\Http\Router::create();
```

Castor router implements `Psr\Http\Server\RequestHandlerInterface` so you can use it to handle any PRS-7 Server Request.
Castor router implements `Psr\Http\Server\RequestHandlerInterface` so you can use it to handle any PSR-7 Server Request.

```php
<?php
Expand All @@ -21,23 +21,24 @@ $router = Castor\Http\Router::create();
$response = $router->handle($aRequest);
```

> NOTE: An empty router will throw a `Castor\Http\ProtocolError` when its `handle` method is called.
> NOTE: An empty router will throw a `Castor\Http\EmptyStackError` when its `handle` method is called.
You can add routes by calling `method` and `path` methods in the router instance and passing a handler.
You can add routes by calling methods named after http methods and passing a path and a handler.

```php
<?php

$router = $router = Castor\Http\Router::create();
$router->method('GET')->path('/users')->handler($listUsersHandler);
$router->method('GET')->path('/users/:id')->handler($findUserHandler);
$router->method('POST')->path('/users')->handler($createUserHandler);
$router->method('DELETE')->path('/users/:id')->handler($deleteUserHandler);
$router->get('/users', $listUsersHandler);
$router->get('/users/:id', $findUserHandler);
$router->post('/users', $createUserHandler);
$router->delete('/users/:id', $deleteUserHandler);
```

As you can see, you can pass routing parameters using `:<param_name>` notation when defining your route.

You can retrieve routing parameters using the `Castor\Http\Router\params` function and passing a Psr Server Request.
You can retrieve routing parameters by calling the `getAttribute` method on the `$request` and passing the param
name.

```php
<?php
Expand All @@ -51,7 +52,7 @@ class MyHandler implements PsrHandler
{
public function handle(PsrRequest $request): PsrResponse
{
$id = params($request)['id'] ?? null;
$id = $request->getAttribute('id');
// TODO: Do something with the id and return a response.
}
}
Expand All @@ -60,13 +61,14 @@ class MyHandler implements PsrHandler
## Path Handlers

As we have shown above, you can create a Route that responds to a method-path match with a specific handler. But you
can also create a Route that executes a handler upon matching a Path. Just call `path` without calling `method`.
can also create a Route that executes a handler upon matching a Path. Just call the `path` function in the router
class.

```php
<?php

$router = Castor\Http\Router::create();
$router->path('/users')->handler($aHandler);
$router->path('/users', $aHandler);
```

The `$aHandler` handler will be executed when the path matches `/users`.
Expand All @@ -77,11 +79,11 @@ By using path matching, you can mount routers on routers and build a routing tre

```php
$routerOne = Castor\Http\Router::create();
$routerOne->method('GET')->handler($aHandler);
$routerOne->method('GET')->path('/:id')->handler($aHandler);
$routerOne->get('/', $aHandler);
$routerOne->get('/:id', $aHandler);

$routerTwo = Castor\Http\Router::create();
$routerTwo->path('/users')->handler($routerOne);
$routerTwo->path('/users', $routerOne);
```

Here we mount `$routerOne` into the `/users` path in `$routerTwo`, which causes all the `$routerOne` routes to match
Expand All @@ -90,5 +92,5 @@ under `/users` path. For instance, the second route with id will match a `GET /u
## Error Handling

Once you have built all of your routes, we recommend wrapping the router into a `Castor\Http\ErrorHandler`. You will
need a Psr Response Factory, and passing a logger is highly recommended. This will print debugging information as well
as normalizing http responses.
need a Psr Response Factory. This is so the router can respond on errors and not simply throw
exceptions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
COMPOSE_FLAGS = --project-directory .castor/docker --env-file=.castor/docker/.env
COMPOSE_CMD = docker compose $(COMPOSE_FLAGS)
COMPOSE_CMD = docker-compose $(COMPOSE_FLAGS)

build:
$(COMPOSE_CMD) build
Expand Down
7 changes: 4 additions & 3 deletions src/DefaultFinalHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@
final class DefaultFinalHandler implements PsrHandler
{
/**
* @throws ProtocolError
* @throws MethodNotAllowed
* @throws RouteNotFound
*/
public function handle(Request $request): Response
{
$message = sprintf('Cannot serve %s %s:', $request->getMethod(), $request->getUri()->getPath());
$allowedMethods = $request->getAttribute(ALLOWED_METHODS_ATTR, []);
if ([] === $allowedMethods) {
throw new ProtocolError(404, $message.' Path not found');
throw new RouteNotFound('Route not found');
}

throw new ProtocolError(405, $message.' Allowed methods are '.implode(', ', $allowedMethods));
throw new MethodNotAllowed('Method not allowed', $allowedMethods);
}
}
4 changes: 1 addition & 3 deletions src/EmptyStackError.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

namespace Castor\Http;

use Exception;

class EmptyStackError extends Exception
class EmptyStackError extends RoutingError
{
}
81 changes: 9 additions & 72 deletions src/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Psr\Http\Message\ResponseInterface as PsrResponse;
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
use Psr\Http\Server\RequestHandlerInterface as PsrHandler;
use Psr\Log\LoggerInterface as PsrLogger;
use Throwable;

/**
Expand All @@ -32,98 +31,36 @@ final class ErrorHandler implements PsrHandler
{
private PsrHandler $next;
private PsrResponseFactory $response;
private ?PsrLogger $logger;
private string $requestIdHeader;
private bool $logClientErrors;

public function __construct(
PsrHandler $next,
PsrResponseFactory $response,
PsrLogger $logger = null,
string $requestIdHeader = 'X-Request-Id',
bool $logClientErrors = false
PsrResponseFactory $response
) {
$this->next = $next;
$this->response = $response;
$this->logger = $logger;
$this->requestIdHeader = $requestIdHeader;
$this->logClientErrors = $logClientErrors;
}

public function handle(PsrRequest $request): PsrResponse
{
$request = $this->identifyRequest($request);

try {
return $this->next->handle($request);
} catch (Throwable $e) {
if (!$e instanceof ProtocolError) {
$e = new ProtocolError(500, 'Internal Server Error', $e);
}
$this->logError($request, $e);

return $this->createErrorResponse($request, $e);
} catch (RouteNotFound $e) {
return $this->createErrorResponse($request, 404);
} catch (MethodNotAllowed $e) {
return $this->createErrorResponse($request, 405);
} catch (EmptyStackError | Throwable $e) {
return $this->createErrorResponse($request, 500);
}
}

private function logError(PsrRequest $request, ProtocolError $error): void
private function createErrorResponse(PsrRequest $request, int $status): PsrResponse
{
if (null === $this->logger) {
return;
}

$code = $error->getCode();

if ($code < 500 && !$this->logClientErrors) {
return;
}

$uri = (string) $request->getUri();
$method = $request->getMethod();
$msg = sprintf('Error %s while trying to %s %s', $code, $method, $uri);
$id = $request->getHeaderLine($this->requestIdHeader);

// We log the error in a error entry.
$this->logger->error($msg, [
'method' => $method,
'code' => $code,
'uri' => $uri,
'request_id' => $id,
'errors' => $error->toArray(),
]);

// We store the payload and headers received in a debug entry.
$this->logger->debug('Debugging information for request '.$id, [
'request_id' => $id,
'payload' => base64_encode((string) $request->getBody()),
'headers' => $request->getHeaders(),
]);
}

private function createErrorResponse(PsrRequest $request, ProtocolError $error): PsrResponse
{
$response = $this->response->createResponse($error->getCode())
->withHeader($this->requestIdHeader, $request->getHeaderLine($this->requestIdHeader))
$response = $this->response->createResponse($status)
->withHeader('Content-Type', 'text/plain')
;
$msg = sprintf('Could not %s %s', $request->getMethod(), $request->getUri());
$response->getBody()->write($msg);

return $response;
}

private function identifyRequest(PsrRequest $request): PsrRequest
{
$id = $request->getHeaderLine($this->requestIdHeader);
if ('' === $id) {
try {
$id = bin2hex(random_bytes(16));
} catch (\Exception $e) {
throw new \RuntimeException('Could not generate a unique id for request', 0, $e);
}
$request = $request->withHeader($this->requestIdHeader, $id);
}

return $request;
}
}
50 changes: 0 additions & 50 deletions src/FlattenRouteParams.php

This file was deleted.

35 changes: 35 additions & 0 deletions src/MethodNotAllowed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

/**
* @project Castor Router
* @link https://github.com/castor-labs/router
* @package castor/router
* @author Matias Navarro-Carter [email protected]
* @license MIT
* @copyright 2021 CastorLabs Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Castor\Http;

use Throwable;

class MethodNotAllowed extends RoutingError
{
private array $allowedMethods;

public function __construct($message = '', array $allowedMethods = [], $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->allowedMethods = $allowedMethods;
}

public function getAllowedMethods(): array
{
return $this->allowedMethods;
}
}
49 changes: 0 additions & 49 deletions src/ProtocolError.php

This file was deleted.

Loading

0 comments on commit d81256a

Please sign in to comment.