rest in practice with symfony2

72
REST in practice with Symfony2

Upload: daniel-londero

Post on 10-May-2015

3.034 views

Category:

Technology


2 download

DESCRIPTION

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

TRANSCRIPT

Page 1: REST in practice with Symfony2

REST in practice with Symfony2

Page 2: REST in practice with Symfony2

@dlondero

Page 3: REST in practice with Symfony2
Page 4: REST in practice with Symfony2
Page 5: REST in practice with Symfony2
Page 6: REST in practice with Symfony2

OFTEN...

Page 7: REST in practice with Symfony2

Richardson Maturity Model

Page 8: REST in practice with Symfony2

NOT TALKING ABOUT...

Page 9: REST in practice with Symfony2

Level 0

POX - RPC

Page 10: REST in practice with Symfony2

Level 1

RESOURCES

Page 11: REST in practice with Symfony2

Level 2

HTTP VERBS

Page 12: REST in practice with Symfony2

Level 3

HYPERMEDIA

Page 13: REST in practice with Symfony2

TALKING ABOUT HOW

TO DO

Page 14: REST in practice with Symfony2

WHAT WE NEED !

•symfony/framework-standard-edition !

•friendsofsymfony/rest-bundle !

•jms/serializer-bundle !

•nelmio/api-doc-bundle

Page 15: REST in practice with Symfony2

//src/Acme/ApiBundle/Entity/Product.php;!!use Symfony\Component\Validator\Constraints as Assert;!use Doctrine\ORM\Mapping as ORM;!!/**! * @ORM\Entity! * @ORM\Table(name="product")! */!class Product!{! /**! * @ORM\Column(type="integer")! * @ORM\Id! * @ORM\GeneratedValue(strategy="AUTO")! */! protected $id;!! /**! * @ORM\Column(type="string", length=100)! * @Assert\NotBlank()! */! protected $name;!! /**! * @ORM\Column(type="decimal", scale=2)! */! protected $price;!! /**! * @ORM\Column(type="text")! */! protected $description;!

Page 16: REST in practice with Symfony2

CRUD

Page 17: REST in practice with Symfony2

Create HTTP POST

Page 18: REST in practice with Symfony2

POST /products HTTP/1.1!Host: acme.com!Content-Type: application/json!!{! "name": "Product #1",! "price": 19.90,! "description": "Awesome product"!}!

Request

Page 19: REST in practice with Symfony2

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"!}!

Response

Page 20: REST in practice with Symfony2

//src/Acme/ApiBundle/Resources/config/routing.yml!!acme_api_product_post:! pattern: /products! defaults: { _controller: AcmeApiBundle:ApiProduct:post, _format: json }! requirements:! _method: POST

Page 21: REST in practice with Symfony2

//src/Acme/ApiBundle/Controller/ApiProductController.php!!use FOS\RestBundle\View\View;!!public function postAction(Request $request)!{! $product = $this->deserialize(! 'Acme\ApiBundle\Entity\Product',! $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;!}

Page 22: REST in practice with Symfony2

Read HTTP GET

Page 23: REST in practice with Symfony2

GET /products/1 HTTP/1.1!Host: acme.com

Request

Page 24: REST in practice with Symfony2

