Technical module to industrialize API REST call with dependency injection using Guzzle library. Provides multiple features to make your life easier while implementing a REST service as Magento developer:
- Prevent code duplication with a basic implementation in di.xml (only 1 class to create)
- Centralize all your REST web service under the same configuration
- Benefits built-in registry and cache system to ensure and secure performance
- Include a generic logger with a debug mode option to retrieve parameters, request and result
- Declare your request and result as a json serialized or not, avoiding multiple implementation of the serializer
composer require zepgram/module-rest
bin/magento module:enable Zepgram_Rest
bin/magento setup:upgrade
- Declare your service in di.xml by implementing
Zepgram\Rest\Service\ApiProvider
as VirtualClass, you can configure it by following the ApiProviderConfig - Declare your VirtualClass in dedicated Pool
Zepgram\Rest\Service\ApiPoolInterface
, the key used will be yourservice_name
- To create your dedicated ApiRequest use the ApiFactory
$this->apiFactory->get('service_name', $rawData)->sendRequest()
where:- service_name represents the service name declared previously in
apiProviders[]
- $rawData is an array of dynamic data that you will receive in
dispatch()
method
- service_name represents the service name declared previously in
- Create a system.xml, and a config.xml that must use the configName injected previously, see Rest api store config:
- section:
rest_api
- group_id:
$configName
- fields:
base_uri
timeout
is_debug
cache_ttl
- section:
- Finally, create a RequestAdapter class for your service extending abstract class
Zepgram\Rest\Model\RequestAdapter
, this class represent your service contract adapter:- public const SERVICE_ENDPOINT: define the service endpoint
- dispatch(DataObject $rawData): initialize data that you will adapt to request the web service
- getBody(): implement body request
- getHeaders(): implement headers
- getUri(): implement uri endpoint (used to handle dynamic values)
- getCacheKey(): implement cache key for your specific request (you must define a unique key)
If you do not declare specific configuration, the request will fall back on default configuration.
To override the default config, you must follow this system config pattern: rest_api/%configName%/base_uri
You can configure your service with Zepgram\Rest\Service\ApiProvider
by creating a
VirtualClass and customize its injections for your needs by following the below configuration:
Variable name | Type | Default value | Is Optional | Description |
---|---|---|---|---|
configName | string | default | no | Value to retrieve group id from system config |
requestAdapter | object | RequestAdapter | no | Adapter class to build and customize the request |
validator | object | null | yes | Validate the service contract |
method | string | GET | yes | Request method |
isJsonRequest | boolean | true | yes | Parse request array to json |
isJsonResponse | boolean | true | yes | Parse response string to array |
isVerify | boolean | true | yes | Enable SSL certificate verification |
Here is a simple implementation example with a service called Foxtrot using the order object as rawData:
FoxtrotOrderRequestAdapter.php
<?php
declare(strict_types=1);
namespace Zepgram\Sales\Rest;
use Magento\Framework\DataObject;
use Magento\Sales\Api\Data\OrderInterface;
use Zepgram\Rest\Model\RequestAdapter;
class FoxtrotOrderRequestAdapter extends RequestAdapter
{
/** @var string */
public const SERVICE_ENDPOINT = 'v1/order/';
/** @var OrderInterface */
private $order;
/**
* {@inheritDoc}
*/
public function dispatch(DataObject $rawData): void
{
$this->order = $rawData->getOrder();
}
/**
* {@inheritDoc}
*/
public function getBody(): array
{
return [
'orderId' => $this->order->getEntityId(),
'customer' => $this->order->getCustomerEmail(),
'orderTotal' => $this->order->getGrandTotal(),
'version' => '1.0',
];
}
/**
* {@inheritDoc}
*/
public function getCacheKey(): ?string
{
return $this->order->getEntityId();
}
}
di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- rest api -->
<virtualType name="FoxtrotOrderApiProvider" type="Zepgram\Rest\Service\ApiProvider">
<arguments>
<argument name="configName" xsi:type="string">foxtrot</argument>
<argument name="requestAdapter" xsi:type="object">Zepgram\Sales\Rest\FoxtrotOrderRequestAdapter</argument>
</arguments>
</virtualType>
<type name="Zepgram\Rest\Service\ApiPoolInterface">
<arguments>
<argument name="apiProviders" xsi:type="array">
<item name="foxtrot_order" xsi:type="object">FoxtrotOrderApiProvider</item>
</argument>
</arguments>
</type>
</config>
system.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="rest_api">
<group id="foxtrot" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0"
showInStore="0">
<label>Foxtrot</label>
<field id="base_uri" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="0"
showInStore="0" canRestore="1">
<label>Base URI</label>
</field>
<field id="timeout" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="0"
showInStore="0" canRestore="1">
<label>Timeout</label>
</field>
<field id="cache_ttl" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="0"
showInStore="0" canRestore="1">
<label>Cache TTL</label>
</field>
<field id="is_debug" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0"
showInStore="0" canRestore="1">
<label>Enable Debug</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
</group>
</section>
</system>
</config>
config.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<rest_api>
<foxtrot>
<base_uri>https://foxtrot.service.io</base_uri>
<timeout>30</timeout>
<cache_ttl>7200</cache_ttl>
<is_debug>1</is_debug>
</foxtrot>
</rest_api>
</default>
</config>
OrderDataExample.php
<?php
declare(strict_types=1);
namespace Zepgram\Sales\Model;
use Zepgram\Rest\Exception\InternalException;
use Zepgram\Rest\Exception\ExternalException;
use Zepgram\Rest\Service\ApiFactory;
use Zepgram\Sales\Api\OrderRepositoryInterface;
class OrderDataExample
{
public function __construct(
private OrderRepositoryInterface $orderRepository,
private ApiFactory $apiFactory
) {}
/**
* @param int $orderId
* @throws MyAweSomeTechnicalException
* @throws MyAwesomeBusinessException
*/
public function execute(int $orderId): void
{
try {
// get raw data
$order = $this->orderRepository->get($orderId);
// transform data
$foxtrotApiRequest = $this->apiFactory->get('foxtrot_order', ['order' => $order]);
// send request
$foxtrotOrderResult = $foxtrotApiRequest->sendRequest();
// handle result
$order->setFoxtrotData($foxtrotOrderResult);
$this->orderRepository->save($order);
} catch (ExternalException $e) {
$context['context_error'] = 'We could not reach foxtrot order service'
// service is unavailable due to technical reason: do something (log, throw, errorMessage..)
$this->logger->error($e, $context);
throw MyAwesomeTechnicalException(__('Foxtrot server error'), $e);
} catch (InternalException $e) {
$context['context_error'] = 'Magento request is wrong, foxtrot order service could not handle it'
// service rejected request for business reason: do something (log, throw, errorMessage..)
throw MyAwesomeBusinessException(__('Bad request error'), $e);
}
}
}
Do not catch something for nothing when you use this module. For example, if you are doing this:
try {
return $apiRequest->send();
} catch (InternalException $e) {
throw new InternalException(__('Internal error'));
} catch (ExternalException $e) {
throw new ExternalException(__('External error'));
} catch (Throwable $e) {
throw $e;
}
It has no value for the code because you are throwing the same exception and hiding the real error. Try/catch MUST only be used when you are able to handle errors for your feature (detailed logs, retry etc...).
Also, the Throwable
catch here will never throw.
The module only return Internal
and External
exception.
Others exceptions thrown are technical exception, they are returned when you do not implement the module correctly.
To handle ALL exceptions thrown by Zepgram_Rest you can simply catch the Zepgram\Rest\Exception\RestException
If you encountered an issue during installation or with usage, please report it on this github repository.
If you have good ideas to improve this module, feel free to contribute.