HTTP Service API Wrappers

Last updated on
28 September 2023

HTTP Service API Wrappers are Services acting as wrapper around existing HTTP Service API and can be defined within your services.yml file:

services:
  my_module.api_wrapper.changeme:
    class: Drupal\my_module\Plugin\HttpServiceApiWrapper\HttpServiceApiWrapperChangeMe
    parent: http_client_manager.api_wrapper.base
    tags:
      - { name: 'http_service_api_wrapper', api: 'changeme_wrapper' }
  • api: is a name you're giving to your wrapper, not to the real HTTP Service API

Wrappers extend the HttpServiceApiWrapperBase class and, as you can see below, they provide some useful methods you could use inside your Wrapper class.

<?php

namespace Drupal\http_client_manager\Plugin\HttpServiceApiWrapper;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\http_client_manager\Entity\HttpConfigRequest;
use Drupal\http_client_manager\Entity\HttpConfigRequestInterface;
use Drupal\http_client_manager\HttpClientManagerFactoryInterface;
use Drupal\http_client_manager\Request\HttpRequestInterface;
use GuzzleHttp\Command\Exception\CommandException;
use GuzzleHttp\Command\Result;
use GuzzleHttp\Command\ResultInterface;

/**
 * Class HttpServiceApiWrapperBase.
 *
 * @package Drupal\http_client_manager\Plugin\HttpServiceWrappers
 */
abstract class HttpServiceApiWrapperBase implements HttpServiceApiWrapperInterface {

  use StringTranslationTrait;

  /**
   * The cache id prefix.
   */
  const CACHE_ID_PREFIX = 'http_config_request';

  /**
   * The Http Client Factory Service.
   *
   * @var \Drupal\http_client_manager\HttpClientManagerFactoryInterface
   */
  protected $httpClientFactory;

  /**
   * Drupal\Core\Cache\CacheBackendInterface definition.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * The Language Manager Service.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * The Messenger Service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * HttpServiceApiWrapperBase constructor.
   *
   * @param \Drupal\http_client_manager\HttpClientManagerFactoryInterface $http_client_factory
   *   The Http Client Factory Service.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The Http Client Manager cache bin.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The Language Manager Service.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The Messenger Service.
   */
  public function __construct(HttpClientManagerFactoryInterface $http_client_factory, CacheBackendInterface $cache, AccountProxyInterface $current_user, LanguageManagerInterface $language_manager, MessengerInterface $messenger) {
    $this->httpClientFactory = $http_client_factory;
    $this->cache = $cache;
    $this->currentUser = $current_user;
    $this->languageManager = $language_manager;
    $this->messenger = $messenger;
  }

