Rest in practice con Symfony2

69
REST in pratica con Symfony2

description

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

Transcript of Rest in practice con Symfony2

Page 1: Rest in practice con Symfony2

REST in praticacon Symfony2

Page 2: Rest in practice con Symfony2

@dlondero

Page 3: Rest in practice con Symfony2
Page 4: Rest in practice con Symfony2
Page 5: Rest in practice con Symfony2
Page 6: Rest in practice con Symfony2
Page 7: Rest in practice con Symfony2

DI SOLITO...

Page 8: Rest in practice con Symfony2

Richardson Maturity Model

Page 9: Rest in practice con Symfony2

NONPARLIAMO DI...

Page 10: Rest in practice con Symfony2

PARLIAMO DI COME FARE

Page 11: Rest in practice con Symfony2

COSA CI SERVE

•symfony/framework-standard-edition

•friendsofsymfony/rest-bundle

•jms/serializer-bundle

•nelmio/api-doc-bundle

Page 12: Rest in practice con 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 13: Rest in practice con Symfony2

CRUD

Page 14: Rest in practice con Symfony2

CreateHTTP POST

Page 15: Rest in practice con Symfony2

POST /products HTTP/1.1Host: acme.comContent-Type: application/json

{ "name": "Product #1", "price": 19.90, "description": "Awesome product"}

Request

Page 16: Rest in practice con Symfony2

HTTP/1.1 201 CreatedLocation: http://acme.com/products/1Content-Type: application/json

{ "product": { "id": 1, "name": "Product #1", "price": 19.9, "description": "Awesome product"}

Response

Page 17: Rest in practice con 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 18: Rest in practice con 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 19: Rest in practice con Symfony2

ReadHTTP GET

Page 20: Rest in practice con Symfony2

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

Request

Page 21: Rest in practice con Symfony2

HTTP/1.1 200 OKContent-Type: application/json

{ "product": { "id": 1, "name": "Product #1", "price": 19.9, "description": "Awesome product"}

Response

Page 22: Rest in practice con Symfony2

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

Page 23: Rest in practice con Symfony2

UpdateHTTP PUT

Page 24: Rest in practice con Symfony2

PUT /products/1 HTTP/1.1Host: acme.comContent-Type: application/json

{ "name": "Product #1", "price": 29.90, "description": "Awesome product"}

Request

Page 25: Rest in practice con Symfony2

HTTP/1.1 204 No Content

HTTP/1.1 200 OKContent-Type: application/json

{ "product": { "id": 1, "name": "Product #1", "price": 29.90, "description": "Awesome product"}

Response

Page 26: Rest in practice con 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 27: Rest in practice con Symfony2

Partial UpdateHTTP PATCH

Page 28: Rest in practice con Symfony2

PATCH /products/1 HTTP/1.1Host: acme.comContent-Type: application/json

{ "price": 39.90,}

Request

Page 29: Rest in practice con Symfony2

HTTP/1.1 204 No Content

HTTP/1.1 200 OKContent-Type: application/json

{ "product": { "id": 1, "name": "Product #1", "price": 39.90, "description": "Awesome product"}

Response

Page 30: Rest in practice con 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 31: Rest in practice con Symfony2

DeleteHTTP DELETE

Page 32: Rest in practice con Symfony2

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

Request

Page 33: Rest in practice con Symfony2

HTTP/1.1 204 No Content

Response

Page 34: Rest in practice con 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 35: Rest in practice con Symfony2

Serialization

Page 36: Rest in practice con 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 37: Rest in practice con Symfony2

Deserialization

Page 38: Rest in practice con 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 39: Rest in practice con Symfony2

Testing

Page 40: Rest in practice con Symfony2
Page 41: Rest in practice con 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 42: Rest in practice con 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 43: Rest in practice con 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 44: Rest in practice con 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 45: Rest in practice con 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 46: Rest in practice con 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 47: Rest in practice con 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 48: Rest in practice con 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 49: Rest in practice con 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 50: Rest in practice con Symfony2

Documentation

Page 51: Rest in practice con Symfony2
Page 52: Rest in practice con 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 53: Rest in practice con Symfony2
Page 54: Rest in practice con Symfony2
Page 55: Rest in practice con Symfony2

Hypermedia?

Page 56: Rest in practice con Symfony2

There’s a bundle for that™

Page 57: Rest in practice con Symfony2

willdurand/hateoas-bundle

Page 58: Rest in practice con Symfony2

fsc/hateoas-bundle

Page 59: Rest in practice con 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 60: Rest in practice con Symfony2
Page 61: Rest in practice con Symfony2

application/hal+json

Page 62: Rest in practice con Symfony2
Page 63: Rest in practice con Symfony2

GET /orders/523 HTTP/1.1Host: example.orgAccept: application/hal+json

HTTP/1.1 200 OKContent-Type: application/hal+json

{ "_links": { "self": { "href": "/orders/523" }, "invoice": { "href": "/invoices/873" } }, "currency": "USD", "total": 10.20}

Page 64: Rest in practice con 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 65: Rest in practice con Symfony2

“Tuttavia, essendo pragmatici, a volte anche

un livello 2 ben fatto è garanzia di una buona API...”

Daniel Londero

Page 66: Rest in practice con Symfony2

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

Roy Fielding

Page 67: Rest in practice con Symfony2

“Ok.”

Daniel Londero

Page 68: Rest in practice con Symfony2

GRAZIE

Page 69: Rest in practice con Symfony2

@dlondero