Introduction

Last updated on
24 November 2022

HTTP Client Manager is a module for developers working with REST Services.

Its main goal is to define a standard usage when dealing with HTTP clients by reducing code duplication and continuous refactoring, managing HTTP Requests in a centralized way, adding a validation layer to both Request params and  Response results, and much more.

Drupal 8/9 already provides an HTTP Client which is accessible by calling the \Drupal::httpClient() static method or by using the http_client service via Dependency Injection.

Here is the static usage method from core/lib/Drupal.php:

/**
 * Returns the default http client.
 *
 * @return\GuzzleHttp\ClientInterface
 *   A guzzle http client instance.
 */
public static function httpClient() {
  return static::getContainer()->get('http_client');
}

Use as a Dependency Injection example:

/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container) {
  return new static(
    $container->get('http_client')
  );
}

How to include into a certain existing service. As an example, the mymodule.services.yml file:

services:

  # My module service definition.
  mymodule_service:
    class: Drupal\mymodule\MymoduleServiceExample
    # Add the HTTP client service as the service argument.
    arguments: ['@http_client']

then inside the MymoduleServiceExample service class:

<?php

namespace Drupal\mymodule;

/**
 * The MymoduleServiceExample service class.
 */
class MymoduleServiceExample {
  
  /**
   * The HTTP client to fetch the feed data with.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;
  
  /**
   * Constructor for MymoduleServiceExample.
   *
   * @param \GuzzleHttp\ClientInterface $http_client
   *   A Guzzle client object.
   */
  public function __construct(ClientInterface $http_client) {
    $this->httpClient = $http_client;
  }

}

As we can see, by taking a look at the code, Drupal uses Guzzle as its default http client and that's great since Guzzle is one of the best http client libraries available for PHP.

Performing http requests by using Guzzle is really an easy thing to be done.
Let's make some examples imagining we are inside a Controller class which exposes two methods bounded to two different Drupal routes.

The first route will show us a list of posts fetched from a REST Service, while the second route will show all the comments related to a single post.

So let's imagine our Controller class looking to something like this:

<?php

namespace Drupal\mymodule\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use GuzzleHttp\ClientInterface;

/**
 * Class MyController.
 *
 * @package Drupal\mymodule\Controller
 */
class MyController extends ControllerBase {

  /**
   * Guzzle\Client instance.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;

  /**
   * {@inheritdoc}
   */
  public function __construct(ClientInterface $http_client) {
    $this->httpClient = $http_client;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('http_client')
    );
  }

  /**
   * Posts route callback.
   *
   * @param int $limit
   *   The total number of posts we want to fetch.
   * @param string $sort
   *   The sorting order.
   *
   * @return array
   *   A render array used to show the Posts list.
   */
  public function posts($limit, $sort) {
    $build = [
      '#theme' => 'mymodule_posts_list',
      '#posts' => [],
    ];

    $request = $this->httpClient->request('GET', 'http://api.example.com/posts', [
      'limit' => $limit,
      'sort' => $sort,
    ]);

    if ($request->getStatusCode() != 200) {
      return $build;
    }

    $posts = $request->getBody()->getContents();
    foreach ($posts as $post) {
      $build['#posts'][] = [
        'id' => $post['id'],
        'title' => $post['title'],
        'text' => $post['text'],
      ];
    }
    return $build;
  }

  /**
   * Post Comments route callback.
   *
   * @param int $postId
   *   The ID of the Post.
   *
   * @return array
   *   A render array used to show the Comments list.
   */
  public function postComments($postId) {
    $build = [
      '#theme' => 'mymodule_post_comments_list',
      '#comments' => [],
    ];

    $request = $this->httpClient->request('GET', 'http://api.example.com/posts/' . $postId . '/comments');

    if ($request->getStatusCode() != 200) {
      return $build;
    }

    $comments = $request->getBody()->getContents();
    foreach ($comments as $comment) {
      $build['#comments'][] = [
        'id' => $comment['id'],
        'title' => $comment['title'],
        'text' => $comment['text'],
      ];
    }
    return $build;
  }

}

Taking a look at the examples above there are some things we should focus on:

  1. The base URL of our endpoints (http://api.example.com/is always the same and we are going to repeat it every time we have to execute an HTTP Request.
    This means that if we have the need to use a different base URL for any reason (eg: need to switch to development services or to use a mocked service provider) we'll have to edit all of them with an awesome job of finding and replace... cool.
  2. The HTTP Requests are executed directly inside the Controller methods, which means the business logic necessary to handle errors or data manipulation is scattered here and there within our custom modules files, maybe cause code redundancy and difficulties for future code refactoring or bug fixing.
  3. The values we are passing as arguments to the http client are not being validated at all. For example, I could pass an array or an object instead of an integer to the method postComments($postId) causing drama and panic attacks all over the world.
    1. Yes, I should add a minimum validation to the request parameters in order to be sure I'm not going to offend the REST Server.

These are just a few issues we should keep in consideration when working with REST Services.
Now let's see how we could improve our code by trying to solve the points listed above:

  1. We could create a new Drupal Service mapping a new MyHttpClient class inside the mymodule.services.yml file within our module directory.
  2. The base URL could become the property of our new class so that we would avoid typing it, again and again, every time we'll have to make an HTTP Request.
  3. We could create a public method for each endpoint we have to query, in order to put all the business logic needed to validate request params, handle errors and manipulate response data in a more efficient way.

These are good starting points for sure but, sadly, are not enough in the long term and the risks are the following:

  • As the number of REST Resources grows, our MyHttpClient class will grow as well, going to contain a lot of public methods for any of those.
  • If we are working with multiple REST Web Services we'll need another HttpClient class, which means another Drupal service may be similar for some aspects to the one previously created and, in some cases, just another argument to be passed to a constructor class
  • Where to put those HTTP Clients? All inside the same module?
  • And when you'll start a new project, will you copy and paste the clients you wrote in your last project and replace things here and there? 

The next few pages of this section will show you how to use HTTP Client Manager to help us solve all that mess.

Help improve this page

Page status: No known problems

You can: