Building Modern APIs

A presentation at PHPUK in February 2020 in London, UK by Rob Allen

Slide 1

Slide 1

Building Modern APIs with Slim Framework Rob Allen, February 2020 Rob Allen ~ @akrabat

Slide 2

Slide 2

Today’s plan • Learn a bit about Slim for context • Learn the features of a good API • Put them into practice within a Slim application • Coffee & cake! Rob Allen ~ @akrabat

Slide 3

Slide 3

What do you know about APIs? Rob Allen ~ @akrabat

Slide 4

Slide 4

Rock-Paper-Scissors An API to play Rock-Paper-Scissors 1. Create a new game 2. Make first player’s move 3. Make second player’s move 4. Get result Rob Allen ~ @akrabat

Slide 5

Slide 5

Create a game $ curl -H “Content-Type: application/json” \ http://localhost:8888/games \ -d ‘{“player1”: “Rob”, “player2”: “Jon”}’ Rob Allen ~ @akrabat

Slide 6

Slide 6

Create a game $ curl -H “Content-Type: application/json” \ http://localhost:8888/games \ -d ‘{“player1”: “Rob”, “player2”: “Jon”}’ { “_links”: { “makeNextMove”: { “href”: “/games/2ab83e2a-98d0-4110-b3ae-90d6fbf5/moves”, “description”: “Make a player’s move” } } } Rob Allen ~ @akrabat

Slide 7

Slide 7

Make a move $ curl -H “Content-Type: application/json” \ http://localhost:8888/games/2ab83e2a-98d0-4110-b3ae-90d6fbf5/moves -d ‘{“player”: “Rob”, “move”: “rock”}’ Rob Allen ~ @akrabat

Slide 8

Slide 8

Make a move $ curl -H “Content-Type: application/json” \ http://localhost:8888/games/2ab83e2a-98d0-4110-b3ae-90d6fbf5/moves -d ‘{“player”: “Rob”, “move”: “rock”}’ { “_links”: { “makeNextMove”: { “href”: “/games/2ab83e2a-98d0-4110-b3ae-90d6fbf5/moves”, “description”: “Make player 2’s move” } } } Rob Allen ~ @akrabat

Slide 9

Slide 9

Make other move $ curl -H “Content-Type: application/json” \ http://localhost:8888/games/2ab83e2a-98d0-4110-b3ae-90d6fbf5/moves -d ‘{“player”: “Jon”, “move”: “rock”}’ Rob Allen ~ @akrabat

Slide 10

Slide 10

Make other move $ curl -H “Content-Type: application/json” \ http://localhost:8888/games/2ab83e2a-98d0-4110-b3ae-90d6fbf5/moves -d ‘{“player”: “Jon”, “move”: “rock”}’ { “result”: “Draw. Both players chose rock”, “_links”: { “newGame”: { “href”: “/games/”, “description”: “Start a new game” } } } Rob Allen ~ @akrabat

Slide 11

Slide 11

Slim The C in MVC Rob Allen ~ @akrabat

Slide 12

Slide 12

Slim Framework • Created by Josh Lockhart (phptherightway.com) • PSR-4 autoloading • PSR-7 Request and Response objects • PSR-15 Middleware and Request Handlers • PSR-11 DI container support Rob Allen ~ @akrabat

Slide 13

Slide 13

Bring your own components Slim provides the router and dispatcher: You provide: • PSR-7 component • PSR-11 DI container • View layer (templates) • Model layer (Database/ORM) Rob Allen ~ @akrabat

Slide 14

Slide 14

Getting started $ composer create-project akrabat/slim4-starter hello-api Slim4-Starter provides: • Slim 4 • Slim-Psr7 • PHP-DI • Monolog Rob Allen ~ @akrabat

Slide 15

Slide 15

