REST in practice with Symfony2

  • 2,138 views
Uploaded on

Translated version of slides used for my talk about creating RESTful APIs with Symfony2 at Italian SymfonyDay (Rome, October 18th 2013)

Translated version of slides used for my talk about creating RESTful APIs with Symfony2 at Italian SymfonyDay (Rome, October 18th 2013)

More in: Technology , Design
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
2,138
On Slideshare
0
From Embeds
0
Number of Embeds
4

Actions

Shares
Downloads
48
Comments
0
Likes
11

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. REST in practice with Symfony2
  • 2. @dlondero
  • 3. OFTEN...
  • 4. Richardson Maturity Model
  • 5. NOT TALKING ABOUT...
  • 6. Level 0 POX - RPC
  • 7. Level 1 RESOURCES
  • 8. Level 2 HTTP VERBS
  • 9. Level 3 HYPERMEDIA
  • 10. TALKING ABOUT HOW TO DO
  • 11. WHAT WE NEED ! •symfony/framework-standard-edition ! •friendsofsymfony/rest-bundle ! •jms/serializer-bundle ! •nelmio/api-doc-bundle
  • 12. //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;!
  • 13. CRUD
  • 14. Create HTTP POST
  • 15. Request POST /products HTTP/1.1! Host: acme.com! Content-Type: application/json! ! {! "name": "Product #1",! "price": 19.90,! "description": "Awesome product"! }!
  • 16. 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"! }!
  • 17. //src/Acme/ApiBundle/Resources/config/routing.yml! ! acme_api_product_post:! pattern: /products! defaults: { _controller: AcmeApiBundle:ApiProduct:post, _format: json }! requirements:! _method: POST
  • 18. //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;! }
  • 19. Read HTTP GET
  • 20. Request GET /products/1 HTTP/1.1! Host: acme.com
  • 21. Response HTTP/1.1 200 OK! Content-Type: application/json! ! {! "product": {! "id": 1,! "name": "Product #1",! "price": 19.9,! "description": "Awesome product"! }!
  • 22. public function getSingleAction(Product $product)! {! return array('product' => $product);! }
  • 23. Update HTTP PUT
  • 24. Request PUT /products/1 HTTP/1.1! Host: acme.com! Content-Type: application/json! ! {! "name": "Product #1",! "price": 29.90,! "description": "Awesome product"! }!
  • 25. 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"! }!
  • 26. //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();! }
  • 27. Partial Update HTTP PATCH
  • 28. Request PATCH /products/1 HTTP/1.1! Host: acme.com! Content-Type: application/json! ! {! "price": 39.90,! }!
  • 29. 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"! }!
  • 30. //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();! }
  • 31. Delete HTTP DELETE
  • 32. Request DELETE /products/1 HTTP/1.1! Host: acme.com
  • 33. Response HTTP/1.1 204 No Content
  • 34. //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();! }
  • 35. Serialization
  • 36. 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;!
  • 37. Deserialization
  • 38. //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;! }!
  • 39. Testing
  • 40. //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')! );! }!
  • 41. //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());! }!
  • 42. //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);! }
  • 43. //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();! }! }!
  • 44. //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! );! }
  • 45. //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());! }
  • 46. //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! );! }
  • 47. //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());! }
  • 48. //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());! }
  • 49. Documentation
  • 50. //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);! }!
  • 51. Hypermedia?
  • 52. There’s a bundle for that™
  • 53. willdurand/hateoas-bundle
  • 54. fsc/hateoas-bundle
  • 55. //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! {! ...! }
  • 56. application/hal+json
  • 57. 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! }
  • 58. “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
  • 59. “Anyway, being pragmatic, sometimes a level 2 well done guarantees a good API…” Daniel Londero
  • 60. “But don’t call it RESTful. Period.” Roy Fielding
  • 61. “Ok.” Daniel Londero
  • 62. THANKS
  • 63. @dlondero