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.

Avatar for Samantha Quiñones

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