  /**
   * Call REST web services.
   *
   * @param string $command
   *   The command name.
   * @param array $args
   *   The command arguments.
   * @param mixed $fallback
   *   The fallback value in case of exception.
   *
   * @return \GuzzleHttp\Command\ResultInterface
   *   The service result.
   */
  protected function call($command, array $args = [], $fallback = []) {
    $httpClient = $this->gethttpClient();
    $http_method = $httpClient->getCommand($command)->getHttpMethod();

    try {
      return $httpClient->call($command, $args);
    }
    catch (CommandException $e) {
      $this->logError($e);

      if (strtolower($http_method) != 'get') {
        $fallback = [
          'error' => TRUE,
          'message' => $e->getMessage(),
        ];
      }
      return new Result($fallback);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function httpConfigRequest($request_name, $expire = FALSE, array $tags = []) {
    $request = HttpConfigRequest::load($request_name);
    if (empty($request)) {
      $args = ['%name' => $request_name];
      $message = $this->t('Undefined HTTP Config Request "%name"', $args);
      throw new \InvalidArgumentException($message);
    }

    if ($expire !== FALSE) {
      return $this->getCachedHttpConfigRequest($request, $expire, $tags);
    }

    try {
      $data = $request->execute()->toArray();
    }
    catch (CommandException $e) {
      $this->logError($e);
      $data = [];
    }
    return $data;
  }

  /**
   * Get cached HTTP Config Request.
   *
   * @param \Drupal\http_client_manager\Entity\HttpConfigRequestInterface $request
   *   The HTTP Config Request to be executed.
   * @param int $expire
   *   The cache expiry time.
   * @param array $tags
   *   An array of cache tags.
   *
   * @return array
   *   The Response array.
   */
  protected function getCachedHttpConfigRequest(HttpConfigRequestInterface $request, $expire, array $tags = []) {
    $lang = $this->languageManager->getCurrentLanguage()->getId();
    $cid = self::CACHE_ID_PREFIX . ':' . $request->id() . ':' . $lang;
    if ($cache = $this->cache->get($cid)) {
      return $cache->data;
    }

    try {
      $data = $request->execute()->toArray();
      $this->cache->set($cid, $data, $expire, $tags);
    }
    catch (CommandException $e) {
      $this->logError($e);
      $data = [];
    }
    return $data;
  }

  /**
   * Logs a command exception.
   *
   * This method is meant to be overridden by any Service Api Wrapper.
   * By default it prints the error message by using the Messenger service.
   *
   * @param \GuzzleHttp\Command\Exception\CommandException $e
   *   The Command Exception object.
   */
  protected function logError(CommandException $e) {
    $this->messenger->addError($e->getMessage());
  }

  /**
   * Get response.
   *
   * This method is meant to be overridden by any Service Api Wrapper.
   * By default it returns the result array, but it can be used to check if the
   * given response contains errors.
   *
   * @param \GuzzleHttp\Command\ResultInterface $result
   *   The command response.
   *
   * @return array
   *   The response array.
   */
  protected function getResponse(ResultInterface $result) {
    return $result->toArray();
  }

  /**
   * Call by Request.
   *
   * @param \Drupal\http_client_manager\Request\HttpRequestInterface $request
   *   The Request bean.
   *
   * @return array|bool
   *   The service response.
   */
  protected function callByRequest(HttpRequestInterface $request) {
    $result = $this->call($request->getCommand(), $request->getArgs(), $request->getFallback());
    return $this->getResponse($result);
  }

}

The final goal of HTTP Service API Wrappers is to have a class containing a method for each command defined in the corresponding HTTP Service API.

How you define your methods is up to you of course, but I'm going now to show you our style guide we decided to use for those wrappers.
It's a sort or magic recipe and all you need is:

  • one final class containing all the names of the commands we defined in the HTTP Service API
  • one final class containing all the parameters of the commands we defined in the HTTP Service API
  • HttpRequest classes used for storing your parameters and data handling / manipulation
  • one Wrapper Interface containing the HTTP Service API name together with all the methods definitions
  • one Wrapper class, used to put all together

If we consider the API we wrote here, this is how our Wrapper should look like:

Defining the Commands class

<?php

namespace Drupal\my_module\api\Commands;


/**
 * Class AcmeServicesContents.
 *
 * @package Drupal\my_module\api\Commands
 */
final class AcmeServicesContents {

  const GET_POSTS = 'GetPosts';

  const GET_POST_COMMENTS = 'GetPostComments';

}

Defining the Parameters class

<?php

namespace Drupal\my_module\api\Parameters;

/**
 * Class AcmeServicesContents.
 *
 * @package Drupal\my_module\api\Parameters
 */
final class AcmeServicesContents {

  const LIMIT = 'limit';

  const SORT = 'sort';

  const POST_ID = 'postId';

}

Defining the HttpRequest classes

GetPosts

<?php


namespace Drupal\my_module\api\Request;


use Drupal\http_client_manager\Request\HttpRequestBase;
use Drupal\my_module\api\Commands\AcmeServicesContents;
use Drupal\my_module\api\Parameters\AcmeServicesContents as Param;

/**
 * Class GetPosts.
 *
 * @package Drupal\my_module\api\Request
 */
class GetPosts extends HttpRequestBase {

  /**
   * The number of posts to be retrieved.
   *
   * @var int
   */
  protected $limit = 10;

  /**
   * The sorting order.
   *
   * @var string
   */
  protected $sort = 'desc';

  /**
   * Get limit.
   *
   * @return int
   *   The number of posts to be retrieved.
   */
  public function getLimit() {
    return (int) $this->limit;
  }

  /**
   * Set limit.
   *
   * @param int $limit
   *   The number of posts to be retrieved.
   *
   * @return $this
   */
  public function setLimit($limit) {
    $this->limit = (int) $limit;
    return $this;
  }

  /**
   * Get sort.
   *
   * @return int
   *   The sorting order.
   */
  public function getSort() {
    return $this->sort;
  }

  /**
   * Set sort.
   *
   * @param string $sort
   *   The sorting order.
   *
   * @return $this
   */
  public function setSort($sort) {
    if (!in_array($sort, ['desc', 'asc'])) {
       throw new \InvalidArgumentException('Not a valid sort criteria');
    }
    $this->sort = $sort;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getCommand() {
    return AcmeServicesContents::GET_POSTS;
  }

  /**
   * {@inheritdoc}
   */
  public function getArgs() {
    return [
      Param::LIMIT => $this->getLimit(),
      Param::SORT => $this->getSort(),
    ];
  }

}

GetPostComments

<?php


namespace Drupal\my_module\api\Request;


use Drupal\http_client_manager\Request\HttpRequestBase;
use Drupal\my_module\api\Commands\AcmeServicesContents;
use Drupal\my_module\api\Parameters\AcmeServicesContents as Param;

/**
 * Class GetPostComments.
 *
 * @package Drupal\my_module\api\Request
 */
class GetPostComments extends HttpRequestBase {

  /**
   * The Post ID.
   *
   * @var int
   */
  protected $postId;

  /**
   * Get Post ID.
   *
   * @return int
   *   The Post ID.
   */
  public function getPostId() {
    return (int) $this->postId;
  }

  /**
   * Set Post ID.
   *
   * @param int $postId
   *   The Post ID.
   *
   * @return $this
   */
  public function setPostId($postId) {
    $this->postId = (int) $postId;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getCommand() {
    return AcmeServicesContents::GET_POST_COMMENTS;
  }

  /**
   * {@inheritdoc}
   */
  public function getArgs() {
    return [
      Param::POST_ID => $this->getPostId(),
    ];
  }

}

Defining the Wrapper service

services:
  my_module.api_wrapper.acme_services_contents:
    class: Drupal\my_module\Plugin\HttpServiceApiWrapper\HttpServiceApiWrapperAcmeServicesContents
    parent: http_client_manager.api_wrapper.base
    tags:
      - { name: 'http_service_api_wrapper', api: 'acme_services_contents_wrapper' }

Defining the Wrapper interface

<?php

namespace Drupal\my_module\Plugin\HttpServiceApiWrapper;


use Drupal\my_module\api\Request\GetPosts;
use Drupal\my_module\api\Request\GetPostComments;

/**
 * Interface HttpServiceApiWrapperAcmeServicesContentsInterface.
 *
 * @package Drupal\my_module\Plugin\HttpServiceApiWrapper
 */
interface HttpServiceApiWrapperAcmeServicesContentsInterface {

  const SERVICE_API = 'acme_services.contents';

  /**
   * Get Posts.
   *
   * @param GetPosts $request
   *   The HTTP Request object.
   *
   * @return array
   *   The service response.
   */
  public function getPosts(GetPosts $request);

  /**
   * Get GetPostComments.
   *
   * @param GetPostComments $request
   *   The HTTP Request object.
   *
   * @return array
   *   The service response.
   */
  public function getPostComments(GetPostComments $request);

}

In this case the SERVICE_API constant is the name of the real HTTP Service API we defined earlier.

Defining the Wrapper class

<?php

namespace Drupal\my_module\Plugin\HttpServiceApiWrapper;


use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\http_client_manager\Plugin\HttpServiceApiWrapper\HttpServiceApiWrapperBase;
use Drupal\my_module\api\Request\GetPosts;
use Drupal\my_module\api\Request\GetPostComments;
use GuzzleHttp\Command\Exception\CommandException;

/**
 * Class HttpServiceApiWrapperCourseCatalogue.
 *
 * @package Drupal\my_module\Plugin\HttpServiceApiWrapper
 */
class HttpServiceApiWrapperAcmeServicesContents extends HttpServiceApiWrapperBase implements HttpServiceApiWrapperAcmeServicesContentsInterface {

  use LoggerChannelTrait;

  /**
   * {@inheritdoc}
   */
  public function getHttpClient() {
    return $this->httpClientFactory->get(self::SERVICE_API);
  }

  /**
   * {@inheritdoc}
   */
  public function getPosts(GetPosts $request) {
    return $this->callByRequest($request);
  }

  /**
   * {@inheritdoc}
   */
  public function getPostComments(GetPostComments $request) {
    return $this->callByRequest($request);
  }

  /**
   * {@inheritdoc}
   */
  protected function logError(CommandException $e) {
    // Better not showing the error with a message on the screen.
    $this->getLogger(self::SERVICE_API)->debug($e->getMessage());
  }

}

And that's it.

Now whenever I'll need to query my ACME Api, I'll just need to inject my wrapper via DI and do something like this:

<?php

// ...

$posts = $this->apiWrapper->getPosts(new GetPosts());

// ...

$request = new GetPosts();
$request->setLimit(30)
  ->setSort('asc');
$posts = $this->apiWrapper->getPosts(request);

// ...

$request = new GetPostComments();
$request->setPostId(123);
$comments = $this->apiWrapper->getPostComments($request);

// ...

Using HttpRequest objects instead of variables adds a lot of consistency to your code even if the effort is slightly higher but I'm sure you won't regret it once tried.

Help improve this page

Page status: No known problems

You can: