Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Rest in practice con Symfony2

4,021 views

Published on

Slide del talk presentato al SymfonyDay 2013 di Roma (18/10/2013)

Published in: Technology

Rest in practice con Symfony2

  1. 1. REST in pratica con Symfony2
  2. 2. @dlondero
  3. 3. DI SOLITO...
  4. 4. Richardson Maturity Model
  5. 5. NON PARLIAMO DI...
  6. 6. PARLIAMO DI COME FARE
  7. 7. COSA CI SERVE •symfony/framework-standard-edition •friendsofsymfony/rest-bundle •jms/serializer-bundle •nelmio/api-doc-bundle
  8. 8. //src/Acme/ApiBundle/Entity/Product.php; use SymfonyComponentValidatorConstraints as Assert; use DoctrineORMMapping as ORM; /** * @ORMEntity * @ORMTable(name="product") */ class Product { /** * @ORMColumn(type="integer") * @ORMId * @ORMGeneratedValue(strategy="AUTO") */ protected $id; /** * @ORMColumn(type="string", length=100) * @AssertNotBlank() */ protected $name; /** * @ORMColumn(type="decimal", scale=2) */ protected $price; /** * @ORMColumn(type="text") */ protected $description;
  9. 9. CRUD
  10. 10. Create HTTP POST
  11. 11. Request POST /products HTTP/1.1 Host: acme.com Content-Type: application/json { "name": "Product #1", "price": 19.90, "description": "Awesome product" }
  12. 12. Response HTTP/1.1 201 Created Location: http://acme.com/products/1 Content-Type: application/json { "product": { "id": 1, "name": "Product #1", "price": 19.9, "description": "Awesome product" }
  13. 13. //src/Acme/ApiBundle/Resources/config/routing.yml acme_api_product_post: pattern: /products defaults: { _controller: AcmeApiBundle:ApiProduct:post, _format: json } requirements: _method: POST
  14. 14. //src/Acme/ApiBundle/Controller/ApiProductController.php use FOSRestBundleViewView; public function postAction(Request $request) { $product = $this->deserialize( 'AcmeApiBundleEntityProduct', $request ); if ($product instanceof Product === false) { return View::create(array('errors' => $product), 400); } $em = $this->getEM(); $em->persist($product); $em->flush(); $url = $this->generateUrl( 'acme_api_product_get_single', array('id' => $product->getId()), true ); $response = new Response(); $response->setStatusCode(201); $response->headers->set('Location', $url); return $response; }
  15. 15. Read HTTP GET
  16. 16. Request GET /products/1 HTTP/1.1 Host: acme.com
  17. 17. Response HTTP/1.1 200 OK Content-Type: application/json { "product": { "id": 1, "name": "Product #1", "price": 19.9, "description": "Awesome product" }
  18. 18. public function getSingleAction(Product $product) { return array('product' => $product); }
  19. 19. Update HTTP PUT
  20. 20. Request PUT /products/1 HTTP/1.1 Host: acme.com Content-Type: application/json { "name": "Product #1", "price": 29.90, "description": "Awesome product" }
  21. 21. Response HTTP/1.1 204 No Content HTTP/1.1 200 OK Content-Type: application/json { "product": { "id": 1, "name": "Product #1", "price": 29.90, "description": "Awesome product" }
  22. 22. //src/Acme/ApiBundle/Controller/ApiProductController.php use FOSRestBundleControllerAnnotationsView as RestView; /** * @RestView(statusCode=204) */ public function putAction(Product $product, Request $request) { $newProduct = $this->deserialize( 'AcmeApiBundleEntityProduct', $request ); if ($newProduct instanceof Product === false) { return View::create(array('errors' => $newProduct), 400); } $product->merge($newProduct); $this->getEM()->flush(); }
  23. 23. Partial Update HTTP PATCH
  24. 24. Request PATCH /products/1 HTTP/1.1 Host: acme.com Content-Type: application/json { "price": 39.90, }
  25. 25. Response HTTP/1.1 204 No Content HTTP/1.1 200 OK Content-Type: application/json { "product": { "id": 1, "name": "Product #1", "price": 39.90, "description": "Awesome product" }
  26. 26. //src/Acme/ApiBundle/Controller/ApiProductController.php use FOSRestBundleControllerAnnotationsView as RestView; /** * @RestView(statusCode=204) */ public function patchAction(Product $product, Request $request) { $validator = $this->get('validator'); $raw = json_decode($request->getContent(), true); $product->patch($raw); if (count($errors = $validator->validate($product))) { return $errors; } $this->getEM()->flush(); }
  27. 27. Delete HTTP DELETE
  28. 28. Request DELETE /products/1 HTTP/1.1 Host: acme.com
  29. 29. Response HTTP/1.1 204 No Content
  30. 30. //src/Acme/ApiBundle/Controller/ApiProductController.php use FOSRestBundleControllerAnnotationsView as RestView; /** * @RestView(statusCode=204) */ public function deleteAction(Product $product) { $em = $this->getEM(); $em->remove($product); $em->flush(); }
  31. 31. Serialization
  32. 32. use JMSSerializerAnnotation as Serializer; /** * @SerializerExclusionPolicy("all") */ class Product { /** * @SerializerExpose * @SerializerType("integer") */ protected $id; /** * @SerializerExpose * @SerializerType("string") */ protected $name; /** * @SerializerExpose * @SerializerType("double") */ protected $price; /** * @SerializerExpose * @SerializerType("string") */ protected $description;
  33. 33. Deserialization
  34. 34. //src/Acme/ApiBundle/Controller/ApiController.php protected function deserialize($class, Request $request, $format = 'json') { $serializer = $this->get('serializer'); $validator = $this->get('validator'); try { $entity = $serializer->deserialize( $request->getContent(), $class, $format ); } catch (RuntimeException $e) { throw new HttpException(400, $e->getMessage()); } if (count($errors = $validator->validate($entity))) { return $errors; } return $entity; }
  35. 35. Testing
  36. 36. //src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php use LiipFunctionalTestBundleTestWebTestCase; class ApiProductControllerTest extends WebTestCase { public function testPost() { $this->loadFixtures(array()); $product = array( 'name' => 'Product #1', 'price' => 19.90, 'description' => 'Awesome product', ); $client = static::createClient(); $client->request( 'POST', '/products', array(), array(), array(), json_encode($product) ); $this->assertEquals(201, $client->getResponse()->getStatusCode()); $this->assertTrue($client->getResponse()->headers->has('Location')); $this->assertContains( "/products/1", $client->getResponse()->headers->get('Location') ); }
  37. 37. //src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php public function testPostValidation() { $this->loadFixtures(array()); $product = array( 'name' => '', 'price' => 19.90, 'description' => 'Awesome product', ); $client = static::createClient(); $client->request( 'POST', '/products', array(), array(), array(), json_encode($product) ); $this->assertEquals(400, $client->getResponse()->getStatusCode()); }
  38. 38. //src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php public function testGetAction() { $this->loadFixtures(array( 'AcmeApiBundleTestsFixturesProduct', )); $client = static::createClient(); $client->request('GET', '/products'); $this->isSuccessful($client->getResponse()); $response = json_decode($client->getResponse()->getContent()); $this->assertTrue(isset($response->products)); $this->assertCount(1, $response->products); $product = $response->products[0]; $this->assertSame('Product #1', $product->name); $this->assertSame(19.90, $product->price); $this->assertSame('Awesome product!', $product->description); }
  39. 39. //src/Acme/ApiBundle/Tests/Fixtures/Product.php use AcmeApiBundleEntityProduct as ProductEntity; use DoctrineCommonPersistenceObjectManager; use DoctrineCommonDataFixturesFixtureInterface; class Product implements FixtureInterface { public function load(ObjectManager $em) { $product = new ProductEntity(); $product->setName('Product #1'); $product->setPrice(19.90); $product->setDescription('Awesome product!'); $em->persist($product); $em->flush(); } }
  40. 40. //src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php public function testGetSingleAction() { $this->loadFixtures(array( 'AcmeApiBundleTestsFixturesProduct', )); $client = static::createClient(); $client->request('GET', '/products/1'); $this->isSuccessful($client->getResponse()); $response = json_decode($client->getResponse()->getContent()); $this->assertTrue(isset($response->product)); $this->assertEquals(1, $response->product->id); $this->assertSame('Product #1', $response->product->name); $this->assertSame(19.90, $response->product->price); $this->assertSame( 'Awesome product!', $response->product->description ); }
  41. 41. //src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php public function testPutAction() { $this->loadFixtures(array( 'AcmeApiBundleTestsFixturesProduct', )); $product = array( 'name' => 'New name', 'price' => 39.90, 'description' => 'Awesome new description' ); $client = static::createClient(); $client->request( 'PUT', '/products/1', array(), array(), array(), json_encode($product) ); $this->isSuccessful($client->getResponse()); $this->assertEquals(204, $client->getResponse()->getStatusCode()); }
  42. 42. //src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php /** * @depends testPutAction */ public function testPutActionWithVerification() { $client = static::createClient(); $client->request('GET', '/products/1'); $this->isSuccessful($client->getResponse()); $response = json_decode($client->getResponse()->getContent()); $this->assertTrue(isset($response->product)); $this->assertEquals(1, $response->product->id); $this->assertSame('New name', $response->product->name); $this->assertSame(39.90, $response->product->price); $this->assertSame( 'Awesome new description', $response->product->description ); }
  43. 43. //src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php public function testPatchAction() { $this->loadFixtures(array( 'AcmeApiBundleTestsFixturesProduct', )); $patch = array( 'price' => 29.90 ); $client = static::createClient(); $client->request( 'PATCH', '/products/1', array(), array(), array(), json_encode($patch) ); $this->isSuccessful($client->getResponse()); $this->assertEquals(204, $client->getResponse()->getStatusCode()); }
  44. 44. //src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php public function testDeleteAction() { $this->loadFixtures(array( 'AcmeApiBundleTestsFixturesProduct', )); $client = static::createClient(); $client->request('DELETE', '/products/1'); $this->assertEquals(204, $client->getResponse()->getStatusCode()); }
  45. 45. Documentation
  46. 46. //src/Acme/ApiBundle/Controller/ApiProductController.php use NelmioApiDocBundleAnnotationApiDoc; /** * Returns representation of a given product * * **Response Format** * * { * "product": { * "id": 1, * "name": "Product #1", * "price": 19.9, * "description": "Awesome product" * } * } * * @ApiDoc( * section="Products", * statusCodes={ * 200="OK", * 404="Not Found" * } * ) */ public function getSingleAction(Product $product) { return array('product' => $product); }
  47. 47. Hypermedia?
  48. 48. There’s a bundle for that™
  49. 49. willdurand/hateoas-bundle
  50. 50. fsc/hateoas-bundle
  51. 51. //src/Acme/ApiBundle/Entity/Product.php; use JMSSerializerAnnotation as Serializer; use FSCHateoasBundleAnnotation as Rest; use DoctrineORMMapping as ORM; /** * @ORMEntity * @ORMTable(name="product") * @SerializerExclusionPolicy("all") * @RestRelation( * "self", * href = @RestRoute("acme_api_product_get_single", * parameters = { "id" = ".id" }) * ) * @RestRelation( * "products", * href = @RestRoute("acme_api_product_get") * ) */ class Product { ... }
  52. 52. application/hal+json
  53. 53. GET /orders/523 HTTP/1.1 Host: example.org Accept: application/hal+json HTTP/1.1 200 OK Content-Type: application/hal+json { "_links": { "self": { "href": "/orders/523" }, "invoice": { "href": "/invoices/873" } }, "currency": "USD", "total": 10.20 }
  54. 54. “What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?” Roy Fielding
  55. 55. “Tuttavia, essendo pragmatici, a volte anche un livello 2 ben fatto è garanzia di una buona API...” Daniel Londero
  56. 56. “But don’t call it RESTful. Period.” Roy Fielding
  57. 57. “Ok.” Daniel Londero
  58. 58. GRAZIE
  59. 59. @dlondero

×