Skip to content

Commit

Permalink
🔖 version 1.1 (#2)
Browse files Browse the repository at this point in the history
* ✅ begun adding tests for exception handling

* ✨ added exception handlers
  • Loading branch information
carbontwelve authored Dec 4, 2017
1 parent 227dfb3 commit fdabfab
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 15 deletions.
29 changes: 16 additions & 13 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 61 additions & 2 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use League\Container\ServiceProvider\AbstractServiceProvider;
use League\Event\EmitterTrait;
use League\Route\RouteCollection;
use Photogabble\Tuppence\ErrorHandlers\ExceptionHandler;
use Photogabble\Tuppence\ErrorHandlers\InvalidHandlerException;
use Photogabble\Tuppence\ErrorHandlers\InvalidHandlerResponseException;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\EmitterInterface;
use Zend\Diactoros\Response\SapiEmitter;
Expand All @@ -27,6 +30,11 @@ class App
*/
private $router;

/**
* @var null|ExceptionHandler
*/
private $exceptionHandler;

public function __construct(EmitterInterface $emitter = null)
{
$this->getContainer()->share('emitter', function () use ($emitter) {
Expand Down Expand Up @@ -149,13 +157,25 @@ public function patch($route, $action)
$this->getRouter()->map('PATCH', $route, $action);
}

/**
* Add a OPTIONS route.
*
* @param $route
* @param $action
*/
public function options($route, $action)
{
$this->getRouter()->map('OPTIONS', $route, $action);
}

/**
* @param ServerRequest|null $request
* @return \Psr\Http\Message\ResponseInterface
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
* @throws \Exception
*/
public function dispatch(ServerRequest $request = null)
public function dispatch(ServerRequest $request = null, $catchesExceptions = true)
{
$this->getContainer()->share('request', function () use ($request) {
if (is_null($request)) {
Expand All @@ -164,14 +184,34 @@ public function dispatch(ServerRequest $request = null)

return $request;
});

$this->emit('before.dispatch', $this->getContainer()->get('request'));

return $this->getRouter()->dispatch($this->getContainer()->get('request'), $this->getContainer()->get('response'));
try {
return $this->getRouter()->dispatch(
$this->getContainer()->get('request'),
$this->getContainer()->get('response')
);
} catch (\Exception $e) {
if (!$catchesExceptions || is_null($this->exceptionHandler)) {
throw $e;
}

$handler = $this->exceptionHandler;
$response = $handler($e);

if (!$response instanceof \Psr\Http\Message\ResponseInterface) {
throw new InvalidHandlerResponseException('The exception handler ['.get_class($handler).'] did not return a valid response.');
}

return $response;
}
}

/**
* @param ServerRequest|null $request
* @return \Psr\Http\Message\ResponseInterface
* @throws \Exception
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
Expand All @@ -183,4 +223,23 @@ public function run(ServerRequest $request = null)

return $response;
}

/**
* @param null|ExceptionHandler $exceptionHandler
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
* @throws InvalidHandlerException
*/
public function setExceptionHandler($exceptionHandler)
{
if (is_string($exceptionHandler)){
$exceptionHandler = $this->getContainer()->get($exceptionHandler);
}

if (!$exceptionHandler instanceof ExceptionHandler) {
throw new InvalidHandlerException('Exception handlers must implement the ExceptionHandler interface.');
}

$this->exceptionHandler = $exceptionHandler;
}
}
23 changes: 23 additions & 0 deletions src/ErrorHandlers/DefaultExceptionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Photogabble\Tuppence\ErrorHandlers;

use Exception;
use League\Route\Http\Exception\NotFoundException as RouteNotFoundException;
use League\Container\Exception\NotFoundException as ContainerNotFoundException;
use Zend\Diactoros\Response\JsonResponse;

class DefaultExceptionHandler implements ExceptionHandler
{
/**
* @param Exception|RouteNotFoundException|ContainerNotFoundException $e
* @return \Psr\Http\Message\ResponseInterface
*/
public function __invoke(Exception $e)
{
return new JsonResponse([
'message' => $e->getMessage(),
'trace' => explode(PHP_EOL, $e->getTraceAsString())
], (method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500));
}
}
13 changes: 13 additions & 0 deletions src/ErrorHandlers/ExceptionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Photogabble\Tuppence\ErrorHandlers;

interface ExceptionHandler
{
/**
* @param \Exception $e
* @return \Psr\Http\Message\ResponseInterface
*/
public function __invoke(\Exception $e);
}

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

namespace Photogabble\Tuppence\ErrorHandlers;

class InvalidHandlerException extends \Exception {}
5 changes: 5 additions & 0 deletions src/ErrorHandlers/InvalidHandlerResponseException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Photogabble\Tuppence\ErrorHandlers;

class InvalidHandlerResponseException extends \Exception {}
5 changes: 5 additions & 0 deletions tests/BootsApp.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ protected function assertResponseOk()
{
$this->assertEquals(200, $this->emitter->getResponse()->getStatusCode());
}

protected function assertResponseCodeEquals($code = 200)
{
$this->assertEquals($code, $this->emitter->getResponse()->getStatusCode());
}
}
97 changes: 97 additions & 0 deletions tests/Feature/RoutesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Photogabble\Tuppence\Tests\Feature;

use Photogabble\Tuppence\ErrorHandlers\DefaultExceptionHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Photogabble\Tuppence\Tests\BootsApp;
Expand Down Expand Up @@ -47,4 +48,100 @@ public function testGetWithQuery()
$this->assertResponseOk();
$this->assertEquals('123', $response);
}

public function testPost()
{
$this->app->post('/foo/bar', function (ServerRequestInterface $request, ResponseInterface $response){
$response->getBody()->write('Hello World!');
return $response;
});

$response = $this->runRequest(ServerRequestFactory::fromGlobals([
'HTTP_HOST' => 'example.com',
'REQUEST_METHOD' => 'POST',
'REQUEST_URI' => '/foo/bar',
], [], [], [], []));

$this->assertResponseOk();
$this->assertEquals('Hello World!', $response);
}

public function testPut()
{
$this->app->put('/foo/bar', function (ServerRequestInterface $request, ResponseInterface $response){
$response->getBody()->write('Hello World!');
return $response;
});

$response = $this->runRequest(ServerRequestFactory::fromGlobals([
'HTTP_HOST' => 'example.com',
'REQUEST_METHOD' => 'PUT',
'REQUEST_URI' => '/foo/bar',
], [], [], [], []));

$this->assertResponseOk();
$this->assertEquals('Hello World!', $response);
}

public function testPatch()
{
$this->app->patch('/foo/bar', function (ServerRequestInterface $request, ResponseInterface $response){
$response->getBody()->write('Hello World!');
return $response;
});

$response = $this->runRequest(ServerRequestFactory::fromGlobals([
'HTTP_HOST' => 'example.com',
'REQUEST_METHOD' => 'PATCH',
'REQUEST_URI' => '/foo/bar',
], [], [], [], []));

$this->assertResponseOk();
$this->assertEquals('Hello World!', $response);
}

public function testDelete()
{
$this->app->delete('/foo/bar', function (ServerRequestInterface $request, ResponseInterface $response){
$response->getBody()->write('Hello World!');
return $response;
});

$response = $this->runRequest(ServerRequestFactory::fromGlobals([
'HTTP_HOST' => 'example.com',
'REQUEST_METHOD' => 'DELETE',
'REQUEST_URI' => '/foo/bar',
], [], [], [], []));

$this->assertResponseOk();
$this->assertEquals('Hello World!', $response);
}

public function testOptions()
{
$this->app->options('/foo/bar', function (ServerRequestInterface $request, ResponseInterface $response){
$response->getBody()->write('Hello World!');
return $response;
});

$response = $this->runRequest(ServerRequestFactory::fromGlobals([
'HTTP_HOST' => 'example.com',
'REQUEST_METHOD' => 'OPTIONS',
'REQUEST_URI' => '/foo/bar',
], [], [], [], []));

$this->assertResponseOk();
$this->assertEquals('Hello World!', $response);
}

public function testExceptionHandled()
{
$this->app->setExceptionHandler(DefaultExceptionHandler::class);
$response = $this->runRequest(ServerRequestFactory::fromGlobals());

$this->assertResponseCodeEquals(404);

$decodedJson = json_decode($response, true);
$this->assertEquals('Not Found', $decodedJson['message']);
}
}
Loading

0 comments on commit fdabfab

Please # to comment.