Getting started Start a web server $ php -S 0.0.0.0:8888 -t public/ PHP 7.4.2 Development Server (http://0.0.0.0:8888) started Rob Allen ~ @akrabat

Slide 16

Slide 16

Getting started Rob Allen ~ @akrabat

Slide 17

Slide 17

Getting started Rob Allen ~ @akrabat

Slide 18

Slide 18

Directory structure ├── │ │ │ │ ├── │ ├── │ │ config/ ├── dependencies.php ├── middleware.php ├── routes.php └── settings.php public/ └── index.php src/ └── Handler/ └── HomePageHandler.php ├── │ │ ├── ├── └── var/ ├── cache/ └── log/ vendor/ composer.json composer.lock Rob Allen ~ @akrabat

Slide 19

Slide 19

How does Slim work? 1. Bootstrap application 2. Execute routing 3. Dispatch route handler 4. Return response Rob Allen ~ @akrabat

Slide 20

Slide 20

Routing 1. Inspect URL and matches segments 2. On match: • Add Route object to Request attribute (route) • Dispatch associated route handler Rob Allen ~ @akrabat

Slide 21

Slide 21

Route handlers • Manage business logic operations • Receive a PSR-7 ServerRequest • Return a PSR-7 Response • Implemented as PSR-15 RequestHandler Rob Allen ~ @akrabat

Slide 22

Slide 22

HomePageHandler class HomePageHandler implements RequestHandlerInterface { public function process( ServerRequestInterface $request, ) : ResponseInterface { $response = new Response(); $response->getBody()->write(“Hello $name”); } } Rob Allen ~ @akrabat

Slide 23

Slide 23

Coding time Create a Slim app Rob Allen ~ @akrabat

Slide 24

Slide 24

HTTP is the foundation Rob Allen ~ @akrabat

Slide 25

Slide 25

It’s all about HTTP HTTP is a stateless request/response protocol that operates by exchanging messages. RFC 7230 Rob Allen ~ @akrabat

Slide 26

Slide 26

Client request POST /v2.1/events/18/comments HTTP/1.1 Host: api.dev.joind.in User-Agent: curl/7.54.0 Accept: application/xml; q=0.8, application/json Content-Type: application/json Content-Length: 59 {“comment”:”Great Talk. Nothing wrong with it!”,”rating”:5} Rob Allen ~ @akrabat

Slide 27

Slide 27

Server response HTTP/1.1 401 Unauthorized Server: Apache/2.2.22 (Debian) Status: 400 Content-Length: 32 Connection: close Content-Type: application/json; charset=utf8 [“Invalid Authorization Header”] Rob Allen ~ @akrabat

Slide 28

Slide 28

PSR-7 OO interfaces to model HTTP • RequestInterface (& ServerRequestInterface) • ResponseInterface • UriInterface • UploadedFileInterface Rob Allen ~ @akrabat

Slide 29

Slide 29

Key feature 1: Immutability Request, Response, Uri & UploadFile are immutable $uri = new Uri(‘https://api.joind.in/v2.1/events’); $uri2 = $uri->withQuery(‘?filter=upcoming’); $uri3 = $uri->withQuery(‘?filter=cfp’); with() methods return a new object with the new information Rob Allen ~ @akrabat

Slide 30

Slide 30

Key feature 1: Immutability // This does not work $response = new Response(); $response->withStatus(200); $response->withHeader(‘Content-type’, ‘text/xml’); Rob Allen ~ @akrabat

Slide 31

Slide 31

Key feature 1: Immutability // This does not work $response = new Response(); $response->withStatus(200); $response->withHeader(‘Content-type’, ‘text/xml’); // This works: Reassign returned object $response = new Response(); $response = $response->withStatus(200); $response = $response->withHeader(‘Content-type’, ‘text/xml’); Rob Allen ~ @akrabat

Slide 32

Slide 32

Key feature 2: Streams Message bodies are streams $body = new Stream(); $body->write(‘<p>Hello’); $body->write(‘World</p>’); $response = (new Response()) ->withStatus(200, ‘OK’) ->withHeader(‘Content-Type’, ‘text/html’) ->withBody($body); Rob Allen ~ @akrabat

Slide 33

Slide 33

Let’s talk APIs Rob Allen ~ @akrabat

Slide 34

Slide 34

Features of a good API • Correctness • Malleability • Error handling • Documentation Rob Allen ~ @akrabat

Slide 35

Slide 35

tl;dr Ensure your API is maintainable and developer-friendly

Slide 36

Slide 36

Get it right! Rob Allen ~ @akrabat

Slide 37

Slide 37

Embrace HTTP methods Method GET PUT DELETE POST PATCH Used for Retrieve data Change data Delete data Change data Update data Idempotent? Yes Yes Yes No No (HTTP methods are also known as verbs) Rob Allen ~ @akrabat

Slide 38

Slide 38

HTTP method negotiation If a given resource (URI) doesn’t support the requested HTTP verb, then return: 405 Method Not Allowed Rob Allen ~ @akrabat

Slide 39

Slide 39

HTTP method negotiation If a given resource (URI) doesn’t support the requested HTTP verb, then return: 405 Method Not Allowed $ curl -i -X PUT -H “Accept: application/json” http://localhost:8888 HTTP/1.1 405 Method Not Allowed Allow: GET Content-type: application/json { “message”: “405 Method Not Allowed”, } Rob Allen ~ @akrabat

Slide 40

Slide 40

URLs matter • Be consistent • Prefer nouns for resource names • Plural names work best for collections • Unique URL for each resource (/users/123) • Resources can be named after business processes too (e.g. customer-enrolment, money-transfer, merges) Rob Allen ~ @akrabat

Slide 41

Slide 41

Status codes Send the right one for the right situation! 1xx 2xx 3xx 4xx 5xx Informational Success Redirection Client error Server error (We’re still working) (All done) (Go elsewhere) (Your fault) (Our fault) Rob Allen ~ @akrabat

Slide 42

Slide 42

Rock-Paper-Scissors Verb URL GET POST GET POST /games /games /games/123 /games/123/moves Purpose List all games Start a new game Find out about game 123 Play a move in game 123 Rob Allen ~ @akrabat

Slide 43

Slide 43

Routing Uris in Slim config/routes.php: $app->get( ‘/games’, App\Handler\ListGames::class )->setName(‘games’); Rob Allen ~ @akrabat

Slide 44

Slide 44

Routes have a method config/routes.php: $app->get( ‘/games’, App\Handler\ListGames::class )->setName(‘games’); // Method Rob Allen ~ @akrabat

Slide 45

Slide 45

$app method => HTTP verb $app->get() $app->post() $app->put() $app->patch() $app->delete() Multiple methods: $app->any() $app->map([‘GET’, ‘POST’], …); Rob Allen ~ @akrabat

Slide 46

Slide 46

Routes have a pattern config/routes.php: $app->get( ‘/games’, App\Handler\ListGames::class )->setName(‘games’); // Patten Rob Allen ~ @akrabat

Slide 47

Slide 47

Route pattern // literal $app->get(‘/games’, $handler); // placeholder within { & } (access as request attribute) $app->get(‘/games/{id}’, $handler); // constrain using regex $app->get(‘/games/{id:\d+}’, $handler); // optional segments use [ & ] $app->get(‘/games[/{id:\d+}]’, $handler); $app->get(‘/news[/{y:\d{4}}[/{m:\d{2}}]]’, $handler); Rob Allen ~ @akrabat

Slide 48

Slide 48

Routes have a handler config/routes.php: $app->get( ‘/games’, App\Handler\ListGames::class )->setName(‘games’); // Handler Rob Allen ~ @akrabat

Slide 49

Slide 49

Route handlers • Manage business logic operations • Receive a PSR-7 ServerRequest • Return a PSR-7 Response • Implemented as PSR-15 RequestHandler Rob Allen ~ @akrabat

Slide 50

Slide 50

Responses in Slim Create a new Response object and use the with methods: $response = new Response(); // status code $response = $response->withStatus(404); // headers $response = $response->withHeader(‘Content-Type’,’application/json’); // body $response->getBody()->write(json_encode([‘foo’ => ‘bar’]))); Rob Allen ~ @akrabat

Slide 51

Slide 51

Coding time Handlers Rob Allen ~ @akrabat

Slide 52

Slide 52

Incoming data Rob Allen ~ @akrabat

Slide 53

Slide 53

Content-Type handling The Content-Type header specifies the format of the incoming data $ curl http://localhost:8888/games \ -H “Content-Type: application/json” \ -d ‘{“player1”: “Rob”, “player2”: “Jon”}’ Rob Allen ~ @akrabat

Slide 54

Slide 54

Read with getBody() $app->post(‘/games’, function ($request, $response) { $data = $request->getBody(); $response->getBody()->write(print_r($data, true)); return $response; } ); Output is a string: ‘{“player1”: “Rob”, “player2”: “Jon”}’ Rob Allen ~ @akrabat

Slide 55

Slide 55

Read with getParsedBody() Add Slim’s body-parsing middleware to your app: $app->addBodyParsingMiddleware(); Use in your handler: $app->post(‘/games’, function ($request, $response) { $data = $request->getParsedBody(); return $response->write(print_r($data, true)); } ); Rob Allen ~ @akrabat

Slide 56

Slide 56

Read with getParsedBody() $ curl -H “Content-Type: application/json” \ -H “Content-Type: application/json” \ -d ‘{“player1”: “Rob”, “player2”: “Jon”}’ Output is an array: Array ( [player1] => Rob, [player2] => Jon ) Rob Allen ~ @akrabat

Slide 57

Slide 57

This also works with XML $ curl “http://localhost:8888/games” \ -H “Content-Type: application/xml” \ -d “<game><player1>Rob</player1><player2>Jon</player2></game>” Output: Array ( [player1] => Rob, [player2] => Jon ) Rob Allen ~ @akrabat

Slide 58

Slide 58

And form data curl “http://localhost:8888/games” \ -H “Content-Type: application/x-www-form-urlencoded” \ -d “player1=Rob’ -d ‘player2=Jon” Output: Array ( [player1] => Rob, [player2] => Jon ) Rob Allen ~ @akrabat

Slide 59

Slide 59

addBodyParsingMiddleware() ? Rob Allen ~ @akrabat

Slide 60

Slide 60

Middleware Middleware is code that exists between the request and response, and which can take the incoming request, perform actions based on it, and either complete the response or pass delegation on to the next middleware in the queue. Matthew Weier O’Phinney Rob Allen ~ @akrabat

Slide 61

Slide 61

Middleware Take a request, return a response Rob Allen ~ @akrabat

Slide 62

Slide 62

Middleware LIFO stack: $app->add(ValidationMiddleware::class); $app->add(AuthMiddleware::class); $app->add(AuthMiddleware::class); $app->addBodyParsingMiddleware(); $app->addErrorMiddleware(true, true, true); Rob Allen ~ @akrabat

Slide 63

Slide 63

PSR-15 MiddlewareInterface namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; interface MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface; } Rob Allen ~ @akrabat

Slide 64

Slide 64

PSR-15 MiddlewareInterface namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; interface MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface; } Rob Allen ~ @akrabat

