Thinking Headless, make REST API
- Anatomy of Magento 2: API
- Check videos (FM2D, Nomad Mage) for API related content
- Verify if browser-based session is only available for customer account types.
- Are integration users only used by the oauth system?
- Create demo extensions which use each authentication type.
- Verify the API versioning strategy.
- What's the thinking behind it?
- How does it affect code execution?
- How to use it in custom code? Should you even do that?
- How to test API?
- Create Magento 2 API cheatsheet
Notes were taken against Magento 2.1.5
There are two APIs built into Magento 2: REST
and SOAP
.
All Magento 2 APIs are versioned, with a major version number in each API resource method name, e.g. catalogProductRepositoryV1
.
This means that any breaking changes will necessitate a new API resource method with a new name. Partly this was done to allow two versions of the API to co-exist when backwards incompatible changes are introduced. The version numbering schema of API methods deliberately does not follow the SemVer guidelines (1).
Resource method names need to follow the regex [a-zA-Z\d]*V[\d]+
.
When a request is made to an API endpoint, the matching resource method name in the webapi
config XML is called and the results returned to the API consumer.
There are four account types (in order of descending permissions): Admin
, Integration
, Customer
and Guest
.
Admin
users can access anything.
Integration
users are only used by the OAuth authentication system. They are intended to be used in situations where a module needs API access to an installation, but admin access has not been granted. Their access to resources is limited to their custom ACL role, self
or anonymous
. Specific ACL roles need to be created for integration users.
Customer
users can only access resources with a type of self
or anonymous
, i.e. Only the data for that specific customer.
Guest
users can only access resources with a type of anonymous
. If Magento cannot authenticate an API consumer, they default to the Guest
type.
- Submit a request to the appropriate
REST
orSOAP
endpoint, specifying the username and password of anAdmin
orCustomer
. - Receive a token.
- Specify this token in future requests.
Tokens never expire, but can be revoked.
The Magento 2 Dev Docs are unusually detailed about making getting a token, making future requests and explaining the different parts of a token request URI:
Magento 2 Dev Docs: Token-based authentication
- Login to the website as an
Admin
orCustomer
. - Submit a request.
- Receive the results.
This type of authentication is useful for when you need to query the API for the current customer's data using a JavaScript widget, for example.
For Customer
s, the resources you can access with this authentication type are limited to resources of the self
and anonymous
type.
For Admin
s, the resources you can access are limited to those defined in the ACL role you are assigned to.
The Magento 2 Dev Docs provide a little bit more detail:
Magento 2 Dev Docs: Session-based authentication
- Create a new integration in the Magento admin.
- Activate the integration in Magento.
- Call the third party application's login page.
- Third party application asks for a request token.
- Magento sends the request token.
- The third party application asks for an access token.
- Magento sends an access token.
- The third party application can now access Magento resources.
In Magento, a third-party extension that uses OAuth for authentication is called an integration. An integration defines which resources the extension can access. The extension can be granted access to all resources or a customized subset of resources.
As the process of registering the integration proceeds, Magento creates the tokens that the extension needs for authentication. It first creates a request token. This token is short-lived and must be exchanged for access token. Access tokens are long-lived and will not expire unless the merchant revokes access to the extension.
The Magento 2 Dev Docs are unusually detailed in this respect:
Magento 2 Dev Docs: OAuth-based authentication
Source: Magento Quickies: Magento 2: Understanding Integration API Users
- Submit a request to the appropriate
REST
orSOAP
endpoint. - Receive response
- Profit
Tokens can be passed for anonymous requests, but they are, for obvious reasons, totally optional.
There are three types of M2 API resource: <ACL Rule Identifer>
, self
, anonymous
.
Each of these are defined in the ref
attribute of the <resource/>
node.
ACL Rule Identifier
refers to an existing Magento 2 ACL permission, e.g:
// File: vendor/magento/module-quote/etc/webapi.xml
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<!-- Managing Cart -->
<route url="/V1/carts/:cartId" method="GET">
<service class="Magento\Quote\Api\CartRepositoryInterface" method="get"/>
<resources>
<resource ref="Magento_Cart::manage" />
</resources>
</route>
</routes>
If you authenticate as a customer, you can only access the following two:
self
is used with the Customer authentication account type. It means only resources with the self
or anonymous
type are accessible.
// File: vendor/magento/module-quote/etc/webapi.xml
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route url="/V1/carts/mine" method="GET">
<service class="Magento\Quote\Api\CartManagementInterface" method="getCartForCustomer"/>
<resources>
<resource ref="self" />
</resources>
<data>
<parameter name="customerId" force="true">%customer_id%</parameter>
</data>
</route>
</routes>
anonymous
is used with the Customer authentication account type. It means only resources with the anonymous
type are accessible.
// File: vendor/magento/module-quote/etc/webapi.xml
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<!-- Managing guest carts -->
<route url="/V1/guest-carts/:cartId" method="GET">
<service class="Magento\Quote\Api\GuestCartRepositoryInterface" method="get"/>
<resources>
<resource ref="anonymous" />
</resources>
</route>
</routes>
Filters allow a result set to be filtered to only return records which match specific criteria.
Filters come in two flavours: Required and Optional. They are specified in the same way.
For example, to retrieve a product record for a specific SKU:
// REST
https://<MAGENTO_HOST_OR_IP>/<MAGENTO_BASE_INSTALL_DIR>/rest/V1/products/:sku
// SOAP (The :sku is passed in the payload as normal with SOAP requests)
http://<magento_host>/soap/default?wsdl&services=catalogProductRepositoryV1
Where :sku
is the product SKU.
There is no longer an XML-RPC
API in Magento 2 as there was in Magento 1.
The SOAP
API no longer has a single WSDL. Instead, individual API objects/services have their own WSDL.
The entire list can be retrieved here: http://<magento_host>/soap/default?wsdl_list=1
Magento 2 Dev Docs: List of service names per module
Unlike Magento 1, there are no API specific ACLs. The API shares the same ACL rules as a regular admin user.
The SOAP and REST based APIs are, from a business logic point of view, equivalent.
Each Magento 2 API request runs through a standard Magento controller. Both REST
and SOAP
APIs have their own
controller for handling requests:
<?php
namespace Magento\Webapi\Controller;
/**
* Front controller for WebAPI REST area.
*/
class Rest implements \Magento\Framework\App\FrontControllerInterface
{
// ...
}
?>
<?php
namespace Magento\Webapi\Controller;
/**
* Front controller for WebAPI SOAP area.
*/
class Soap implements \Magento\Framework\App\FrontControllerInterface
{
// ...
}
?>
To query the SOAP
API, dispatch a request to an endpoint in the following format:
http://<magento_host>/soap/<store_code>?wsdl&services=<serviceName1,serviceName2,..>
store_code
can be one of:
default
: The default store code.<store_code>
: A store code which exists in this installation.all
. This is a special value which only applies to the CMS and Product modules. If this value is specified, the API call affects all the merchant's stores.
serviceNameN
is the name of the resource method you want to query. E.g. If you want to get a list of Products, you would specify the catalogProductRepositoryV1
resource method name.
Multiple resource method names can be specified in one SOAP
request by separating them using the ,
separator:
http://<magento_host>/soap/<store_code>?wsdl&services=<serviceName1,serviceName2,..>
The entire list can be retrieved for a specific installation here: http://<magento_host>/soap/default?wsdl_list=1
Alternatively, you can find a list of all the resource methods in a vanilla installation in the source link below.
Source: Magento 2 Dev Docs: List of service names per module
- API consumer hits the appropriate endpoint (e.g.
http://<magento_host>/soap/default?wsdl&services=catalogProductRepositoryV1
) Magento\Webapi\Controller\Soap::dispatch
receives request, then sets the application area, checks for and validates theWSDL
, if present and hands off handling of the request toMagento\Webapi\Controller\Soap::handle()
.- Magento validates the request body
- If the request has an empty body or has invalid XML, Magento throws an
\Magento\Framework\Webapi\Exception
with messageInvalid XML
. - If the request has an XML document type node, Magento throws an
\Magento\Framework\Webapi\Exception
with messageInvalid XML: Detected use of illegal DOCTYPE
. - In either case, Magento terminates the request and responds with an
HTTP 500
error code.
- If the request has an empty body or has invalid XML, Magento throws an
- Magento generates a local
WSDL
URI (this is done to be able to pass theWSDL
schema to a new PHPSoapServer
object without needing authorisation) and passes it to the new PHPSoapServer
object, along with some default options (encoding,SOAP
version). - When Magento creates a new
SoapServer
object using theMagento\Webapi\Model\Soap\ServerFactory
, automatic DI injects a\Magento\Webapi\Controller\Soap\Request\Handler
object and sets it against the PHPSoapServer
object. - Magento calls the
SoapServer::handle()
method, which delegates to the\Magento\Webapi\Controller\Soap\Request\Handler::__call()
magic method to determine which controller and method should be dispatched to handle the request.
- Magento checks if the request needs to be made over HTTPS and if it was.
- Magento checks the request against the appropriate ACL permissions.
- Magento then tries to match the requested resource method with a resource method defined in one of the
webapi.xml
files, which are merged and cached on the first API request. - If a cached version of the merged
webapi.xml
tree doesn't exist, aMagento\Webapi\Model\ServiceMetadata
object is created which parses it, converts it to an array, serialises it and stores it in the cache.- Since the configuration files are biased towards a
REST
Ful version of the universe, theServiceMetadata
object is used to transform the configuration into information theSOAP
handler object can use to match up aSOAP
request with the PHP class and method. - Part of the parsing of the tree includes using PHP's Reflection API to detect all the methods in each resource class and adding that to the array which is eventually cached.
- Since the configuration files are biased towards a
- The passed request method is then cross-referenced against the array, which is used to find the appropriate class and method used to fulfil the request.
- Magento then uses the Object Manager to instantiate the appropriate object for the request and then calls the resource method against the instantiated object and returns the response.
Source: Magento Quickies: Magento 2: Understanding the Web API Architecture
This workflow assumes that the API consumer has already obtained the necessary token to make an API request.
- API consumer hits the appropriate endpoint (e.g.
http://<magento_host>/rest/V1/products/
) Magento\Webapi\Controller\Rest::dispatch()
receives request, sets the application area and then passes off to another method to process the request.- The Service Class and Service Method are detected from the endpoint.
- The Service Method is called and the output is returned.
- The data is then converted into a scalar value or array of scalar values using the
Magento\Framework\Webapi\ServiceOutputProcessor
class. - If any filter parameters were specified, they are applied to the data.
- The data is assigned to the response object and returned to the API consumer.
For both REST
and SOAP
, the logic calling the Service Method is wrapped in a try ... catch
block, with exceptions being caught by PHP's built-in Exception
class.
Exceptions are then processed by Magento\Framework\Webapi\ErrorProcessor::maskException()
whose function is to sterilise exception messages if not running in developer
mode.
Exceptions are caught and then assigned to the response object.
Exceptions are caught, then a new \Magento\Webapi\Model\Soap\Fault
object is created and returned in the response.
- Define the resource in the
webapi.xml
file - Add your API methods to an
Interface
- Add a
Model
which implements yourInterface
- Add a
preference
to yourdi.xml
.
All resource methods are defined in a app/code/[Vendor]/[Module]/etc/webapi.xml
file:
// File: app/code/ProjectEight/AddNewApiMethod/etc/webapi.xml
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<!-- Example: curl http://127.0.0.1/index.php/rest/V1/calculator/add/1/2 -->
<route url="/V1/calculator/add/:numberOne/:numberTwo" method="GET">
<!-- The 'add' method of the class which implements this interface will be called when this endpoint is hit -->
<service class="ProjectEight\AddNewApiMethod\Api\CalculatorInterface" method="add"/>
<resources>
<!-- Anyone can access this resource -->
<resource ref="anonymous"/>
</resources>
</route>
</routes>
// File: vendor/magento/module-quote/etc/webapi.xml
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<!-- Managing Cart -->
<route url="/V1/carts/:cartId" method="GET">
<service class="Magento\Quote\Api\CartRepositoryInterface" method="get"/>
<resources>
<resource ref="Magento_Cart::manage" />
</resources>
</route>
<!-- Other routes follow ... -->
</routes>
Where:
<route>
: Defines an endpoint which can be queried by an API.url="[endpoint]"
: Defines the URL, inREST
syntax, even if this resource is to be used by aSOAP
client.method="[HTTP Method]"
: Defines the HTTP method used to query this endpoint, e.g.GET
,POST
,PUT
,DELETE
.
<service/>
: Defines the interface (class=""
) and PHP method name (method="get"
) which will be called. Note thatmethod=""
does not refer to the HTTP method, not does it have to match the HTTP method!<resources>
: Defines one or more<resource>
nodes, which determine which permission types can access this resource.<resource/>
: Defines the permission type which can access this resource (A Magento 2 ACL role identifier,self
oranonymous
. The latter two are explained further below).
These are the basic nodes, there are others available as well. Check the vendor/magento/module-webapi/etc/webapi.xsd
for the definition of all the possible nodes and attributes.
The type hints in the doc blocks are important, because Magento 2 uses them to infer the types of passed arguments.
<?php
namespace ProjectEight\AddNewApiMethod\Api;
use ProjectEight\AddNewApiMethod\Api\Data\PointInterface;
interface CalculatorInterface
{
/**
* Add two numbers together
*
* @param int $numberOne
* @param int $numberTwo
*
* @return int
*/
public function add($numberOne, $numberTwo);
/**
* Sum an array of numbers.
*
* @param float[] $numbers The array of numbers to sum.
*
* @return float The sum of the numbers.
*/
public function sum($numbers);
/**
* Compute mid-point between two points.
*
*
* @param ProjectEight\AddNewApiMethod\Api\Data\PointInterface $pointOne The first point.
* @param ProjectEight\AddNewApiMethod\Api\Data\PointInterface $pointTwo The second point.
*
* @return ProjectEight\AddNewApiMethod\Api\Data\PointInterface The mid-point.
*/
public function midPoint($pointOne, $pointTwo);
}
<?php
namespace ProjectEight\AddNewApiMethod\Model;
use ProjectEight\AddNewApiMethod\Api\CalculatorInterface;
use ProjectEight\AddNewApiMethod\Api\Data\PointInterface;
use ProjectEight\AddNewApiMethod\Api\Data\PointInterfaceFactory;
class Calculator implements CalculatorInterface
{
/**
* Factory for creating new Point instances. This code will be automatically
* generated because the type ends in "Factory".
*
* @var PointInterfaceFactory
*/
private $pointFactory;
/**
* Constructor.
*
* @param PointInterfaceFactory $pointFactory Factory for creating new Point instances.
*/
public function __construct(PointInterfaceFactory $pointFactory)
{
$this->pointFactory = $pointFactory;
}
/**
* Add two numbers together
*
* @param int $numberOne
* @param int $numberTwo
*
* @return int
*/
public function add($numberOne, $numberTwo)
{
$sum = $numberOne + $numberTwo;
return $sum;
}
/**
* Sum an array of numbers.
*
* @param float[] $numbers The array of numbers to sum.
*
* @return float The sum of the numbers.
*/
public function sum($numbers)
{
$total = 0.0;
foreach ($numbers as $number) {
$total += $number;
}
return $total;
}
/**
* Compute mid-point between two points.
*
* @param PointInterface $pointOne The first point.
* @param PointInterface $pointTwo The second point.
*
* @return PointInterface The mid-point.
*/
public function midPoint($pointOne, $pointTwo)
{
$point = $this->pointFactory->create();
$point->setX(($pointOne->getX() + $pointTwo->getX()) / 2.0);
$point->setY(($pointOne->getY() + $pointTwo->getY()) / 2.0);
return $point;
}
}
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="ProjectEight\AddNewApiMethod\Api\CalculatorInterface"
type="ProjectEight\AddNewApiMethod\Model\Calculator"/>
<preference for="ProjectEight\AddNewApiMethod\Api\Data\PointInterface"
type="ProjectEight\AddNewApiMethod\Model\Point" />
</config>
You can now call your new method using REST
:
# Add two numbers
$ curl http://magento2-sample-modules.localhost.com/index.php/rest/V1/calculator/add/1/2
3
# Add an array of floating point numbers
$ curl -d '{"numbers":[1.1,2.2,3.3]}' -H 'Content-Type: application/json' http://magento2-sample-modules.localhost.com/index.php/rest/V1/calculator/sum
6.6
# Compute the mid-point between two points
$ curl -d '{"pointOne":{"x":10,"y":10},"pointTwo":{"x":30,"y":50}}' -H 'Content-Type: application/json' http://magento2-sample-modules.localhost.com/index.php/rest/V1/calculator/midpoint
{"x":20,"y":30}
Source [2]
// Create an admin token for future requests
$ curl --request POST http://magento2-sample-modules.localhost.com/index.php/rest/default/V1/integration/admin/token -H "Content-Type:application/json" -d '{"username":"admin","password":"password123"}'
ewkm55iwv9kl0g9ul194lxlgwo6jp93p
// Create a new customer
$ curl --request POST http://magento2-sample-modules.localhost.com/index.php/rest/default/V1/customers -H "Content-Type:application/json" -d \
'{
"customer": {
"email": "simonfrost2010@gmail.com",
"firstname": "Simon",
"lastname": "Frost",
"addresses": [{
"defaultShipping": true,
"defaultBilling": true,
"firstname": "Simon",
"lastname": "Frost",
"region": {
"regionCode": "NY",
"region": "New York",
"regionId":43
},
"postcode": "10755",
"street": ["123 Oak Ave"],
"city": "Purchase",
"telephone": "512-555-1111",
"countryId": "US"
}]
},
"password": "password123"
}'
// Create a customer token for future requests
$ curl --request POST http://magento2-sample-modules.localhost.com/index.php/rest/default/V1/integration/customer/token -H "Content-Type:application/json" -d '{"username":"simonfrost2010@gmail.com","password":"password123"}'
// Create an empty basket for this customer
$ curl --request POST http://magento2-sample-modules.localhost.com/index.php/rest/default/V1/carts/mine -H "Content-Type:application/json" -H "Authorization: Bearer 11mfta00er02w5uq9q50d1xxp9gnx85k"