rest in practice with symfony2
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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/1.jpg)
REST in practice with Symfony2
![Page 2: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/2.jpg)
@dlondero
![Page 3: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/3.jpg)
![Page 4: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/4.jpg)
![Page 5: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/5.jpg)
![Page 6: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/6.jpg)
OFTEN...
![Page 7: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/7.jpg)
Richardson Maturity Model
![Page 8: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/8.jpg)
NOT TALKING ABOUT...
![Page 9: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/9.jpg)
Level 0
POX - RPC
![Page 10: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/10.jpg)
Level 1
RESOURCES
![Page 11: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/11.jpg)
Level 2
HTTP VERBS
![Page 12: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/12.jpg)
Level 3
HYPERMEDIA
![Page 13: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/13.jpg)
TALKING ABOUT HOW
TO DO
![Page 14: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/14.jpg)
WHAT WE NEED !
•symfony/framework-standard-edition !
•friendsofsymfony/rest-bundle !
•jms/serializer-bundle !
•nelmio/api-doc-bundle
![Page 15: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/15.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/16.jpg)
CRUD
![Page 17: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/17.jpg)
Create HTTP POST
![Page 18: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/18.jpg)
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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/19.jpg)
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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/20.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/21.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/22.jpg)
Read HTTP GET
![Page 23: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/23.jpg)
GET /products/1 HTTP/1.1!Host: acme.com
Request
![Page 24: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/24.jpg)
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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/25.jpg)
public function getSingleAction(Product $product)!{! return array('product' => $product);!}
![Page 26: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/26.jpg)
Update HTTP PUT
![Page 27: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/27.jpg)
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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/28.jpg)
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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/29.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/30.jpg)
Partial Update HTTP PATCH
![Page 31: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/31.jpg)
PATCH /products/1 HTTP/1.1!Host: acme.com!Content-Type: application/json!!{! "price": 39.90,!}!
Request
![Page 32: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/32.jpg)
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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/33.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/34.jpg)
Delete HTTP DELETE
![Page 35: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/35.jpg)
DELETE /products/1 HTTP/1.1!Host: acme.com
Request
![Page 36: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/36.jpg)
HTTP/1.1 204 No Content
Response
![Page 37: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/37.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/38.jpg)
Serialization
![Page 39: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/39.jpg)
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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/40.jpg)
Deserialization
![Page 41: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/41.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/42.jpg)
Testing
![Page 43: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/43.jpg)
![Page 44: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/44.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/45.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/46.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/47.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/48.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/49.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/50.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/51.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/52.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/53.jpg)
Documentation
![Page 54: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/54.jpg)
![Page 55: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/55.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/56.jpg)
![Page 57: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/57.jpg)
![Page 58: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/58.jpg)
Hypermedia?
![Page 59: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/59.jpg)
There’s a bundle for that™
![Page 60: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/60.jpg)
willdurand/hateoas-bundle
![Page 61: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/61.jpg)
fsc/hateoas-bundle
![Page 62: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/62.jpg)
//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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/63.jpg)
![Page 64: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/64.jpg)
application/hal+json
![Page 65: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/65.jpg)
![Page 66: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/66.jpg)
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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/67.jpg)
“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](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/68.jpg)
“Anyway, being pragmatic, sometimes a level 2 well done guarantees a good API…”
Daniel Londero
![Page 69: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/69.jpg)
“But don’t call it RESTful. Period.”
Roy Fielding
![Page 70: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/70.jpg)
“Ok.”
Daniel Londero
![Page 71: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/71.jpg)
THANKS
![Page 72: REST in practice with Symfony2](https://reader031.vdocuments.us/reader031/viewer/2022020115/554f3ff4b4c90572088b5257/html5/thumbnails/72.jpg)
@dlondero