Slim 4: PHP’s microframework

A presentation at Midwest PHP in April 2020 in Minneapolis, MN, USA by Rob Allen

Slide 1

Slide 1

Rob Allen, April 2020

Slide 2

Slide 2

The C in MVC Rob Allen ~ @akrabat

Slide 3

Slide 3

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

Slide 4

Slide 4

Hello world use … $app = AppFactory::create(); $app->get( ‘/ping’, function (Request $request, Response $response) { $response->getBody()->write(json_encode([‘ack’ => time()])); return $response->withHeader(‘Content-Type’, ‘application/json’); }); $app->run(); Rob Allen ~ @akrabat

Slide 5

Slide 5

Hello world use … $app = AppFactory::create(); // Init $app->get( ‘/ping’, function (Request $request, Response $response) { $response->getBody()->write(json_encode([‘ack’ => time()])); return $response->withHeader(‘Content-Type’, ‘application/json’); }); $app->run(); // Run Rob Allen ~ @akrabat

Slide 6

Slide 6

Hello world use … $app = AppFactory::create(); $app->get( // Method ‘/ping’, function (Request $request, Response $response) { $response->getBody()->write(json_encode([‘ack’ => time()])); return $response->withHeader(‘Content-Type’, ‘application/json’); }); $app->run(); Rob Allen ~ @akrabat

Slide 7

Slide 7

Hello world use … $app = AppFactory::create(); $app->get( ‘/ping’, // Pattern function (Request $request, Response $response) { $response->getBody()->write(json_encode([‘ack’ => time()])); return $response->withHeader(‘Content-Type’, ‘application/json’); }); $app->run(); Rob Allen ~ @akrabat

Slide 8

Slide 8

Hello world use … $app = AppFactory::create(); $app->get( ‘/ping’, function (Request $request, Response $response) { // Handler $response->getBody()->write(json_encode([‘ack’ => time()])); return $response->withHeader(‘Content-Type’, ‘application/json’); }); $app->run(); Rob Allen ~ @akrabat

Slide 9

Slide 9

Hello world $ curl -i http://localhost:8888/ping HTTP/1.1 200 OK Host: localhost:8888 Connection: close Content-Type: application/json { “ack”:1570046120 } Rob Allen ~ @akrabat

Slide 10

Slide 10

PSR-7 is the foundation Rob Allen ~ @akrabat

Slide 11

Slide 11

It’s all about HTTP Request: {METHOD} {URI} HTTP/1.1 Header: value1,value2 Another-Header: value Message body Rob Allen ~ @akrabat

Slide 12

Slide 12

It’s all about HTTP Response: HTTP/1.1 {STATUS_CODE} {REASON_PHRASE} Header: value Message body Rob Allen ~ @akrabat

Slide 13

Slide 13

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

Slide 14

Slide 14

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’); Rob Allen ~ @akrabat

Slide 15

Slide 15

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’, ‘application/header’) ->withBody($body); Rob Allen ~ @akrabat

Slide 16

Slide 16

I write APIs so let’s talk in that context! Rob Allen ~ @akrabat

Slide 17

Slide 17

A good API framework • Understands HTTP methods • Can receive data in various formats • Has useful error reporting Rob Allen ~ @akrabat

Slide 18

Slide 18

HTTP verbs Rob Allen ~ @akrabat

Slide 19

Slide 19

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 Rob Allen ~ @akrabat

Slide 20

Slide 20

Routing HTTP methods in Slim // specific HTTP methods map to methods on $app $app->get(‘/games’, ListGamesHandler::class); $app->post(‘/games’, CreateGameHandler::class); // routing multiple HTTP methods $app->any(‘/games’, GamesHandler::class); $app->map([‘GET’, ‘POST’], ‘/games’, GamesHandler::class); Rob Allen ~ @akrabat

Slide 21

Slide 21

Dynamic routes $app->get(‘/games/{id}’, function($request, $response) { $id = $request->getAttribute(‘id’); $games = $this->gameRepository->loadById($id); $body = json_encode([‘game’ => $game]); $response->getBody()->write($body); $response = $response->withHeader( ‘Content-Type’, ‘application/json’); return $response; }); Rob Allen ~ @akrabat

Slide 22

Slide 22

It’s just Regex // numbers only $app->get(‘/games/{id:\d+}’, $callable); // optional segments $app->get(‘/games[/{id:\d+}]’, $callable); $app->get(‘/news[/{y:\d{4}}[/{m:\d{2}}]]’, $callable); Rob Allen ~ @akrabat

Slide 23

Slide 23

Invalid HTTP request methods If the HTTP method is not supported, return the 405 status code $ http —json PUT http://localhost:8888/ping HTTP/1.1 405 Method Not Allowed Allow: GET Connection: close Content-Length: 53 Content-type: application/json Host: localhost:8888 { “message”: “Method not allowed. Must be one of: GET” } Rob Allen ~ @akrabat

Slide 24

Slide 24

Incoming data Rob Allen ~ @akrabat

Slide 25

Slide 25

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 26

Slide 26

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

Slide 27

Slide 27

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 28

Slide 28

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

Slide 29

Slide 29

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 30

Slide 30

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 31

Slide 31

addBodyParsingMiddleware() ? Rob Allen ~ @akrabat

Slide 32

Slide 32

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 33

Slide 33

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

Slide 34

Slide 34

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 35

Slide 35

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 36

Slide 36

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 37

Slide 37

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 38

Slide 38

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 39

Slide 39

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 40

Slide 40

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 41

Slide 41

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 42

Slide 42

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 43

Slide 43

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 44

Slide 44

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

Slide 45

Slide 45

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

Slide 46

Slide 46

Use this one $app->add(‘/ping’, PingHandler::class); Rob Allen ~ @akrabat

Slide 47

Slide 47

PSR-15 RequestHandlerInterface 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 48

Slide 48

PSR-15 RequestHandlerInterface 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 49

Slide 49

PSR-15 RequestHandlerInterface 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 50

Slide 50

PSR-15 RequestHandlerInterface 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 51

Slide 51

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

Slide 52

Slide 52

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

Slide 53

Slide 53

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

Slide 54

Slide 54

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

Slide 55

Slide 55

When things go wrong Rob Allen ~ @akrabat

Slide 56

Slide 56

Error handling • Internal logging • Rendering for output Rob Allen ~ @akrabat

Slide 57

Slide 57

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

Slide 58

Slide 58

PSR-11: Container interface The goal set by ContainerInterface is to standardize how frameworks and libraries make use of a container to obtain objects and parameters https://www.php-fig.org/psr/psr-11/ Rob Allen ~ @akrabat

Slide 59

Slide 59

PSR-3: Logger interface The main goal is to allow libraries to receive a Psr\Log\LoggerInterface object and write logs to it in a simple and universal way https://www.php-fig.org/psr/psr-3/ Rob Allen ~ @akrabat

Slide 60

Slide 60

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 61

Slide 61

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 62

Slide 62

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 63

Slide 63

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 64

Slide 64

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 65

Slide 65

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 66

Slide 66

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 67

Slide 67

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) { throw new HttpNotFoundException($request, ‘Game not found’, $e); } … Rob Allen ~ @akrabat

Slide 68

Slide 68

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) { throw new HttpNotFoundException($request, ‘Game not found’, $e); } … Rob Allen ~ @akrabat

