Upload
oro-inc
View
1.110
Download
0
Embed Size (px)
Citation preview
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 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"]}
RMM LEVEL 0
• Aka "The Swamp of POX"
• HTTP as a tunneling mechanism
• "Procedural" communication (RPC)
• Single endpoint (per operation)
RMM LEVEL 1
http://martinfowler.com/articles/richardsonMaturityModel.html
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
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 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
}
}
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());}