Rest in practice con Symfony2
-
Upload
daniel-londero -
Category
Technology
-
view
2.460 -
download
1
description
Transcript of Rest in practice con Symfony2
REST in praticacon Symfony2
@dlondero
DI SOLITO...
Richardson Maturity Model
NONPARLIAMO 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 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;
CRUD
CreateHTTP POST
POST /products HTTP/1.1Host: acme.comContent-Type: application/json
{ "name": "Product #1", "price": 19.90, "description": "Awesome product"}
Request
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
//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 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;}
ReadHTTP GET
GET /products/1 HTTP/1.1Host: acme.com
Request
HTTP/1.1 200 OKContent-Type: application/json
{ "product": { "id": 1, "name": "Product #1", "price": 19.9, "description": "Awesome product"}
Response
public function getSingleAction(Product $product){ return array('product' => $product);}
UpdateHTTP PUT
PUT /products/1 HTTP/1.1Host: acme.comContent-Type: application/json
{ "name": "Product #1", "price": 29.90, "description": "Awesome product"}
Request
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
//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();}
Partial UpdateHTTP PATCH
PATCH /products/1 HTTP/1.1Host: acme.comContent-Type: application/json
{ "price": 39.90,}
Request
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
//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();}
DeleteHTTP DELETE
DELETE /products/1 HTTP/1.1Host: acme.com
Request
HTTP/1.1 204 No Content
Response
//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();}
Serialization
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;
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 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') ); }
//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( '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);}
//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(); }}
//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 );}
//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());}
//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( '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());}
//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());}
Documentation
//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);}
Hypermedia?
There’s a bundle for that™
willdurand/hateoas-bundle
fsc/hateoas-bundle
//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{ ...}
application/hal+json
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}
“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