Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RESTing with OroCRM

lsmith
August 11, 2015

RESTing with OroCRM

Introduction to REST theory as well as how to use and extend the OroCRM REST APIs.

lsmith

August 11, 2015
Tweet

More Decks by lsmith

Other Decks in Technology

Transcript

  1. 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
  2. Everyone who has ever talked about REST, at some point

    has said something idiotic about REST. Except for maybe Roy Fielding.
  3. For example, I once thought it meant converting all GET

    parameters to virtual directories. Differentiating GET and POST seemed like overzealous academics.
  4. 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.
  5. 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.
  6. 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"]}
  7. RMM LEVEL 0 • Aka "The Swamp of POX" •

    HTTP as a tunneling mechanism • "Procedural" communication (RPC) • Single endpoint (per operation)
  8. RMM LEVEL 1 • Aka "Resources" • Individual resources, i.e.

    URIs • "Object orientated" communication
  9. RMM LEVEL 2 • Aka "HTTP Verbs" • Client uses

    specific HTTP method • Server uses HTTP status codes
  10. HTTP VERBS Method Safe Idempotent GET yes yes HEAD yes

    yes POST no no PUT no yes DELETE no yes (*) .. no no
  11. 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?
  12. 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
  13. RMM LEVEL 3 • Aka "Hypermedia Control" • Service discovery

    via link relations • ATOM, HAL, JSON-LD, IANA Link Rel
  14. 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
  15. RMM VS REST VS REAL LIFE • Browsers are bad

    REST clients • REST is protocol independent • Few (no?) clients really leverage HATEOAS
  16. UNIFIED RESOURCE IDENTIFIER • URIs identify resources • URIs are

    format independent • URI "file extensions" != RESTful
  17. 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
  18. • 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
  19. 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)
  20. 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
  21. 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”
  22. 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.
  23. 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"
  24. 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
  25. 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",
  26. 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 } }
  27. 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
  28. 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
 {
 .. }
  29. 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
 );
 }
  30. 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') ..
 }
  31. 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);
 }
  32. 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
 );
 }
  33. 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.');
 }
 }

  34. 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
  35. 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
  36. 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" },
  37. 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());
 } ..

  38. 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());
 }