Share
Embed
RESTing with OroCRM
Introduction to REST theory as well as how to use and extend the OroCRM REST APIs.
lsmith
August 11, 2015
Transcript
54 slides
-
Agile Web Development Liip.ch – RESTING WITH Learn how to
use and extend the OroCRM REST API -
AGENDA • Introduction to REST theory • How to discover
OroCRM Rest APIs • How to use OroCRM REST APIs • How to add a new REST APIs to OroCRM -
Everyone who has ever talked about REST, at some point
has said something idiotic about REST. Except for maybe Roy Fielding. -
For example, I once thought it meant converting all GET
parameters to virtual directories. Differentiating GET and POST seemed like overzealous academics. -
I only began to understand why REST really makes sense
and what REST it is really about when I started looking into cache headers and reverse proxies. -
REST is all about leveraging HTTP and constraining your application
to a set of rules, so that users of your API can safely apply assumptions about the behavior of your application. -
HTTP Request Anatomy GET /notes HTTP/1.1 Host: symfony-rest-edition.lo Accept: application/json;q=0.9,*/*;q=0.8
Content-Type: application/json Content-Length: length -
HTTP Response Anatomy HTTP/1.1 200 OK Allow: GET, POST Cache-Control:
max-age=15, public, s-maxage=30 Content-Type: application/json Date: Wed, 15 Jan 2014 15:09:01 GMT Last-Modified: Wed, 15 Jan 2014 14:09:03 GMT Server: Apache/2.2.24 (Unix) DAV/2 PHP/5.4.20 mod_ssl/ 2.2.24 OpenSSL/0.9.8y Vary: Accept-Encoding,Accept-Language {"notes":["a","b","c"]} -
RMM LEVEL 0 • Aka "The Swamp of POX" •
HTTP as a tunneling mechanism • "Procedural" communication (RPC) • Single endpoint (per operation) -
RMM LEVEL 1 • Aka "Resources" • Individual resources, i.e.
URIs • "Object orientated" communication -
RMM LEVEL 2 • Aka "HTTP Verbs" • Client uses
specific HTTP method • Server uses HTTP status codes -
HTTP VERBS Method Safe Idempotent GET yes yes HEAD yes
yes POST no no PUT no yes DELETE no yes (*) .. no no -
SAFE VS. IDEMPOTENT • Safe means cacheable • Idempotent means
result independent of the # of executions, but.. • Does this apply to server state or also to the HTTP response? • Is DELETE idempotent or not, ie. what should be the response for a DELETE requests on a non existent resource? 404 or 200? -
HTTP STATUS CODES Code range Description Example 1xx Information 100
- Continue 2xx Successful 201 - Created 3xx Redirection 301 - Moved Permanently 4xx Client Error 404 - Not Found 5xx Server Error 501 - Not Implemented -
RMM LEVEL 3 • Aka "Hypermedia Control" • Service discovery
via link relations • ATOM, HAL, JSON-LD, IANA Link Rel -
RMM VS REST VS REAL LIFE • Most developers consider
RMM Level 2 sufficient for REST • RMM Level 3 is a precondition but not sufficient for REST • ie. RMM only covers a subset of what REST requires • RMM Level 3 makes URI forma ing ma er less -
RMM VS REST VS REAL LIFE • Browsers are bad
REST clients • REST is protocol independent • Few (no?) clients really leverage HATEOAS -
UNIFIED RESOURCE IDENTIFIER • URIs identify resources • URIs are
format independent • URI "file extensions" != RESTful -
MEDIA TYPES • Identifies a representation format • Custom types
use application/vnd.[XYZ] • Used inside the Accept / Content-Type headers Header Description Content-Type HTTP message format Accept HTTP response format preference -
• Finding appropriate response format • No standardized algorithm available
• Apache mod_negotiation algorithm is documented • Also covers encoding (Accept-Encoding) and language (Accept- Language) negotiation CONTENT TYPE NEGOTIATION -
EXAMPLE Accept: application/json, application/xml;q=0.9, text/ html;q=0.8, text/*;q=0.7, */*;q=0.5 Priority Description
q=1.0 application/json q=0.9 application/xml q=0.8 text/html q=0.7 text/* (ie. any text) q=0.5 */* (ie. any media type) -
OUT OF THE BOX OROCRM REST APIS • $> app/console
router:debug | grep api • API Docs via NelmioApiDocBundle, ie. “/api/doc/“ • Enduser friendly API overview • Includes a sandbox to try out API calls • Information is extracted from Annotations on the Controllers -
USING OROCRM REST APIS • Constructing the URL for: api/rest/{version}/accounts/{id}.{_format}
• Symfony uses a syntax based on RFC 6570 for templated URI • {version} = “v1” (recommended) or “latest” • {id} = ID of an Account instance • {_format} = lazy content type negotiation, ie. “.json”, or use HTTP header “Accept: application/json” -
http://httpie.org/ HTTPie (pronounced aych-tee-tee-pie) is a command line HTTP client.
Its goal is to make CLI interaction with web services as human-friendly as possible. It provides a simple “http” command that allows for sending arbitrary HTTP requests using a simple and natural syntax, and displays colorized output. HTTPie can be used for testing, debugging, and generally interacting with HTTP servers. -
WSSE SECURITY $> http --json http://orocrm.lo/api/rest/v1/accounts HTTP/1.1 401 Unauthorized Cache-Control:
no-cache Content-Type: application/json Date: Wed, 22 Jul 2015 12:09:08 GMT Server: Apache/2.4.10 (Unix) PHP/5.6.9 Set-Cookie: CRMID=i45n7g8phauhrfr2h53fh576q1; path=/; HttpOnly Transfer-Encoding: chunked WWW-Authenticate: WSSE realm="Secured API", profile="UsernameToken" -
GENERATING THE WSSE HEADER $> app/console oro:wsse:generate-header c51ef6cb3e87dcc6f077b93f0ceb778d23669364 To use
WSSE authentication add following headers to the request: Authorization: WSSE profile="UsernameToken" X-WSSE: UsernameToken Username="admin", PasswordDigest="pgkLCLmWcld9xkTUJ4Rxnj+Ww50=", Nonce="MjU3ZWMzYzI1ZTVmYjg5NA==", Created=“2015-07-22T14:12:27+02:00" http://www.orocrm.com/documentation/index/current/cookbook/how-to-use-wsse-authentication -
READING OROCRM REST APIS $> http --json http://orocrm.lo/api/rest/v1/accounts 'X-WSSE: UsernameToken
Username="admin", PasswordDigest="pgkLCLmWcld9xkTUJ4Rxnj+Ww50=", Nonce="MjU3ZWMzYzI1ZTVmYjg5NA==", Created=“2015-07-22T14:12:27+02:00"' [ { "contacts": {}, "createdAt": "2015-07-06T09:12:31+00:00", "defaultContact": "Mr. Jerry Coleman", "id": 1, "name": "Life Plan Counselling", "organization": "Liip Test", -
WRITING TO OROCRM REST APIS $> http POST --json http://orocrm.lo/api/rest/v1/accounts/1
'X-WSSE: UsernameToken Username="admin", PasswordDigest="pgkLCLmWcld9xkTUJ4Rxnj+Ww50=", Nonce="MjU3ZWMzYzI1ZTVmYjg5NA==", Created=“2015-07-22T14:12:27+02:00"' < account.json $> cat account.json { "account": { "owner": 2 } } -
APPROACHES TO CREATE A NEW API • OroCRM has uses
various different approaches • Long term goal of OroCRM is to provide SOAP and REST via the same code • Available approaches: • Helper methods in Oro Platform RestGetController • Serialization via JMS Serializer or via Symfony core Serializer • Oro Platform EntitySerializer is a work in progress which make it even easier to cover REST and SOAP with the same controller -
BASIC CONTROLLER SETUP <?php namespace Acme\Bundle\CartBundle\Controller\Api\Rest; use FOS\RestBundle\Controller\Annotations\NamePrefix;
use FOS\RestBundle\Routing\ClassResourceInterface; use Oro\Bundle\SoapBundle\Controller\Api\Rest\RestController; /** * @NamePrefix("acme_api_") */ class CartController extends RestController implements ClassResourceInterface { .. } -
IMPLEMENTING GETTING A LIST (1/3) /** * REST GET list
* * @ApiDoc( * description="Get all carts", * resource=true * ) * @AclAncestor("orocrm_magento_cart_view") * * @return JsonResponse */ public function cgetAction() { /** @var Cart[] $carts */ $carts = $this->getManager()->getListQueryBuilder()->getQuery()->execute(); return new JsonResponse( $this->getPreparedItems($carts, self::$fields), Codes::HTTP_OK ); } -
IMPLEMENTING GETTING A LIST (2/3) <?php namespace Acme\Bundle\CartBundle\Controller\Api\Rest; use
OroCRM\Bundle\MagentoBundle\Entity\Cart; use Symfony\Component\HttpFoundation\JsonResponse; use FOS\RestBundle\Controller\Annotations\NamePrefix; use FOS\RestBundle\Routing\ClassResourceInterface; use FOS\RestBundle\Util\Codes; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Oro\Bundle\SecurityBundle\Annotation\AclAncestor; use Oro\Bundle\SoapBundle\Controller\Api\Rest\RestController; class CartController extends RestController implements ClassResourceInterface { static $fields = array('id', 'subTotal', 'grandTotal', 'taxAmount', 'customer') .. } -
IMPLEMENTING GETTING A LIST (3/3) /** * {@inheritdoc} */ public
function getManager() { return $this->get('acme.cart.manager.api'); } /** * Prepare entity field for serialization * * @param string $field * @param mixed $value */ protected function transformEntityField($field, &$value) { if ($value instanceof Customer) { $value = array('name' => $value->getLastName()); return; } parent::transformEntityField($field, $value); } -
IMPLEMENTING GETTING A ONE ENTITY /** * REST GET one
* * @ApiDoc( * description="Get one cart", * resource=true * ) * @AclAncestor("orocrm_magento_cart_view") * @param int $cartId * * @return JsonResponse */ public function getAction($cartId) { /** @var Cart $cart */ $cart = $this->getManager()->find($cartId); return new JsonResponse( $this->getPreparedItem($cart, self::$fields), empty($cartId) ? Codes::HTTP_NOT_FOUND : Codes::HTTP_OK ); } -
FULL EXAMPLE <?php namespace OroCRM\Bundle\MagentoBundle\Controller\Api\Rest; use OroCRM\Bundle\MagentoBundle\Entity\Customer; use
OroCRM\Bundle\MagentoBundle\Entity\Cart; use Symfony\Component\HttpFoundation\JsonResponse; use FOS\RestBundle\Controller\Annotations\NamePrefix; use FOS\RestBundle\Routing\ClassResourceInterface; use FOS\RestBundle\Util\Codes; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Oro\Bundle\SecurityBundle\Annotation\AclAncestor; use Oro\Bundle\SoapBundle\Controller\Api\Rest\RestController; /** * @NamePrefix("acme_api_") */ class CartController extends RestController implements ClassResourceInterface { static $fields = array('id', 'subTotal', 'grandTotal', 'taxAmount', 'customer'); /** * REST GET list * * @ApiDoc( * description="Get all carts", * resource=true * ) * @AclAncestor("orocrm_magento_cart_view") * * @return JsonResponse */ public function cgetAction() { /** @var Cart[] $carts */ $carts = $this->getManager()->getListQueryBuilder()->getQuery()->execute(); return new JsonResponse( $this->getPreparedItems($carts, self::$fields), Codes::HTTP_OK ); } /** * REST GET one * * @ApiDoc( * description="Get one cart", * resource=true * ) * @AclAncestor("orocrm_magento_cart_view") * @param int $cartId * * @return JsonResponse */ public function getAction($cartId) { /** @var Cart $cart */ $cart = $this->getManager()->find($cartId); return new JsonResponse( $this->getPreparedItem($cart, self::$fields), empty($cartId) ? Codes::HTTP_NOT_FOUND : Codes::HTTP_OK ); } public function getManager() { return $this->get('orocrm_magento.cart.manager.api'); } protected function transformEntityField($field, &$value) { if ($value instanceof Customer) { $value = array('name' => $value->getLastName()); return; } parent::transformEntityField($field, $value); } public function getFormHandler() { throw new \BadMethodCallException('FormHandler is not available.'); } } -
REQUIRED SERVICE # services.yml parameters: acme.cart.manager.api.class: Oro\Bundle\SoapBundle\..\ApiEntityManager services: acme.cart.manager.api: class:
%acme.cart.manager.api.class% parent: oro_soap.manager.entity_manager.abstract arguments: - %orocrm_magento.entity.cart.class% - @doctrine.orm.entity_manager -
REQUIRED ROUTE DEFINITION # routing.yml acme_bundle_cart_api: resource: "@AcmeCartBundle/Controller/Api/Rest/CartController.php" type: rest
prefix: api/rest/{version} requirements: version: latest|v1 defaults: version: latest -
USING OUR NEW OROCRM REST API $> http --json http://orocrm.lo/api/rest/v1/carts
‘X-WSSE..’ .. [ { "customer": { "name": "Clark" }, "grandTotal": "163.9248", "id": 1, "subTotal": "163.9248", "taxAmount": "12.6748" }, -
TESTING OROCRM REST APIS (1/2) use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase; class ApiCartControllerTest
extends WebTestCase { /* Taken from BazingaRestExtraBundle */ protected function assertJsonResponse($response, $statusCode = 200) { $this->assertEquals( $statusCode, $response->getStatusCode(), $response->getContent() ); $this->assertTrue( $response->headers->contains('Content-Type', 'application/json'), $response->headers ); } protected function setUp() { $this->initClient(array(), $this->generateWsseAuthHeader()); } .. -
TESTING OROCRM REST APIS (2/2) public function testGetCarts() { $this->client->request('HEAD',
'/api/rest/v1/carts'); $response = $this->client->getResponse(); $this->assertEquals(200, $response->getStatusCode(), $response->getContent()); $this->client->request('GET', ‘/api/rest/v1/carts'); $response = $this->client->getResponse(); $this->assertJsonResponse($response); $this->assertEquals(‘..’, $response->getContent()); } public function testGetCart() { $this->client->request('GET', '/api/rest/v1/carts/1'); $response = $this->client->getResponse(); $this->assertJsonResponse($response); $this->assertEquals('{"id": 1,"subTotal":"163.9248","grandTotal":"163.9248","taxAmount":"12.6748","customer": {"name":"Clark"}}', $response->getContent()); }