Slide 65

Slide 65

PSR-15 MiddlewareInterface namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; interface MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface; } Rob Allen ~ @akrabat

Slide 66

Slide 66

PSR-15 MiddlewareInterface namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; interface MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface; } Rob Allen ~ @akrabat

Slide 67

Slide 67

TimerMiddleware class TimerMiddleware implements MiddlewareInterface { public function process($request, $handler) { $start = microtime(true); $response = $handler->handle($request); $taken = microtime(true) - $start; return $response->withHeader(‘Time-Taken’, $taken); } } Rob Allen ~ @akrabat

Slide 68

Slide 68

TimerMiddleware class TimerMiddleware implements MiddlewareInterface { public function process($request, $handler) { $start = microtime(true); $response = $handler->handle($request); $taken = microtime(true) - $start; return $response->withHeader(‘Time-Taken’, $taken); } } Rob Allen ~ @akrabat

Slide 69

Slide 69

TimerMiddleware class TimerMiddleware implements MiddlewareInterface { public function process($request, $handler) { $start = microtime(true); $response = $handler->handle($request); $taken = microtime(true) - $start; return $response->withHeader(‘Time-Taken’, $taken); } } Rob Allen ~ @akrabat

Slide 70

Slide 70

TimerMiddleware class TimerMiddleware implements MiddlewareInterface { public function process($request, $handler) { $start = microtime(true); $response = $handler->handle($request); $taken = microtime(true) - $start; return $response->withHeader(‘Time-Taken’, $taken); } } Rob Allen ~ @akrabat

Slide 71

Slide 71

TimerMiddleware class TimerMiddleware implements MiddlewareInterface { public function process($request, $handler) { $start = microtime(true); $response = $handler->handle($request); $taken = microtime(true) - $start; return $response->withHeader(‘Time-Taken’, $taken); } } Rob Allen ~ @akrabat

Slide 72

Slide 72

Route Handlers The other half of PSR-15! Rob Allen ~ @akrabat

Slide 73

Slide 73

Route Handlers Any callable! $app->add(‘/’, $app->add(‘/’, $app->add(‘/’, $app->add(‘/’, $app->add(‘/’, function ($request, $response) { … }); [‘RootController’, ‘aStaticFunction’]); [new RootController(), ‘aFunction’]); RootController::class.’:pingAction’); RootHander::class); Rob Allen ~ @akrabat

Slide 74

Slide 74

Use the PSR-15 one $app->add(‘/’, RootHandler::class); Rob Allen ~ @akrabat

Slide 75

Slide 75

PSR-15 RouteHandlerInterface namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; interface RequestHandlerInterface { public function handle( ServerRequestInterface $request ): ResponseInterface; } Rob Allen ~ @akrabat

Slide 76

Slide 76

PSR-15 RouteHandlerInterface namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; interface RequestHandlerInterface { public function handle( ServerRequestInterface $request ): ResponseInterface; } Rob Allen ~ @akrabat