Slide 69

Slide 69

Rendering errors Rob Allen ~ @akrabat

Slide 70

Slide 70

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

Slide 71

Slide 71

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 72

Slide 72

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

Slide 73

Slide 73

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

Slide 74

Slide 74

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

Slide 75

Slide 75

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

Slide 76

Slide 76

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 77

Slide 77

{ “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” } (With $displayDetails = true) Rob Allen ~ @akrabat

Slide 78

Slide 78

To sum up Rob Allen ~ @akrabat

Slide 79

Slide 79

Resources • http://slimframework.com • https://akrabat.com/category/slim-framework/ • https://github.com/akrabat/slim4-rps-api • https://github.com/akrabat/slim4-starter Rob Allen ~ @akrabat

Slide 80

Slide 80

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

Slide 81

Slide 81

Photo credits - The Fat Controller: HiT Entertainment - Foundation: https://www.flickr.com/photos/armchairbuilder/6196473431 - APIs: https://www.flickr.com/photos/ebothy/15723500675 - Verbs: https://www.flickr.com/photos/160866001@N07/45904136621/ - 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/thirdrail/18126260 - Rocket launch: https://www.flickr.com/photos/gsfc/16495356966 - Stars: https://www.flickr.com/photos/gsfc/19125041621 Rob Allen ~ @akrabat