Rest in practice con Symfony2

3,849 views

Published on

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

Published in: Technology
1 Comment
5 Likes
Statistics
Notes
No Downloads
Views
Total views
3,849
On SlideShare
0
From Embeds
0
Number of Embeds
27
Actions
Shares
0
Downloads
68
Comments
1
Likes
5
Embeds 0
No embeds

No notes for slide

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

×