Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RESTful Web Services with Silex

RESTful Web Services with Silex

Silex is a lightweight micro-framework built on Symfony components. Don’t let its small footprint fool you, though. Silex is powerful enough to form the backbone of even the most complex service-oriented application. In this talk, I will cover the basics of creating a Silex application, building service providers, and constructing RESTful controllers.

Samantha Quiñones

October 29, 2014
Tweet

More Decks by Samantha Quiñones

Other Decks in Technology

Transcript

  1. Topics • What is REST? • What is Silex? •

    Building Blocks • Books API • Caching • HATEOAS & HAL • Further Reading… • Contact Info & Feedback
  2. Beginnings • Introduced by Roy Fielding in 2000 • Architectural

    style designed for distributed systems • Described as a collection of constraints which define the roles and responsibilities of participants in a hypermedia system
  3. REST Constraints • Client-Server • Stateless • Cacheable • Uniform

    Interface • Layered System • Code-On-Demand
  4. REST Elements - Resources • Abstractions that have a durable

    and unique identifier • http://i.imgur.com/SNJHJyE.jpg
  5. REST Elements - Control Data • Metadata used to control

    how resources are accessed & consumed • Cache-control • Media type negotiation
  6. REST Elements - Connectors • Servers (apache, nginx) • Clients

    (browsers, API clients) • Caches (browser cache, Varnish) • Resolvers (bind) • Tunnels (SSL, SOCKS)
  7. What is Silex? • Micro-framework • Similar to Flask (python),

    Express (node.js). Inspired by Sinatra (ruby) • Created by Fabien Potencier & Igor Wiedler • Open Source (MIT License) • PHP 5.3+ • http://silex.sensiolabs.org
  8. Why use Silex? • Micro-framework “built on the shoulders of

    Symfony 2” • Simple API • Provides the “C” in MVC (or the “M” in RMR)
  9. What Does Silex Do? 1. Creates a Request object from

    globals 2. Dispatch Request to controller 3. Return Response to client
  10. Silex Application Silex\Application(); • Combination DIC & app kernel •

    Extends Pimple, a lightweight DIC • Implements Symfony’s HttpKernel interface
  11. <?php require_once __DIR__.'/../vendor/autoload.php'; $app = new Silex\Application(); $app->get('/hello', function ()

    use ($app) { return 'Hello world!'; }); $app->run(); Hello World Application Instance Route & Controller
  12. Routing <?php $app->get('/resource/{id}', function (Application $app, $id) { $resource =

    $app['resources']->getResourceById($id); if (null === $resource) { $app->abort(404, "Resource $id not found"); } return $app->json($resource); }); Method Route Pattern Controller
  13. Handling Errors $app->post('/resource', function (Request $request, Application $app) { $resource

    = json_decode($request->getContent(), true); if (empty($resource)) { return new Response('Invalid resource', 400); } try { $id = $app['db']->create($resource); } catch (\DatabaseException $e) { $app->abort(500, 'Failed to store resource'); } return new Response('Created', 201); }); Set Response Status Code Abort (throw exception)
  14. Handling Errors <?php $app->error(function(\Exception $exc, $code) use ($app) { $app['logger']->error('An

    error occurred! ' . $exc->getMessage()); }); $app->error(function(\Exception $exc, $code) { return new Response($exc->getMessage(), $code); }); Returning a response terminates the chain.
  15. Before Middleware <?php $app->before(function (Request $request, Application $app) { $app['profiler']->startRequest($request);

    }, Application::EARLY_EVENT); $app->before(function (Request $request) { if (0 === strpos($request->headers->get('Content-Type'), 'application/json')) { $data = json_decode($request->getContent(), true); $request->request->replace(is_array($data) ? $data : array()); } }); Run before routing & security After MW can be attached to App or Route
  16. After Middleware <?php $app->after(function (Request $request, Response $response) { if

    (!$response->headers->has('Content-MD5')) { $response->headers->set( 'Content-MD5', md5((string) $response->getContent()) ); } }); After MW can be attached to App or Route
  17. Middleware Execution Order 1. Application Before 2. Route Before 3.

    Route After 4. Application After 5. Application Finish
  18. DI & Pimple • Extremely small, simple, lightweight Dependency Injection

    container for PHP • Pure PHP or PHP Extension • Array-like interface (\ArrayAccess) • http://pimple.sensiolabs.org
  19. Pimple Example $container = new Pimple\Container(); $container['some.parameter'] = 'foo'; //

    adding a parameter $container['some.service'] = function ($c) { // adding a service return new ServiceClass(); }
  20. Shared Services <?php // Lazy-loaded: new Service created on each

    access $app['some.lazy.service'] = function () { return new Service(); }; // Lazy-loaded: Service created once $app['some.shared.service'] = $app->share(function () { return new Service(); });
  21. Service Dependencies <?php // Lazy-loaded: Service created once $app['some.shared.service'] =

    $app->share(function () { return new Service(); }); // Lazy-loaded $app['some.needy.service'] = $app->share(function ($app) { return new Service($app['some.shared.service']); }; Also lazy- loaded $app is provided
  22. Core Services • $app[‘request’] - Current request object • $app[‘routes’]

    - RouteCollection • $app[‘logger’] - PSR Logger And more!
  23. Core Parameters • $app[‘request.http_port’] = 80 • $app[‘request.https_port’] = 443

    • $app[‘locale’] = ‘en’ • $app[‘charset’] = ‘utf-8’ • $app[‘debug’] = false
  24. Service Providers • Classes that create service definitions • Analogous

    to packages & bundles in other frameworks • Help structure complex service definitions
  25. Built-In Providers • Doctrine • Monolog • Swift Mailer •

    Twig • HttpCache • Session • Serializer • Validator • Form • URL Generator And more!
  26. Custom Providers <?php class SomeServiceProvider implements ServiceProviderInterface { public function

    register(Application $app) { $app['some.service'] = $app->protect( function ($service_param) use ($app) { return new Service( $service_param, $app['some.dependency'] ) } ); } }
  27. composer.json { "name": "squinones/silex-rest", "description": "Code Examples for RESTful Webservices

    with Silex", "require": { "silex/silex": "~1.2", "symfony/serializer": "~2.5" }, "autoload": { "psr-4": { "Squinones\\ApiExample\\": "src/" } } } Silex Service provider
  28. Model Layer Book (src/models/Book.php) int getID() void setAuthor(string $author) string

    getAuthor() void setTitle(string $author) string getTitle() BookRepository (src/models/BookRepository.php) Array(<Book>) getAll() Book get(int $id) int|null save(Book $book) void delete(Book)
  29. Application Bootstrap $app = new Application(); $app['format'] = 'json'; $app['db']

    = $app->share(function() { $conn = 'sqlite:' . __DIR__ . '/../data/silex-rest.db'; return new \PDO($conn); }); Create App Setting Params Defining a DB Service
  30. Custom Service Definitions $app['repo.books'] = $app->share(function ($app) { return new

    BookRepository($app['db']); }); $app['converters.book'] = $app->protect(function ($id) use ($app) { $book = $app['repo.books']->get($id); if (!$book) { throw new NotFoundHttpException('Book '.$id.' not found'); } return $book; }); Repo Class Protected Service
  31. Registering Event Handlers $app->before(function (Request $request) { if (0 ===

    strpos($request->headers->get('Content-Type'), 'application/json')) { $data = json_decode($request->getContent(), true); $request->request->replace(is_array($data) ? $data : array()); } }); $app->after(function (Request $request, Response $response) { $response->headers->set('Content-Type', 'application/json'); }); $app->error(function (HttpException $exc, $code) { return new Response(null, $code); }); Error Handler Before After
  32. Get Collection $app->get('/books', function (Application $app) { $books = $app['repo.books']->getAll();

    return $app['serializer']->serialize($books, $app['format']); }); Get all books from Repo Return serialized info
  33. Get Collection > $ curl -XGET -i 'localhost:9999/books' HTTP/1.1 200

    OK Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: no-cache Date: Tue, 28 Oct 2014 20:58:36 GMT Content-Type: application/json [{"author":"Neil Gaiman","id":"1","title":"American Gods"},{"author":"Herman Melville","id":"2","title":"Moby Dick"},{"author":"Dick Hayhurst","id":"6","title":"The Bullpen Diaries"}]
  34. Getting a Book $app->get('/books/{book}', function (Application $app, Book $book) {

    return $app['serializer']->serialize($book, $app['format']); })->convert('book', $app['converters.book']) ->bind('book'); Input is Book Converter! Named controller
  35. Getting a Book > $ curl -XGET -i 'localhost:9999/books/1' HTTP/1.1

    200 OK Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: no-cache Date: Tue, 28 Oct 2014 21:02:11 GMT Content-Type: application/json {"author":"Neil Gaiman","id":"1","title":"American Gods"}
  36. Getting a !Book > $ curl -XGET -i 'localhost:9999/books/42' HTTP/1.1

    404 Not Found Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: no-cache Date: Tue, 28 Oct 2014 21:03:53 GMT Content-Type: application/json
  37. Converter Aborts Early $app['converters.book'] = $app->protect(function ($id) use ($app) {

    $book = $app['repo.books']->get($id); if (!$book) { throw new NotFoundHttpException('Book '.$id.' not found'); } return $book; }); Bypasses controller
  38. Creating a Book $app->post('/books', function (Application $app, Request $request) {

    $book = new Book(); $book->setAuthor($request->request->get('author')); $book->setTitle($request->request->get('title')); $id = $app['repo.books']->save($book); $response = new Response(null, 201); $response->headers->set( 'Location', $app['url_generator']->generate('book', ['book' => $id]) ); return $response; }); Request data from Before MW
  39. Creating a Book > $ curl -XPOST -i 'localhost:9999/books' -H

    "Content- Type: application/json" --data '{"author":"Jules Verne", "title": "Vingt mille lieues sous les mers"}' HTTP/1.1 201 Created Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: no-cache Date: Tue, 28 Oct 2014 21:20:52 GMT Location: /books/11 Content-Type: application/json Location header
  40. Creating a Book > $ curl -XGET -i 'localhost:9999/books/11' HTTP/1.1

    200 OK Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: no-cache Date: Tue, 28 Oct 2014 21:21:27 GMT Content-Type: application/json {"author":"Jules Verne","id":"11","title":"Vingt mille lieues sous les mers"}
  41. Updating a Book $app->put('/books/{book}', function (Application $app, Request $request, Book

    $book) { $book->setAuthor($request->request->get('author')); $book->setTitle($request->request->get('title')); $app['repo.books']->save($book); return new Response(null, 200); })->convert('book', $app['converters.book']); Request data from Before MW
  42. Updating a Book > $ curl -XPUT -i 'localhost:9999/books/10' -H

    "Content- Type: application/json" --data '{"author":"Jules Verne", "title": "20,000 Leagues Under the Sea"}' HTTP/1.1 200 OK Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: no-cache Date: Tue, 28 Oct 2014 21:35:33 GMT Content-Type: application/json
  43. Updating a Book > $ curl -XGET -i 'localhost:9999/books/10' HTTP/1.1

    200 OK Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: no-cache Date: Tue, 28 Oct 2014 21:39:33 GMT Content-Type: application/json {"author":"Jules Verne","id":"10","title":"20,000 Leagues Under the Sea"}
  44. Deleting a Book $app->delete('/books/{book}', function (Application $app, Book $book) {

    $app['repo.books']->delete($book); return new Response(null, 204); })->convert('book', $app['converters.book']); Book from Converter
  45. Deleting a Book > $ curl -XDELETE -i 'localhost:9999/books/10' HTTP/1.1

    204 No Content Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: no-cache Date: Tue, 28 Oct 2014 21:41:59 GMT Content-Type: application/json
  46. Deleting a Book > $ curl -XGET -i 'localhost:9999/books/10' HTTP/1.1

    404 Not Found Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: no-cache Date: Tue, 28 Oct 2014 21:42:26 GMT Content-Type: application/json Book is gone :c
  47. $app->after(function (Request $request, Response $response) use ($app) { if (!$response->getMaxAge())

    { $response->setMaxAge(3600); } $response->setPublic(); }); After Middleware Symfony Repsonse Cache helpers
  48. Cache Headers curl -XGET -i 'localhost:9999/books/10' HTTP/1.1 404 Not Found

    Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: max-age=3600, public Date: Tue, 28 Oct 2014 21:56:17 GMT Content-Type: application/json
  49. Overriding Cache Control $app->get('/books', function (Application $app) { $books =

    $app['repo.books']->getAll(); return $app['serializer']->serialize($books, $app['format']); })->after(function (Request $request, Response $response) { $response->setMaxAge(60); }); Runs before App Middleware
  50. Overriding Cache Control > $ curl -XGET -i 'localhost:9999/books' HTTP/1.1

    200 OK Host: localhost:9999 Connection: close X-Powered-By: PHP/5.4.30 Cache-Control: max-age=60, public Date: Wed, 29 Oct 2014 18:17:33 GMT Content-Type: application/json+hal
  51. HATEOAS • Representations of resources in a hypermedia system should

    reflect their relationships to themselves and other resources
  52. JSON != Hypermedia • JSON is not a hypermedia format

    • Proposed extensions to JSON to meet hypermedia needs
  53. JSON + HAL • Hypertext Application Language • Proposed by

    Mike Kelly • Describes a convention for exposing hypermedia links in JSON and XML
  54. JSON + HAL { "id": "1", "title": "American Gods", "author":

    "Neil Gaiman", "_links": { "self": { "href": "/books/1" } } }
  55. A New Service $app['resources.book'] = $app->protect(function (Book $book) use ($app)

    { return [ 'id' => $book->getId(), 'title' => $book->getTitle(), 'author' => $book->getAuthor(), '_links' => [ 'self' => [ 'href' => $app['url_generator']->generate( 'book', [ 'book' => $book->getId() ] ) ] ] ]; });
  56. Collection Wrapper $app->get('/books', function (Application $app) { $books = $app['repo.books']->getAll();

    $collection = [ "count" => count($books), "total" => count($books), "_embedded" => [ "books" => array_map($app['resources.book'], $books), ], "_links" => [ 'self' => [ 'href' =>$app['url_generator']->generate('books') ] ] ]; return $app['serializer']->serialize($collection, $app['format']); })->after(function (Request $request, Response $response) { $response->setMaxAge(60); })->bind('books');
  57. Collection { "count": 3, "total": 3, "_embedded": { "books": [

    … ] }, "_links": { "self": { "href": "/books" } } }
  58. Single Resources $app->get('/books/{book}', function (Application $app, Book $book) { return

    $app['serializer']->serialize( $app['resources.book']($book), $app['format'] ); })->convert('book', $app['converters.book']) ->bind('book'); Resourcify
  59. Single Resource { "id": "1", "title": "American Gods", "author": "Neil

    Gaiman", "_links": { "self": { "href": "/books/1" } } }
  60. Image Attributions Sleeping Dog - Eugene0126jp - CC Share-Alike LEGO

    Color Bricks - Alan Chia - CC Share-Alike Let’s Build a Snowman - © Cannibal Films, Ltd. HAL 9000 - 2001 Wikia - CC Share-Alike Child Reading - Tim Pierce - CC Share-Alike