.Blog

Come interrogare un servizio esterno sviluppando un sito internet?

Come interrogare un servizio esterno sviluppando un sito internet?

Nello sviluppo di un sito più o meno complesso, può capitare di dover integrare la visualizzazione di contenuti forniti da un servizio esterno, senza la loro completa importazione. Il servizio può essere fornito da un diverso dipartimento della nostra azienda o da un ente esterno, può essere già esistente oppure deve essere completamente sviluppato. Insomma, i casi in cui ci ritroviamo ad operare posso essere i più diversi.

Immaginiamo, ad esempio, che nell'organizzazione per cui lavoriamo deve essere rinnovato uno dei siti sviluppati. Tra le altre cose, il sito avrà una sezione news in cui verranno mostrate notizie fornite dal sistema editoriale aziendale (applicazione terza rispetto al sito). Per fare questo sarà necessario interrogare le API del sistema editoriale (API che ancora deve essere sviluppata) per recuperare i dati della notizia quando un utente del sito vuole visualizzarla. Nell’articolo che segue vedremo un modo con cui risolvere situazioni di questo tipo.
L’articolo tratterà:

  • il modo con cui interrogare il servizio del sistema editoriale:
    • mock delle API del sistema editoriale con Apiary;
    • definizione del servizio Drupal con cui interrogare le API del sistema editoriale (questa operazione viene fatta usando il modulo http_client_manager);
  • il path del sito in cui visualizzare la news, definendo una coppia route/controller;
  • definizione del paramconverter con cui recuperiamo i dati della notizia dal sistema editoriale;
  • definizione del hook_theme per impostare il layout della notizia.

Tutto il codice verrà incluso in un modulo custom che chiameremo “External News”.

Come implementare l’API e recuperare i dati dal web service di una terza parte

Come abbiamo detto sopra, nella nostra organizzazione è il sistema editoriale che si occupa di generare contenuti di tipo news, i quali poi verranno visualizzati nel sito web. Le news dovranno essere esposte attraverso una API interrogabile da remoto. Le specifiche della API già esistono ma non la loro implementazione (che segue un percorso diverso rispetto allo sviluppo del sito). Per risolvere questo problema è possibile usare la piattaforma Apiary che permette, tra le altre cose, la definizione dei mock per la nostra API. L’utilizzo di Apiary è molto semplice e la definizione della API viene fatta con API Blueprint, un linguaggio di alto livello per la progettazione di API web. La documentazione per l’uso della nostra API è disponibile a questo indirizzo.

Ora dobbiamo occuparci del modo con cui recuperare i dati dal web service del sistema editoriale. In Drupal 8, interrogare API esterne può essere fatto con l’uso di Drupal::httpClient. Questo metodo restituisce un’istanza del client Guzzle (definita nel servizio del core http_client). Se il client http deve essere usato in più punti del nostro codice e deve interrogare più metodi del servizio remoto, può avere senso includere tutto in un servizio drupal in cui scrivere il codice di cui abbiamo bisogno. Esiste però un modulo che rende tutto questo molto semplice, per di più senza la necessità di scrivere del codice PHP: HTTP Client Manager. Tutto quello che dobbiamo fare è scrivere alcuni file di configurazione in json. Dalla pagina del modulo:

Http Client Manager introduces a new Guzzle based plugin which allows you to manage HTTP clients using Guzzle Service Descriptions via JSON files, in a simple and efficient way

