54
Agile Web Development Liip.ch RESTING WITH Learn how to use and extend the OroCRM REST API

Resting with OroCRM Webinar

  • Upload
    oro-inc

  • View
    1.110

  • Download
    0

Embed Size (px)

Citation preview

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

INTRODUCTION TO REST

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 AnatomyGET /notes HTTP/1.1Host: symfony-rest-edition.loAccept: application/json;q=0.9,*/*;q=0.8Content-Type: application/jsonContent-Length: length

HTTP Response AnatomyHTTP/1.1 200 OKAllow: GET, POSTCache-Control: max-age=15, public, s-maxage=30Content-Type: application/jsonDate: Wed, 15 Jan 2014 15:09:01 GMTLast-Modified: Wed, 15 Jan 2014 14:09:03 GMTServer: Apache/2.2.24 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.24 OpenSSL/0.9.8yVary: Accept-Encoding,Accept-Language

{"notes":["a","b","c"]}

REST MATURITY MODEL

http://martinfowler.com/articles/richardsonMaturityModel.html

RMM LEVEL 0

• Aka "The Swamp of POX"

• HTTP as a tunneling mechanism

• "Procedural" communication (RPC)

• Single endpoint (per operation)

RMM LEVEL 0

http://martinfowler.com/articles/richardsonMaturityModel.html

RMM LEVEL 1

• Aka "Resources"

• Individual resources, i.e. URIs

• "Object orientated" communication

RMM LEVEL 1

http://martinfowler.com/articles/richardsonMaturityModel.html

RMM LEVEL 2

• Aka "HTTP Verbs"

• Client uses specific HTTP method

• Server uses HTTP status codes

RMM LEVEL 2

http://martinfowler.com/articles/richardsonMaturityModel.html

HTTP VERBS

Method Safe IdempotentGET yes yesHEAD yes yesPOST no noPUT no yesDELETE 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 Permanently4xx 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 LEVEL 3

http://martinfowler.com/articles/richardsonMaturityModel.html

HYPERTEXT AS THE ENGINE OF APPLICATION STATE

= HATEOAS

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

CONTENT TYPE NEGOTIATION

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 DescriptionContent-Type HTTP message formatAccept 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 Descriptionq=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

}

}

DEMO TIME

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

<?phpnamespace 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)

<?phpnamespace 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<?phpnamespace 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());}

DEMO TIME

Agile Web Development Liip.ch

QUESTIONS?

THANK YOU VERY MUCH!