Slide 77

Slide 77

PSR-15 RouteHandlerInterface namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; interface RequestHandlerInterface { public function handle( ServerRequestInterface $request ): ResponseInterface; } Rob Allen ~ @akrabat

Slide 78

Slide 78

PSR-15 RouteHandlerInterface namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; interface RequestHandlerInterface { public function handle( ServerRequestInterface $request ): ResponseInterface; } Rob Allen ~ @akrabat

Slide 79

Slide 79

HomePageHandler use use use use Psr\Http\Message\ResponseInterface; Psr\Http\Message\ServerRequestInterface as Request; Psr\Http\Server\RequestHandlerInterface; Slim\Psr7\Response; class HomePageHandler implements RequestHandlerInterface { public function handle(Request $request): ResponseInterface { $response = new Response(); $response->getBody()->write(‘Hello World’); return $response; } } Rob Allen ~ @akrabat

Slide 80

Slide 80

HomePageHandler use use use use Psr\Http\Message\ResponseInterface; Psr\Http\Message\ServerRequestInterface as Request; Psr\Http\Server\RequestHandlerInterface; Slim\Psr7\Response; class HomePageHandler implements RequestHandlerInterface { public function handle(Request $request): ResponseInterface { $response = new Response(); $response->getBody()->write(‘Hello World’); return $response; } } Rob Allen ~ @akrabat

Slide 81

Slide 81

HomePageHandler use use use use Psr\Http\Message\ResponseInterface; Psr\Http\Message\ServerRequestInterface as Request; Psr\Http\Server\RequestHandlerInterface; Slim\Psr7\Response; class HomePageHandler implements RequestHandlerInterface { public function handle(Request $request): ResponseInterface { $response = new Response(); $response->getBody()->write(‘Hello World’); return $response; } } Rob Allen ~ @akrabat

Slide 82

Slide 82

HomePageHandler use use use use Psr\Http\Message\ResponseInterface; Psr\Http\Message\ServerRequestInterface as Request; Psr\Http\Server\RequestHandlerInterface; Slim\Psr7\Response; class HomePageHandler implements RequestHandlerInterface { public function handle(Request $request): ResponseInterface { $response = new Response(); $response->getBody()->write(‘Hello World’); return $response; } } Rob Allen ~ @akrabat

Slide 83

Slide 83

Coding time Reading data Rob Allen ~ @akrabat

Slide 84

Slide 84

Malleability Rob Allen ~ @akrabat

Slide 85

Slide 85

Decouple your representation Rob Allen ~ @akrabat

Slide 86

Slide 86

Hypermedia • Media type used for a representation • Links relationships between representations and states • Decouples client from server • Rename endpoints at will • Re-home endpoints on different domains Rob Allen ~ @akrabat

Slide 87

Slide 87

JSON and Hypermedia JSON does not have a defined way of providing hypermedia links Options: • “Link” header (GitHub approach) • application/collection+json • application/hal+json Rob Allen ~ @akrabat

Slide 88

Slide 88

Hypermedia payloads • Links should be fully qualified • Always include self relation • Add links to other related resources & collections Rob Allen ~ @akrabat

Slide 89

Slide 89

application/hal+json http://stateless.co/hal_specification.html { “_links”: { “self”: { “href”: “https://example.com/orders/523” }, “warehouse”: { “href”: “https://example.com/warehouse/56” }, “invoice”: { “href”: “https://example.com/invoices/873” } }, “currency”: “GBP”, “status”: “shipped”, “total”: 123.45 } Rob Allen ~ @akrabat

Slide 90

Slide 90

