Rest in practice con Symfony2
Upcoming SlideShare
Loading in...5
×
 

Rest in practice con Symfony2

on

  • 1,810 views

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

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

Statistics

Views

Total Views
1,810
Views on SlideShare
1,802
Embed Views
8

Actions

Likes
4
Downloads
27
Comments
1

4 Embeds 8

http://www.linkedin.com 3
https://www.linkedin.com 3
https://twitter.com 1
https://it.linkedin.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

CC Attribution License

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Rest in practice con Symfony2 Rest in practice con Symfony2 Presentation Transcript

  • REST in pratica con Symfony2
  • @dlondero
  • DI SOLITO...
  • Richardson Maturity Model
  • NON PARLIAMO DI...
  • PARLIAMO DI COME FARE
  • COSA CI SERVE •symfony/framework-standard-edition •friendsofsymfony/rest-bundle •jms/serializer-bundle •nelmio/api-doc-bundle
  • //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;
  • CRUD
  • Create HTTP POST
  • Request POST /products HTTP/1.1 Host: acme.com Content-Type: application/json { "name": "Product #1", "price": 19.90, "description": "Awesome product" }
  • 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" }
  • //src/Acme/ApiBundle/Resources/config/routing.yml acme_api_product_post: pattern: /products defaults: { _controller: AcmeApiBundle:ApiProduct:post, _format: json } requirements: _method: POST
  • //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; }
  • Read HTTP GET
  • Request GET /products/1 HTTP/1.1 Host: acme.com
  • Response HTTP/1.1 200 OK Content-Type: application/json { "product": { "id": 1, "name": "Product #1", "price": 19.9, "description": "Awesome product" }
  • public function getSingleAction(Product $product) { return array('product' => $product); }
  • Update HTTP PUT
  • Request PUT /products/1 HTTP/1.1 Host: acme.com Content-Type: application/json { "name": "Product #1", "price": 29.90, "description": "Awesome product" }
  • 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" }
  • //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(); }
  • Partial Update HTTP PATCH
  • Request PATCH /products/1 HTTP/1.1 Host: acme.com Content-Type: application/json { "price": 39.90, }
  • 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" }
  • //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(); }
  • Delete HTTP DELETE
  • Request DELETE /products/1 HTTP/1.1 Host: acme.com
  • Response HTTP/1.1 204 No Content
  • //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(); }
  • Serialization
  • 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;
  • Deserialization
  • //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; }
  • Testing
  • //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') ); }
  • //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()); }
  • //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); }
  • //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(); } }
  • //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 ); }
  • //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()); }
  • //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 ); }
  • //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()); }
  • //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()); }
  • Documentation
  • //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); }
  • Hypermedia?
  • There’s a bundle for that™
  • willdurand/hateoas-bundle
  • fsc/hateoas-bundle
  • //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 { ... }
  • application/hal+json
  • 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 }
  • “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
  • “Tuttavia, essendo pragmatici, a volte anche un livello 2 ben fatto è garanzia di una buona API...” Daniel Londero
  • “But don’t call it RESTful. Period.” Roy Fielding
  • “Ok.” Daniel Londero
  • GRAZIE
  • @dlondero