HTTP/1.1 200 OK!Content-Type: application/json!!{! "product": {! "id": 1,! "name": "Product #1",! "price": 19.9,! "description": "Awesome product"!}!

Response

Page 25: REST in practice with Symfony2

public function getSingleAction(Product $product)!{! return array('product' => $product);!}

Page 26: REST in practice with Symfony2

Update HTTP PUT

Page 27: REST in practice with Symfony2

PUT /products/1 HTTP/1.1!Host: acme.com!Content-Type: application/json!!{! "name": "Product #1",! "price": 29.90,! "description": "Awesome product"!}!

Request

Page 28: REST in practice with Symfony2

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"!}!

Response

Page 29: REST in practice with Symfony2

//src/Acme/ApiBundle/Controller/ApiProductController.php!!use FOS\RestBundle\Controller\Annotations\View as RestView;!!/**! * @RestView(statusCode=204)! */!public function putAction(Product $product, Request $request)!{! $newProduct = $this->deserialize(! 'Acme\ApiBundle\Entity\Product',! $request! );!! if ($newProduct instanceof Product === false) {! return View::create(array('errors' => $newProduct), 400);! }!! $product->merge($newProduct);!! $this->getEM()->flush();!}

Page 30: REST in practice with Symfony2

Partial Update HTTP PATCH

Page 31: REST in practice with Symfony2

PATCH /products/1 HTTP/1.1!Host: acme.com!Content-Type: application/json!!{! "price": 39.90,!}!

Request

Page 32: REST in practice with Symfony2

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"!}!

Response

Page 33: REST in practice with Symfony2

//src/Acme/ApiBundle/Controller/ApiProductController.php!!use FOS\RestBundle\Controller\Annotations\View 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();!}

Page 34: REST in practice with Symfony2

Delete HTTP DELETE

Page 35: REST in practice with Symfony2

DELETE /products/1 HTTP/1.1!Host: acme.com

Request

Page 36: REST in practice with Symfony2

HTTP/1.1 204 No Content

Response

Page 37: REST in practice with Symfony2

//src/Acme/ApiBundle/Controller/ApiProductController.php!!use FOS\RestBundle\Controller\Annotations\View as RestView;!!/**! * @RestView(statusCode=204)! */!public function deleteAction(Product $product)!{! $em = $this->getEM();! $em->remove($product);! $em->flush();!}

Page 38: REST in practice with Symfony2

Serialization

Page 39: REST in practice with Symfony2

use JMS\Serializer\Annotation as Serializer;!!/**! * @Serializer\ExclusionPolicy("all")! */!class Product!{! /**! * @Serializer\Expose! * @Serializer\Type("integer")! */! protected $id;!! /**! * @Serializer\Expose! * @Serializer\Type("string")! */! protected $name;!! /**! * @Serializer\Expose! * @Serializer\Type("double")! */! protected $price;!! /**! * @Serializer\Expose! * @Serializer\Type("string")! */! protected $description;!

Page 40: REST in practice with Symfony2

Deserialization

Page 41: REST in practice with Symfony2

//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;!}!

Page 42: REST in practice with Symfony2

Testing

Page 43: REST in practice with Symfony2
Page 44: REST in practice with Symfony2

//src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php!!use Liip\FunctionalTestBundle\Test\WebTestCase;!!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')! );! }!

Page 45: REST in practice with Symfony2

//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());!}!

Page 46: REST in practice with Symfony2

//src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php!!public function testGetAction()!{! $this->loadFixtures(array(! 'Acme\ApiBundle\Tests\Fixtures\Product',! ));!! $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);!}

Page 47: REST in practice with Symfony2

//src/Acme/ApiBundle/Tests/Fixtures/Product.php!!use Acme\ApiBundle\Entity\Product as ProductEntity;!!use Doctrine\Common\Persistence\ObjectManager;!use Doctrine\Common\DataFixtures\FixtureInterface;!!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();! }!}!

Page 48: REST in practice with Symfony2

//src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php!!public function testGetSingleAction()!{! $this->loadFixtures(array(! 'Acme\ApiBundle\Tests\Fixtures\Product',! ));!! $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! );!}

Page 49: REST in practice with Symfony2

//src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php!!public function testPutAction()!{! $this->loadFixtures(array(! 'Acme\ApiBundle\Tests\Fixtures\Product',! ));!! $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());!}

Page 50: REST in practice with Symfony2

//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! );!}

Page 51: REST in practice with Symfony2

//src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php!!public function testPatchAction()!{! $this->loadFixtures(array(! 'Acme\ApiBundle\Tests\Fixtures\Product',! ));!! $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());!}

Page 52: REST in practice with Symfony2

//src/Acme/ApiBundle/Tests/Controller/ApiProductControllerTest.php!!public function testDeleteAction()!{! $this->loadFixtures(array(! 'Acme\ApiBundle\Tests\Fixtures\Product',! ));!! $client = static::createClient();! $client->request('DELETE', '/products/1');! $this->assertEquals(204, $client->getResponse()->getStatusCode());!}

Page 53: REST in practice with Symfony2

Documentation

Page 54: REST in practice with Symfony2
Page 55: REST in practice with Symfony2

//src/Acme/ApiBundle/Controller/ApiProductController.php!!use Nelmio\ApiDocBundle\Annotation\ApiDoc;!!/**! * 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);!}!

Page 56: REST in practice with Symfony2
Page 57: REST in practice with Symfony2
Page 58: REST in practice with Symfony2

Hypermedia?

Page 59: REST in practice with Symfony2

There’s a bundle for that™

Page 60: REST in practice with Symfony2

willdurand/hateoas-bundle

Page 61: REST in practice with Symfony2

fsc/hateoas-bundle

Page 62: REST in practice with Symfony2

//src/Acme/ApiBundle/Entity/Product.php;!!use JMS\Serializer\Annotation as Serializer;!use FSC\HateoasBundle\Annotation as Rest;!use Doctrine\ORM\Mapping as ORM;!!/**! * @ORM\Entity! * @ORM\Table(name="product")! * @Serializer\ExclusionPolicy("all")! * @Rest\Relation(! * "self", ! * href = @Rest\Route("acme_api_product_get_single", ! * parameters = { "id" = ".id" })! * )! * @Rest\Relation(! * "products", ! * href = @Rest\Route("acme_api_product_get")! * )! */!class Product!{! ...!}

Page 63: REST in practice with Symfony2
Page 64: REST in practice with Symfony2

application/hal+json

Page 65: REST in practice with Symfony2
Page 66: REST in practice with Symfony2

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!}

Page 67: REST in practice with Symfony2

“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

Page 68: REST in practice with Symfony2

“Anyway, being pragmatic, sometimes a level 2 well done guarantees a good API…”

Daniel Londero

Page 69: REST in practice with Symfony2

“But don’t call it RESTful. Period.”

Roy Fielding

Page 70: REST in practice with Symfony2

“Ok.”

Daniel Londero

Page 71: REST in practice with Symfony2

THANKS

Page 72: REST in practice with Symfony2

@dlondero