Quindi http_client_manager permette di definire il modo con cui verrà interrogato il servizio esterno utilizzando il Guzzle service descriptions.
I passi da seguire per la configurazione di HTTP Cliente Manager sono i seguenti:

  1. nel nostro modulo (external_news) dobbiamo definire l’esistenza della API esterna creando il file external_news.http_services_api.yml e popolandolo come segue:
    news:
    title: "My Web portal - Servizio news"
    api_path: src/Plugin/RestService/news_service.json
    base_url: "https://my.editorial.service.it:1234"
    config:
    command.params:
    command.request_options:
    timeout: 32
    connect_timeout: 30
    # auth: ['my-user', 'my-password', 'Basic']

    Nel file json per il nostro servizio esterno viene definito: il machine_name, il titolo, il path del file in cui viene descritto e le eventuali credenziali per l'autenticazione;

  2. quindi, definiamo un servizio Drupal con cui recuperare il nostro client manager. Nel file external_news.services.yml scriviamo:
    services:
      external_news.http_client.news:
        parent: http_client_manager.client_base
        arguments: ['news']
    </code type="PHP">
    </li><li>per ultimo, scriviamo i <strong>file che descrivono il servizio</strong> ed i suoi metodi:
    <code type="PHP">
    // src/Plugin/RestService/news_service.json
    {
      "name": "My Web portal - Servizio news",
      "apiVersion": "2017-07-17 12:00",
      "description": "Services used by My Web portal.",
      "includes": [
        "news/news_detail.json"
      ]
    }

    // src/Plugin/RestService/news/news_detail.json
    {
      "operations": {
        "GetNews": {
          "httpMethod": "GET",
          "uri": "/services/getnews",
          "summary": "Get list of News",
          "parameters": {
            "nRighe": {
              "type": "integer",
              "location": "query",
              "description": "The total number of news to be retrieved",
              "required": false,
              "default": 4
            },
            "nPagina": {
              "type": "integer",
              "location": "query",
              "description": "The page number",
              "required": false,
              "default": 1
            },
            "idLingua": {
              "type": "integer",
              "location": "query",
              "description": "it = Italian, en = English",
              "required": false,
              "default": "it"
            },
            "stato": {
              "type": "string",
              "location": "query",
              "description": "The news state: DRAFT, PUBLISHED, UNPUBLISHED",
              "required": false,
              "default": "PUBLISHED"
            }
          }
        },
        "GetNewsDetail": {
          "httpMethod": "GET",
          "uri": "/services/news/{idNotizia}",
          "summary": "Get details for a single News",
          "parameters": {
            "idNotizia": {
              "type": "integer",
              "location": "uri",
              "description": "The news id",
              "required": false
            }
          }
        }
      },
      "models": {}
    }

    Per ciascuno dei metodi dell’API va indicato il metodo HTTP, la URI e gli eventuali parametri.

A questo punto la definizione del servizio è terminata. Dopo aver pulito la cache di Drupal, nella pagina di configurazione del modulo http_client_managet è possibile vedere sia la presenza della API :

che dei comandi associati:


Come definire la route a cui associare un Controller

Ora che abbiamo la possibilità di accedere alla API, possiamo occuparci di rendere visibili le news sul nostro sito. Possiamo farlo definendo una nuova route nel nostro modulo, a cui poi associare un Controller che si occuperà di generare il contenuto della pagina.
Nel modulo creiamo il nuovo file external_news.routing.yml in cui scriviamo la definizione della route:

external_news.news_content:
  path: '/news/{news}'
  defaults:
    _controller: '\Drupal\external_news\Controller\ExternalNewsContent::content'
  requirements:
    _role: 'anonymous+authenticated'
  options:
    parameters:
      news:
        type: 'external:news'

Nella route il punto da notare è nella definizione del parametro, viene usata la chiave ‘type’. Dalla documentazione su drupal.org leggiamo:

type: This is where we tell what entity to use…

La chiave type dice a Drupal quale entità dovrà caricare a partire dal valore del parametro per quella route. L’operazione di casting verrà fatta da un servizio apposito taggato come ‘paramconverter’, il cui id è uguale al valore della chiave type sul parametro (normalmente è il machine name dell’entità). Nel nostro caso, i dati della news vengono presi dal servizio esterno. Dovremo quindi scrivere un servizio Drupal custom di tipo Parameter Converter. Nel file external_news.services.yml aggiungiamo il seguente codice:

external:news:
  class: Drupal\external_news\ParamConverter\ExternalNewsParamConverter
  arguments: ['@external_news.http_client.news']
  tags:
    - { name: paramconverter }

Il servizio è implementato nella classe ExternalNewsParamConverter a cui viene passato via dependency injection il servizio con cui interrogare la nostra API esterna. La classe deve implementare l’interfaccia ParamConverterInterface che definisce due metodi:

  • public function convert($value, $definition, $name, array $defaults): per il casting del parametro;
  • public function applies($definition, $name, Route $route): per capire quando deve essere usato il servizio.

Nel servizio i due metodi vengono implementati come segue:

/**
   * {@inheritdoc}
   */

  public function applies($definition, $name, Route $route) {
    return (!empty($definition['type']) && $definition['type'] == 'external:news');
  }

/**
   * {@inheritdoc}
   */

  public function convert($value, $definition, $name, array $defaults) {
   
    if (empty($value)) {
      return NULL;
    }

    $co = \Drupal::cache()->get('external_news_' . $value);
    if (!empty($co)) {
      return $co->data;
    }

    $news = [];
    try {
      $result = $this->externalNewsHttpClientNews->call('GetNewsDetail', [
        'idNotizia' => (int) $value,
      ]);

      if ($result['success']) {
        /* items array has:
         *
         * 'numero_righe' => 4
         * 'pagina_visualizzata' => 1
         * 'totale_pagine' => 5
         * 'totale_righe' => 19
         * 'news' => array(4)
         *
         */

        $items = $result['items'];
        $news = array_pop($items['news']);
        \Drupal::cache()->set('external_news_' . $value, $news);
      }
    }
    catch (CurlException $e) {
      // If the call to 'GetNewsDetail' for some reason timeouts
      // log it as error (the News Detail page does not shown no data)
      $this->logger->error('Caught exception on call to organigramma: %m',
        [
          '%m' => $e->getMessage(),
        ]
      );
    }

    return $news;
  }

Il primo semplicemente verifica che nella definizione del parametro il tipo sia quello corretto: 'external:news', mentre il secondo verifica la presenza della news in cache e si comporta nel seguente modo:

  • nel caso in cui la notizia sia in cache, viene restituito il dato in cache;
  • in caso di cache miss viene usato il servizio ‘external_news.http_client.news’ per recuperare il dato, popolare la cache, quindi il restituire il dato.

Ora nel metodo ExternalNewsContent::content, che si occuperà del contenuto della pagina, avremo a disposizione tutti i dati di una news.

Il metodo del Controller in effetti è molto semplice, si occupa solo di restituire un rendered array che impacchetti i dati della news in modo corretto. Per questo, viene definito un nuovo template implementando l’hook_theme nel file external_news.module:

/**
 * Implements hook_theme().
 */

function external_news_theme() {
  return [
    'external_news' => [
      'variables' => [
        'news' => [],
      ],
    ],
  ];
}

E viene creato il file twig associato templates/external-news.html.twig:

{#
/**
 * @file
 * Theme override to display a node.
 *
 * Available variables:
 * - news: an array with key/value data for a news
 *     titolo: news title
 *     testo: body of the news
 *     data_pubblicazione: news publish date
 */

#}

<h1>{{ news.titolo }}</h1>
<span>{{ news.data_pubblicazione }}</span>
<div><p>{{ news.testo }}</p></div>

Il metodo ExternalNewsContent::content come abbiamo detto restituirà solo il renderer array:

/**
   * Content.
   *
   * @return string
   *   Return external_news rendered array.
   */

  public function content($news) {
    return [
      '#theme' => 'external_news',
      '#news' => $news,
      '#cache' => [
        'keys' => ['external_news_detail'],
        'contexts' => [],
        'tags' => [],
        'max-age' => 86400,
      ],
    ];
  }


Conclusione

A questo punto il modulo è terminato e noi abbiamo la possibilità di visualizzare le notizie del sistema editoriale sul nostro sito. Il codice del modulo è disponibile nel repo all’indirizzo: https://github.com/wellnet/external-news