23 Gennaio, 2019 | Di

Come integrare le Telegram Bot API in Drupal 8 (Parte 2)

Come integrare le Telegram Bot API in Drupal 8 (Parte 2)

Dove eravamo rimasti?

Nel precedente articolo (Parte 1) abbiamo creato un nuovo Servizio per Drupal (rendendolo noto al Service Container tramite il file drupalgram.services.yml) che svolge il ruolo di fornire un'interfaccia con le API dell'SDK Telegram che si è deciso di usare. Lo stato del modulo è, evidentemente, embrionale e necessita di essere integrato maggiormente per rientrare appieno nell'ecosistema dei moduli di Drupal 8.

Parentesi teorica

Prima di continuare con l'implementazione del nostro modulo potremmo soffermarci su una porzione di codice inerente al servizio TelegramAPI per una breve "parentesi teorica":

<?php
class TelegramAPI {
  // Proprietà che conterrà l'istanza dell'oggetto Api.
  private $telegram;
 
  function __construct() {
    $this->telegram = new Api(TELEGRAM_TOKEN);
  }
  ...

Nel corso della prima parte di questo tutorial ho fatto riferimento all'articolo "Come utilizzare il Service Container in Drupal 8 - Parte 1" e se l'avete letto avrete sicuramente incontrato il concetto di Dependency Injection. Tenetelo a mente, ne parliamo fra poco.

Tornando al codice qui sopra: cosa succede quando Drupal va a leggere il file drupalgram.services.yml e trova definito il servizio drupalgram.telegram_api?

Certamente tenterà di instanziarne un'oggetto. Come? Chiamando il costruttore della classe. In questo punto specifico accade una cosa che è progettualmente sbagliata: la nostra classe TelegramAPI dipende strettamente da un'altra: Api (ovvero la classe che serve a inizializzare l'SDK Telegram). È qui che il nostro servizio viola un principio importante per la sua stessa esistenza: la Dependency Injection.

Per capire perché l'attuale impostazione del codice è progettualmente errata, poniamoci una domanda: cosa succede se un altro sviluppatore (un contributore della community, un collega dello stesso team, ecc...) vuole utilizzare il nostro servizio sfruttando, per una sua esigenza, un'altro SDK o un'altra libreria PHP? Non può farlo! Se non riscrivendosene un altro, magari identico, che appunto sfrutta l'altra libreria, l'altra dipendenza. Abbiamo quindi reso non riutilizzabile, in quanto strettamente legato ad una specifica dipendenza, il nostro servizio. Sarebbe necessario, a tal proposito, riscrivere il codice del nostro servizio diversamente. Per questioni di tempo/praticità lasciamo il codice così com'è. Rimando comunque alla lettura dell'articolo citato sopra.

Continuando: Settings Form

Come abbiamo potuto constatare dal codice scritto nella prima parte del tutorial, abbiamo alcuni valori che risultano essere hardcoded: TELEGRAM_TOKEN e TELEGRAM_CHAT_ID. Questo significa che se chi utilizza il modulo vorrà cambiarli (ovviamente) dovrà farlo accedendo al codice sorgente della classe TelegramAPI... e questo fa venire la pelle d'oca solo a scriverlo. Sarebbe il caso, quindi, di fornire col nostro modulo una Settings Form raggiungibile tramite un determinato URL path, che nel gergo MVC è chiamata route (o rotta).

Attenendoci sempre alle specifiche dello standard PSR4 adottato da Drupal 8 (il mapping lo troviamo qui) le classi che implementano Form andranno necessariamente scritte all'interno della cartella src/Form, all'interno della root del modulo (nel nostro caso drupalgram/src/Form). Creiamo qui un file (che corrisponderà esattamente al nome della classe al suo interno definita) chiamato DrupalgramSettingsForm.php.

<?php
namespace Drupal\drupalgram\Form;
 
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\ConfigFormBase;
 
class DrupalgramSettingsForm extends ConfigFormBase {
  // ID della form.
  public function getFormId() {
    return 'drupalgram.settings';
  }
  // Nome delle configurazioni modificabili
  protected function getEditableConfigNames() {
    return [
      'drupalgram.settings'
    ];
  }
 
  public function buildForm(array $form, FormStateInterface $form_state) {
    $configuration = $this->configFactory()->get('drupalgram.settings');
    // Definisco campo di tipo "textfield" (vedi Form API di Drupal 8)
    $form['telegram_token'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Telegram Bot Token'),
      '#description' => $this->t('Bot token, obtained (from @BotFather telegram bot) during its creation.'),
      '#default_value' => $configuration->get('telegram_token')
    ];
    // Definisco campo di tipo "textfield" (vedi Form API di Drupal 8)
    $form['telegram_chat_id'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Telegram Chat ID'),
      '#description' => $this->t('Chat ID where bot sends messages.'),
      '#default_value' => $configuration->get('telegram_chat_id'),
    ];
    // Finalizzo la form chiamando il buildForm di ConfigFormBase
    // il quale aggiunge, per esempio, il pulsante di submit collegato
    // al metodo $this->submitForm
    return parent::buildForm($form, $form_state);
  }
 
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Chiedo al configFactory di restituirmi l'oggetto Config editabile,
    // così come chiamato in $this->getEditableConfigNames(). In questo modo
    // potrò modificare, appunto, i valori delle configurazioni serializzate su DB alla chiave drupalgram.settings.
    $configuration = $this->configFactory()->getEditable('drupalgram.settings');
    // Salvo i valori immessi tramite la form sul config's storage (DB).
    $configuration->set('telegram_token', $form_state->getValue('telegram_token'));
    $configuration->set('telegram_chat_id', $form_state->getValue('telegram_chat_id'));
    // Finalizzo con save()
    $configuration->save();
    // In fine passo l'array form alla submitForm() della classe che stiamo estendendo
    // (ConfigFormBase) per richiamare eventuali suoi comportamenti
    // (in questo caso stampa un messaggio di stato sull'avvenuto salvataggio)
    parent::submitForm($form, $form_state);
  }
}

Precisamente abbiamo:

  • Esteso la classe ConfigFormBase;
  • Aggiunti due campi (vedi la documentazione riguardanti le Form API) alla form tramite il metodo buildForm;
  • Gestito il salvataggio, nel Config Storage (DB), dei valori immessi, tramite il metodo submitForm.

Abbiamo ora la nostra form di configurazione. Vediamo, dunque, come renderla raggiungibile tramite un opportuno URL path.

Routing in Drupal 8

Per chi proviene da Drupal 7 ricorderà bene in che modo venivano gestite le risposte da fornire in base a specifiche richieste: hook_menu. Questo permetteva, nel caso più basilare, di istruire Drupal 7 rispetto a quale callback chiamare nel momento in cui si visitava uno specifico URL path. Ad esempio:

// D7 
// https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_menu/7.x
function drupalgram_menu() {
    $items['abc/def'] = array(
        'page callback' => 'drupalgram_test_callback',
    );
    return $items;
}

Con questo codice si mappava sul path abc/def (es: https://ilmiosito.com/abc/def) la risposta restituita dall'esecuzione della callback drupalgram_test_callback.

In Drupal 8 il vecchio hook_menu e le logiche ad esso legate (così come molte altre componenti del core Drupal) sono state rimpiazzate dalla componente di Symfony chiamata Routing. Essendo Symfony un framework MVC, le rotte vengono gestite non più da un semplice callback ma da un Controller (un oggetto più complesso, all'interno del quale è definito il metodo che genererà la risposta adatta alla visita di quella rotta).

Definire una rotta in Drupal 8

Le rotte (similmente a quanto fatto per definire un nuovo servizio) vanno definite all'interno di un file chiamato nomemodulo.routing.yml. Nel nostro caso, quindi: drupalgram.routing.yml. In questo caso, ad intervenire nella gestione della rotta non sarà un Controller bensì la nostra Form, della quale indicheremo il percorso completo per raggiungerla: \Drupal\drupalgram\Form\DrupalgramSettingsForm.

drupalgram.config_form:
  path: '/admin/drupalgram/config'
  defaults:
    _form: '\Drupal\drupalgram\Form\DrupalgramSettingsForm'
    _title: 'Drupalgram settings'
  requirements:
    _permission: 'administer site configuration'

Qui sopra:

  • abbiamo definito una rotta (avente ID drupalgram.config_form)
  • l'abbiamo mappata sul path '/admin/drupalgram/config'
  • la rotta riceverà come valori di defaults i seguenti:
    • _form: ovvero la form che verrà renderizzata per risolvere tale richiesta 
    • _title:  titolo della pagina visualizzata
  • abbiamo, infine, specificato dei requisiti richiesti, nel nostro caso:
    • _permission: il/i permesso/i di cui l'utente deve disporre per accedere alla rotta

Dopo aver pulito le cache (operazione necessaria), dovremmo poter visitare (purchè loggati con un profilo che abbia come permesso 'administer site configuration') il path /admin/drupalgram/config e vedere, come risultato, la pagina che renderizza la nostra form. 

Drupalgram Settings Form (route: /admin/drupalgram/path)
La pagina risponde all'URL path /admin/drupalgram/path.

Avendo gestito il submitForm potremo impostare qui i valori per TELEGRAM_TOKEN e TELEGRAM_CHAT_ID e "fissarli" nel Config Storage. A questo punto non ci resta che usare tali valori nel nostro servizio.

Preleviamo i valori dal Config Storage

Per usare i valori che abbiamo salvato nel Config Storage all'interno del nostro servizio TelegramAPI dovremo innanzitutto modificare quest'ultimo affinché non ci siano più quei due valori (il token ed il chat_id) cablati nel codice. Quindi eliminiamo quei "define" alla riga 10 e 13 (e relativi commenti).  Creiamo due proprietà (protected) della classe TelegramAPI le quali valorizzeremo, nel __consturct, con i valori che andremo a prendere dal config storage. Il codice finale, sarà più o meno:

<?php
 
namespace Drupal\drupalgram\Services\TelegramAPI;
use Telegram\Bot\Api;
 
/**
 * Class TelegramAPI
 *
 * La classe TelegramAPI implementerà le funzioni che
 * richiameranno le API del Telegram Bot API SDK
 */
class TelegramAPI {
  // Proprietà che conterrà l'istanza dell'oggetto Api.
  private $telegram;
  protected $TELEGRAM_TOKEN, $TELEGRAM_CHAT_ID;
 
  function __construct() {
    $moduleSettings = \Drupal::configFactory()->get('drupalgram.settings');
    $this->TELEGRAM_TOKEN = $moduleSettings->get('telegram_token');
    $this->TELEGRAM_CHAT_ID = $moduleSettings->get('telegram_chat_id');
 
    $this->telegram = new Api($this->TELEGRAM_TOKEN);
  }
 
  /**
   * Il seguente metodo sarà quello che si occuperà di recapitarci
   * il messaggio su telegram!
   *
   * Il parametro $message conterrà il testo da inviarci
   * Il parametro $parse_mode si riferisce alla possibilità di
   * passare del testo formattato secondo le regole del linguaggio
   * di markup Markdown
   */
  public function sendMessage($message, $parse_mode = 'MARKDOWN') {
    $this->telegram->sendMessage([
      'chat_id' => $this->TELEGRAM_CHAT_ID,
      'text' => $message,
      'parse_mode' => $parse_mode,
    ]);
  }
  ...

Conclusione

Abbiamo finalmente reso configurabile il nostro modulo! Volendo testare il corretto funzionamento del servizio, possiamo eseguire il seguente codice nella form "Execute PHP" (pagina visibile al path /devel/php solo se installato il modulo devel):

\Drupal::service('drupalgram.telegram_api')->sendMessage('Prova messaggio');

Se tutto funziona, come funzionava per la Parte 1, vuol dire che ci siamo.

Nella terza, ed ultima, parte di questa serie di tutorial implementeremo un blocco che espone una form di contatto al submit della quale il nostro bot ci notificherà tramite messaggio privato su telegram dell'azione svolta tramite il nostro sito.