Drupal 8 - Consume an ASMX SOAP service (Exchange rate | BCCR)

By kenneth, Thu, 07/18/2019 - 16:21

Authored on



EDIT ON MAY 19 2020:



From time to time by different reasons I got involved into interviews processes, then technical questions stuff come to my mind, when it's about Drupal back-end integrations and customizations. What I used to ask is something like:

"Are you into back-end integrations?" - `sure thing!` They used to say, then I shoot my first question.

"Have you ever consume a service? RESTful API/ASMX SOAP?"

At least, in my experience, it seems that people are starting to forget about SOAP services, from my perspective isn't too bad, since we have JSON and RESTful APIs out there. However there are a lot of legacy implementations around the web which are still using this kind of SOAP services.

Then I thought to kill two birds with one rock, share a little bit my experience on how to consume a service by using Drupal 8 and also by using a SOAP service available here at Costa Rica from the Banco Central de Costa Rica, (a.k.a., BCCR) what is exposing exchange rate information.

ASMX SOAP Definition

The documentation is your friend, if you didn't know it already, now you know. We need to find the documentation in order to use the service, it is not required you may think but trust me it helps a lot. I found the definition at official Bank's page

After some back and forth I found a public subdomain where the ASMX file is available, 


By click "The following operations are supported. For a formal definition, please review the Service Description." It wil redirect you to XML service definition (it just includes the following query string "?WSDL" to the URL).

Then I read the documentation and I understood that I need to consume their XML method one,


This method requires an index/indicator to retrieve the proper type of value we want to show up, it's a public list of them available here:


In our case we may use the following codes:

  • Compra Diaria (317) | Buying rate
  • Venta Diaria (318) | Selling rate
Exchange rate module

Keep it simple, as far as you can! For the sake of this tutorial I will create a new module which will contain a service to consume the SOAP web service mentioned above and also a controller to show up the exchange rate information retrieved from BCCR's web service.


name: Exchange Rate
description: Consume ASMX SOAP service to show up exchange rate from BCCR
package: keboca

type: module
core: 8.x

  - drupal:serialization

Next thing, we need to define two new services and setup our dependency injection properly:


    class: \Drupal\exchange_rate\ExchangeRateManager
    arguments: ['@http_client', '@serializer', '@cache.default']
    class: \Drupal\exchange_rate\Controller\ExchangeRateController
    arguments: ['@exchange_rate.manager']

Based on previous service definition we need to create our manager class per se, it means the arguments should be injected into manager's constructor class, it will look like this:



namespace Drupal\exchange_rate;

use Drupal\Component\Datetime\DateTimePlus;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Cache\CacheBackendInterface;
use GuzzleHttp\Client;
use Symfony\Component\Serializer\Serializer;

 * Class ExchangeRateManager
 * @package Drupal\exchange_rate
class ExchangeRateManager {

   * @var \GuzzleHttp\Client
  protected $httpClient;

   * @var \Symfony\Component\Serializer\Serializer
  protected $serializer;

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

   * ExchangeRateManager constructor.
   * @param \GuzzleHttp\Client $http_client
   * @param \Symfony\Component\Serializer\Serializer $serializer
   * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
  public function __construct(Client $http_client, Serializer $serializer, CacheBackendInterface $cacheBackend) {
    $this->httpClient = $http_client;
    $this->serializer = $serializer;
    $this->cache = $cacheBackend;

  // code...

Now we have to create our method in order to consume the SOAP service, it may look like this:

   * Retrieve exchange rate information from cache or by consuming the API.
   * @param array $dates
   * @param int $type
   * @return array|null
  public function getExchangeRates(array $dates, int $type) {
    // Init resource.
    $resource = NULL;

    // Build cache ID value based on given values.
    $cid = implode('::', [Tags::implode($dates), $type]);

    // Looks up value from cache.
    if ($cache = $this->cache->get($cid)) {
      $resource = $cache->data;
    else {
      // Otherwise consume API resource.
      $resource = $this->consume($dates, $type);

      // When resource was properly defined.
      if (!is_null($resource)) {

        // Then include it into cache.
        $this->cache->set($cid, $resource);

    // Return parsed value.
    return $resource;

Two important things about method above, I am using utility class from Drupal core: "Tags" that's the reason why I am including the use statement in order to import it properly: "Drupal\Component\Utility\Tags". Also I am using an internal method named "consume", it looks like this:

   * Consume API service by using given arguments.
   * @param array $dates
   * @param int $type
   * @return null|array
  protected function consume(array $dates, int $type) {
    // Extract date range.
    list($start_date, $end_date) = $dates;

    // Build query string.
    $params = [
      'tcIndicador' => $type,
      'tcFechaInicio' => $start_date,
      'tcFechaFinal' => $end_date,
      'tcNombre' => time(),
      'tnSubNiveles' => 'N',

    // Define endpoint URL.
    $query_string = http_build_query($params);
    $url = sprintf(
    //@TODO: I highly recommend to use configuration instead of URL hardcoded.

    // Init resource variable.
    $resource = NULL;

    try {
      // Consume service.
      $response = $this->httpClient->get($url);

      // Verify HTTP response code.
      if ($response->getStatusCode() == 200) {
        // Retrieve XML response content.
        $xml = $response->getBody()->getContents();

        // Extract content as an array.
        $resource = $this->parseXMLContent($xml);
      else {
        // Throw a runtime exception.
        throw New \RuntimeException(
          'Error while consuming API. Error code: ' . $response->getStatusCode()
    } catch (\RuntimeException $e) {
      // Show error message and log error message.
      watchdog_exception(__METHOD__, $e);

    return $resource;

Here I am using another internal method named "parseXMLContent" in order to parse XML content to an array, it looks as shown below:

   * Parse XML value from resource API.
   * @param string $xml
   * @return array
  protected function parseXMLContent(string $xml) {
    // Decode given XML content by using serializer.
    $resource = $this->serializer->decode($xml, 'xml');

    // Then parse it again to retrieve array value to iterate.
    $resource = $this->serializer->decode($resource['0'], 'xml');

    // Parse exchange rate information.
    $resource = array_map(function ($item) {
      /** @var DateTimePlus $date */
      $date = DateTimePlus::createFromFormat(DATE_ATOM, $item['DES_FECHA']);

      return [
        'date' => $date->format('d/m/Y'),
        'rate' => '₡' . number_format($item['NUM_VALOR'], 2),
    }, $resource['INGC011_CAT_INDICADORECONOMIC']);

    // Return value as an array.
    return $resource;

Here I am using "DateTimePlus" utility class from Drupal core, then I am importing it by including use statement at the top of the class: "Drupal\Component\Datetime\DateTimePlus" as well.

It should be good enough to consume our API, then let's go and see how a custom controller will be coded to expose retrieved exchange rate information. Believe it or not, we now may setup controller as service reason why I defined a service named "exchange_rate.controller" then we go to use it into our routing file as shown here:


  path: '/exchange-rate/bccr'
    _controller: 'exchange_rate.controller:content'
    _permission: 'access content'

Now we go to create our controller as a service, like this:



namespace Drupal\exchange_rate\Controller;

use Drupal\Component\Datetime\DateTimePlus;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Controller\ControllerBase;
use Drupal\exchange_rate\ExchangeRateManager;

 * Class ExchangeRateController
 * @package Drupal\exchange_rate\Controller
class ExchangeRateController extends ControllerBase {

   * @var \Drupal\exchange_rate\ExchangeRateManager
  protected $manager;

   * ExchangeRateController constructor.
   * @param \Drupal\exchange_rate\ExchangeRateManager $exchangeRateManager
  public function __construct(ExchangeRateManager $exchangeRateManager) {
    $this->manager = $exchangeRateManager;

  public function content() {
    // Init content variable and extract current timestamp.
    $content = [];
    $timestamp = time();

    /** @var DateTimePlus $startDate */
    $startDate = DateTimePlus::createFromTimestamp($timestamp)
      ->sub(\DateInterval::createFromDateString('10 day'));

    /** @var DateTimePlus $endDate */
    $endDate = DateTimePlus::createFromTimestamp($timestamp);

    // Setup resources types to consume.
    $resources = [
      317 => 'buying',
      318 => 'selling',
    foreach ($resources as $key => $value) {
      /** @var array $resource */
      $resource = $this->manager->getExchangeRates([
      ], $key);

      // Build up table type for each resource type.
      $content[$value] = [
        '#type' => 'table',
        '#attributes' => [
          'class' => [
        '#caption' => $this->t(
          Unicode::ucfirst($value) . ' rate'
        '#header' => [
        '#rows' => array_map(function ($item) {
          return [
        }, $resource),

    return $content;

If you browse to the new controller's URL ("/exchange-rate/bccr") then you should be able to see it in action.  Yay!!!


Last but not least I am using Drupal 8.7 version, it should be similar if you're using previous version but it is worth to let you know about it. I know, it seems pretty straight-forward by using Drupal 8, what I can say, it is what it is! Of course, if you're more like old really old school, then you still can use "SoapClient" php class, take a look here.

Hope it helps you at some point somehow!

PD: A demo is available here also I uploaded whole custom module to Github.


Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.