Base URL in Slim class BaseUrlMiddleware implements MiddlewareInterface { public function process($request, $handler) { $uri = $request->getUri(); $scheme = $uri->getScheme(); $authority = $uri->getAuthority(); $rootUrl = ($scheme !== ” ? $scheme . ‘:’ : ”) . ($authority !== ” ? ‘//’ . $authority : ”); $request = $request->withAttribute(‘base_url’, $rootUrl); return $handler->handle($request); Rob Allen ~ @akrabat

Slide 91

Slide 91

Creating links public function handle(ServerRequestInterface $request) { $baseUrl = $request->getAttribute(‘base_url’); $listGamesUrl = $baseUrl . ‘/games’; $gameUrl = $baseUrl . ‘/games/’ . $id; // … Rob Allen ~ @akrabat

Slide 92

Slide 92

HAL payloads: collection $hal = new Hal($baseUrl . ‘/games’); foreach ($games as $game) { $data[‘player1’] = $game->player1(); $data[‘player2’] = $game->player2(); //… $self = $baseUrl . ‘/games/’ . $game->id; $resource = new Hal($self, $data); $hal->addResource(‘game’, $resource); } $hal->setData([‘count’ => count($games)]); $json = $hal->asJson(true); Rob Allen ~ @akrabat

Slide 93

Slide 93

HAL output $ curl http://localhost:8888/games/0a1846ab-df37-4f7b-8e42-c0ef3ec { “player1”: “Rob”, “player2”: “Jon”, “status”: “Game complete”, “created”: “2019-09-30 08:49:33”, “result”: “Rob wins. rock beats scissors.”, “winner”: “Rob”, “_links”: { … } } Rob Allen ~ @akrabat

Slide 94

Slide 94

HAL links { … “winner”: “Rob”, “_links”: { “self”: { “href”: “http://localhost:8888/games/0a1846ab-df37-4f7b-8e42… }, “newGame”: { “href”: “http://localhost:8888/games/”, “description”: “Start a new game” } } } Rob Allen ~ @akrabat

Slide 95

Slide 95

Pagination Mobile devices don’t that much memory! • Link relations: first, last, next & prev relations • Include total count of items too Rob Allen ~ @akrabat

Slide 96

Slide 96

Coding time Hypermedia Rob Allen ~ @akrabat

Slide 97

Slide 97

Error handling Rob Allen ~ @akrabat

Slide 98

Slide 98

Error handling • Internal logging of errors for ourselves • Error representations are first class citizens • Code for computers, messages for humans Rob Allen ~ @akrabat

Slide 99

Slide 99

Injecting a logger More PSRs! 11 & 3 Rob Allen ~ @akrabat

Slide 100

Slide 100

Configure PHP-DI $containerBuilder = new ContainerBuilder(); $containerBuilder->addDefinitions([ // Factory for a PSR-3 logger LoggerInterface::class => function (ContainerInterface $c) { $logger = new Logger($settings[‘name’]); $logger->pushHandler(new ErrorLogHandler()); return $logger; }, ]); AppFactory::setContainer($containerBuilder->build()); $app = AppFactory::create(); Rob Allen ~ @akrabat

Slide 101

Slide 101

Configure PHP-DI $containerBuilder = new ContainerBuilder(); $containerBuilder->addDefinitions([ // Factory for a PSR-3 logger LoggerInterface::class => function (ContainerInterface $c) { $logger = new Logger($settings[‘name’]); $logger->pushHandler(new ErrorLogHandler()); return $logger; }, ]); AppFactory::setContainer($containerBuilder->build()); $app = AppFactory::create(); Rob Allen ~ @akrabat

Slide 102

Slide 102

Configure PHP-DI $containerBuilder = new ContainerBuilder(); $containerBuilder->addDefinitions([ // Factory for a PSR-3 logger LoggerInterface::class => function (ContainerInterface $c) { $logger = new Logger($settings[‘name’]); $logger->pushHandler(new ErrorLogHandler()); return $logger; }, ]); AppFactory::setContainer($containerBuilder->build()); $app = AppFactory::create(); Rob Allen ~ @akrabat

Slide 103

Slide 103

Configure PHP-DI $containerBuilder = new ContainerBuilder(); $containerBuilder->addDefinitions([ // Factory for a PSR-3 logger LoggerInterface::class => function (ContainerInterface $c) { $logger = new Logger($settings[‘name’]); $logger->pushHandler(new ErrorLogHandler()); return $logger; }, ]); AppFactory::setContainer($containerBuilder->build()); $app = AppFactory::create(); Rob Allen ~ @akrabat

Slide 104

Slide 104

Configure PHP-DI $containerBuilder = new ContainerBuilder(); $containerBuilder->addDefinitions([ // Factory for a PSR-3 logger LoggerInterface::class => function (ContainerInterface $c) { $logger = new Logger($settings[‘name’]); $logger->pushHandler(new ErrorLogHandler()); return $logger; }, ]); AppFactory::setContainer($containerBuilder->build()); $app = AppFactory::create(); Rob Allen ~ @akrabat

Slide 105

Slide 105

PHP-DI autowiring Just type-hint your constructor! class GetGameHandler implements RequestHandlerInterface { private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } … Rob Allen ~ @akrabat

Slide 106

Slide 106

Logging class GetGameHandler implements RequestHandlerInterface { public function handle(Request $request): ResponseInterface this->logger->info(“Fetching game”, [‘id’ => $id]); try { $games = $this->gameRepository->loadById($id); } catch (NotFoundException $e) { this->logger->info(“No game found”, [‘id’ => $id]); throw new HttpNotFoundException($request, ‘No Game’, $e); } … Rob Allen ~ @akrabat

Slide 107

Slide 107

Logging class GetGameHandler implements RequestHandlerInterface { public function handle(Request $request): ResponseInterface this->logger->info(“Fetching game”, [‘id’ => $id]); try { $games = $this->gameRepository->loadById($id); } catch (NotFoundException $e) { this->logger->info(“No game found”, [‘id’ => $id]); throw new HttpNotFoundException($request, ‘No Game’, $e); } … Rob Allen ~ @akrabat

Slide 108

Slide 108

Rendering errors Rob Allen ~ @akrabat

Slide 109

Slide 109

Slim’s error handling Add Slim’s error handling middleware to render exceptions $displayDetails = true; $logErrors = true; $logDetails = true; $app->addErrorMiddleware($displayDetails, $logErrors, $logDetails); Rob Allen ~ @akrabat

Slide 110

Slide 110

Error rendering $ http -j DELETE http://localhost:8888/game Rob Allen ~ @akrabat

Slide 111

Slide 111

Error rendering $ http -j DELETE http://localhost:8888/game HTTP/1.1 405 Method Not Allowed Allow: GET Content-type: application/json Rob Allen ~ @akrabat

Slide 112

Slide 112

Error rendering $ http -j DELETE http://localhost:8888/game HTTP/1.1 405 Method Not Allowed Allow: GET Content-type: application/json { “exception”: [ { “code”: 405, “file”: “…/Slim/Middleware/RoutingMiddleware.php”, “message”: “Method not allowed: Must be one of: GET”, “type”: “Slim\Exception\HttpMethodNotAllowedException” } ], “message”: “Method not allowed: Must be one of: GET” } Rob Allen ~ @akrabat

Slide 113

Slide 113

Convert errors to HTTP exceptions public function handle(Request $request): ResponseInterface { $id = $request->getAttribute(‘id’); try { $game = $this->gameRepository->loadById($id); } catch (GameNotFoundException $e) { throw new HttpNotFoundException($request, ‘No Game’, $e); } catch (Exception $e) { throw new HttpInternalServerErrorException($request, ‘An unknown error occurred’, $e); } Rob Allen ~ @akrabat

Slide 114

Slide 114

Not found error public function handle(Request $request): ResponseInterface { $id = $request->getAttribute(‘id’); try { $game = $this->gameRepository->loadById($id); } catch (GameNotFoundException $e) { throw new HttpNotFoundException($request, ‘No Game’, $e); } catch (Exception $e) { throw new HttpInternalServerErrorException($request, ‘An unknown error occurred’, $e); } Rob Allen ~ @akrabat

Slide 115

Slide 115

Not found error $ http -j http://localhost:8888/games/1234 HTTP/1.1 404 Not Found Content-type: application/json { “message”: “No Game” } (Production mode: $displayDetails = false) Rob Allen ~ @akrabat

Slide 116

Slide 116

Generic error public function handle(Request $request): ResponseInterface { $id = $request->getAttribute(‘id’); try { $game = $this->gameRepository->loadById($id); } catch (GameNotFoundException $e) { throw new HttpNotFoundException($request, ‘No Game’, $e); } catch (Exception $e) { throw new HttpInternalServerErrorException($request, ‘An unknown error occurred’, $e); } Rob Allen ~ @akrabat

Slide 117

Slide 117

Generic error $ http -j http://localhost:8888/games/abcd HTTP/1.1 500 Internal Server Error Content-type: application/json { “exception”: [ { “code”: 500, “file”: “…/src/Handler/GetGameHandler.php”, “line”: 43, “message”: “An unknown error occurred”, “type”: “Slim\Exception\HttpInternalServerErrorException” }, Rob Allen ~ @akrabat

Slide 118

Slide 118

{ “code”: 40, “file”: “…/lib/Assert/Assertion.php”, “line”: 2752, “message”: “Value “abcd” is not a valid integer.”, “type”: “Assert\InvalidArgumentException” } ], “message”: “An unknown error occurred” } (Development mode: $displayDetails = true) Rob Allen ~ @akrabat

Slide 119

Slide 119

Coding time Error responses Rob Allen ~ @akrabat

Slide 120

Slide 120

Documentation Rob Allen ~ @akrabat

Slide 121

Slide 121

Documentation • Tutorials • Reference Rob Allen ~ @akrabat

Slide 122

Slide 122

OpenAPI Specification • Spec-first API design • Tooling: https://openapi.tools • Reference website: ReDoc • Linting/validation: Spectral • Mock server: Prism Rob Allen ~ @akrabat

Slide 123

Slide 123

OpenAPI Specification openapi: “3.0.2” info: title: Rock-Paper-Scissors version: “1.0.0” description: An implementation of Rock-Paper-Scissors contact: name: Rob Allen servers: - url: https://rock-paper-scissors.example.com Rob Allen ~ @akrabat

Slide 124

Slide 124

OpenAPI Specification paths: /games: post: summary: Create a new game description: Create a new game of Rock-Paper-Scissors operationId: createGame tags: - Game requestBody: description: Game to add content: application/json: schema: $ref: ‘#/components/schemas/NewGameRequest’ Rob Allen ~ @akrabat

Slide 125

Slide 125

Test with prism $ npm install -g @stoplight/prism-cli $ prism mock rps-openapi.yaml › › › › [CLI] [HTTP [CLI] [CLI] … awaiting Starting Prism… SERVER] info Server listening at http://127.0.0.1:4010 info POST http://127.0.0.1:4010/games info POST http://127.0.0.1:4010/games/e3..1/moves Rob Allen ~ @akrabat

Slide 126

Slide 126

Test with prism $ curl -i -X POST http://localhost:4010/games HTTP/1.1 400 Bad Request content-type: application/json {“message”:”Must provide both player1 and player2”} Rob Allen ~ @akrabat

Slide 127

Slide 127

Test with prism $ curl -i -X POST http://localhost:4010/games HTTP/1.1 400 Bad Request content-type: application/json {“message”:”Must provide both player1 and player2”} › › › › › › › [HTTP SERVER] post /games info Request received [NEGOTIATOR] info Request contains an accept header: / [VALIDATOR] warning Request did not pass the validation rules [NEGOTIATOR] success Found response 400. [NEGOTIATOR] success The response 400 has a schema. [NEGOTIATOR] success Responding with status code 400 [VALIDATOR] error Violation: request Body parameter is required Rob Allen ~ @akrabat

Slide 128

Slide 128

Demo The RPS OpenAPI Spec Rob Allen ~ @akrabat

Slide 129

Slide 129

To sum up Rob Allen ~ @akrabat

Slide 130

Slide 130

To sum up • HTTP method negotiation • Content-type negotiation • Hypermedia • Error handling • Documentation Rob Allen ~ @akrabat

Slide 131

Slide 131

Resources • https://akrabat.com/category/api/ • https://akrabat.com/category/slim-framework/ • https://github.com/akrabat/slim4-rps-api • https://apihandyman.io • https://apievangelist.com • http://restcookbook.com Rob Allen ~ @akrabat

Slide 132

Slide 132

Thank you! Rob Allen - Independent API developer http://akrabat.com - @akrabat Rob Allen ~ @akrabat

Slide 133

Slide 133

Photo credits - The Fat Controller: HiT Entertainment - Foundation: https://www.flickr.com/photos/armchairbuilder/6196473431 - APIs: https://www.flickr.com/photos/ebothy/15723500675 - Incoming Data: https://www.flickr.com/photos/natspressoffice/13085089605 - Computer code: https://www.flickr.com/photos/n3wjack/3856456237/ - Road sign: https://www.flickr.com/photos/ell-r-brown/6804246004 - Car crash: EuroNCAP - Writing: https://www.flickr.com/photos/froderik/9355085596/ - Error screen: https://www.flickr.com/photos/lrosa/2867091465/ - Books: https://www.flickr.com/photos/mdales/48981707361/ - Pattery wheel: https://www.flickr.com/photos/61091655@N00/6831352744/ - Rocket launch: https://www.flickr.com/photos/gsfc/16495356966 - Stars: https://www.flickr.com/photos/gsfc/19125041621 Rob Allen ~ @akrabat