26 Giugno, 2017 | Di Wellnet

Drupal 8: consigli per recuperare una relazione

Drupal 8: consigli per recuperare una relazione

Il problema

Qualche giorno fa ho sviluppato un modulo per Drupal 8 e ho avuto a che fare per la prima volta con le relazioni tra Content Entity (Bundleless), rappresentate tramite Entity Reference. Ho dato un rapido sguardo agli strumenti offerti da Drupal 8 e mi son reso conto di quanto segue, supponendo che l'entità A abbia una entity reference verso l'entità B (A->B):

  1. non esistono strumenti per navigare facilmente da B ad A;
  2. l'IDE spesso non è in grado di darci un valido suggerimento anche quando tentiamo di navigare nel verso giusto (cioè di recuperare B a partire da A).

Per il punto 2, se volessimo ad esempio recuperare l'autore di un nodo, potremmo utilizzare uno snippet di questo tipo:

<?php
...
$node = \Drupal\node\Entity\Node::load(280);
...
... = $node->uid->entity->name->value);
...

L'aspetto più negativo di questo codice è che non saprei quale plugin installare sull'IDE per far sì che mi suggerisca la sequenza: $node->uid->entity->name->value.

Un tentativo di risoluzione: le classi astratte implementate

Per tentare di risolvere i due problemi sopra esposti, ho realizzato un piccolo prototipo per dimostrare come si possa agevolare la navigazione delle relazioni nei due versi, semplificando la scrittura e al tempo stesso mettendo in moto il suggeritore dell'IDE. Il protototipo è stato realizzato per semplicità implementando tutte le componenti nello stesso modulo ed utilizzando Drupal Console per la generazione automatica del codice.

Ho definito nel modulo le entità che implementano il diagramma delle classi allegato. Insomma il solito esempio un po' sgangherato e poco realistico, del tenore di quelli che si possono trovare in qualunque manuale di base di OOP, ma abbastanza semplice da aiutarci a fissare le idee nello sviluppo dei componenti essenziali. Dopo aver generato il modulo, le entità e il CRUD con Drupal Console, ho implementato un adattatore che viene richiamato dalla classe base delle entità del modulo. In un successivo refactoring del prototipo, l'adattatore e la classe base potranno andare a far parte di un componente riutilizzabile. Vediamo in dettaglio la definizione della classe base, delle entità e dell'adattatore.

<?php
namespace Drupal\auto\Entity;
 
use Drupal\Core\Entity\ContentEntityBase;
 
/**
 * Class ContentEntityExtended.
 */
abstract class ContentEntityExtended extends ContentEntityBase {
 
  /**
   * Adapter.
   * 
   * @var \Drupal\auto\Adapter\AdapterBase 
   */
  protected $adapter;
 
  /**
   * Classe concreta dell'adattatore
   * 
   * @return string
   */
  abstract protected function adapterClass();
 
  /**
   * {@inheritdoc}
   */
  public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = array()) {
    parent::__construct($values, $entity_type, $bundle, $translations);
    $adapterClass = $this->adapterClass();
    $this->adapter = new $adapterClass($this);
  }
 
  /**
   * {@inheritdoc}
   */
  public function &__get($name) {
    if ($name == 'adapter') {
      return $this->adapter;
    }
    return parent::__get($name);
  }
 
  /**
   * @return \Drupal\auto\Adapter\AdapterBase
   */
  public function getAdapter() {
    return $this->adapter;
  }
 
}

La classe base è una semplice estensione di ContentEntityBase, che fa parte del core di Drupal 8. Ecco una rapida carrellata dei metodi definiti e/o implementati:

  • il metodo astratto adapterClass costringe le sottoclassi (entità) a dichiarare l'adattatore concreto che dovrà essere utilizzato da ognuna di esse;
  • il metodo magico __get viene sovrascritto per consentire l'accesso diretto all'adattatore (quando, nel seguito, commenteremo le entità concrete, vedremo che il tipo dell'adattatore concreto verrà suggerito all'IDE da una annotazione @property apposta nell'entità concreta); da ciò consegue, tra l'altro, che un campo non potrà chiamarsi adapter.

Vediamo l'adattatore astratto:

<?php
namespace Drupal\auto\Adapter;
 
use Drupal\auto\Entity\ContentEntityExtended;
use Drupal\Core\Entity\EntityTypeManagerInterface;
 
/**
 * Class AdapterBase.
 */
abstract class AdapterBase {
 
  const HAS_ONE_REFERENCED = 1;
 
  const HAS_ONE_REFERENCING = 2;
 
  const HAS_MANY_REFERENCED = 10;
 
  const HAS_MANY_REFERENCING = 20;
 
  /**
   * Entità da adattare.
   * 
   * @var \Drupal\auto\Entity\ContentEntityExtended
   */
  protected $entity;
 
  public function __construct(ContentEntityExtended $entity) {
    $this->entity = $entity;
  }
 
  /**
   * Implementazione del metodo magico get.
   */
  public function __get($name) {
    if ($name == 'entity') {
      return $this->entity;
    }
    $relationships = $this->relationships();
    if (isset($relationships[$name])) {
      $relationship = $relationships[$name];
      switch ($relationship[0]) {
        case static::HAS_ONE_REFERENCED:
          return $this->getSingleReferenced($name);
          break;
        case static::HAS_ONE_REFERENCING:
          return $this->getSingleReferencing($name,  $relationship[1]);
          break;
        case static::HAS_MANY_REFERENCED:
          return $this->getMultipleReferenced($name);
          break;
        case static::HAS_MANY_REFERENCING:
          return $this->getMultipleReferencing($name, $relationship[1]);
          break;
      }
    }
    else {
      $attributes = $this->attributes();
      if (isset($attributes[$name]) && !empty($attributes[$name]['multiple'])) {
        return array_map(function ($val) { return $val['value']; }, $this->entity->get($name)->getValue());
      }
      else {
        return $this->entity->get($name)->getString();
      }
    }
  }
 
  /**
   * Restituisce l'elenco degli attributi dell'entità adattata.
   * 
   * @return array
   */
  protected function attributes() {
    return [];
  }
 
  /**
   * Restituisce l'elenco delle relazioni dell'entità adattata.
   * 
   * @return array
   */
  protected function relationships() {
    return [];
  }
 
  /**
   * Restituisce una entità singola riferita.
   * 
   * @param string $entity_id
   * @return \Drupal\auto\Adapter\AdapterBase|NULL
   */
  protected function getSingleReferenced($entity_id) {
    $return = NULL;
    if ($firstReferenced = $this->entity->get($entity_id)->first()) {
      $return = $firstReferenced->get('entity')->getTarget()->getValue()->getAdapter();
    }
    return $return;
  }
 
  /**
   * Restituisce una entità singola che si riferisce a quella dell'adattatore.
   * 
   * @param string $entity_id
   * @param string $referencing_field
   * @return \Drupal\auto\Adapter\AdapterBase|NULL
   */
  protected function getSingleReferencing($entity_id, $referencing_field) {
    $return = NULL;
    $searchValues = [
      $referencing_field => $this->entity->id(),
    ];
    $entityList = \Drupal::service('entity_type.manager')->getStorage($entity_id)->loadByProperties($searchValues);
    if (!empty($entityList)) {
      $entity = array_shift($entityList);
      $return = $entity->getAdapter();
    }
    return $return;
  }
 
  /**
   * Restituisce una lista di entità riferite.
   * 
   * @param string $entity_id
   * @return \Drupal\auto\Adapter\AdapterBase[]
   */
  protected function getMultipleReferenced($entity_id) {
    $return = [];
    foreach ($this->entity->get($entity_id) as $referenced) {
      $return[] = $referenced->get('entity')->getTarget()->getValue()->getAdapter();
    }
    return $return;
  }
 
  /**
   * Restituisce una lista do entità che si riferiscono a quella dell'adattatore.
   * 
   * @param string $entity_id
   * @param string $referencing_field
   * @return \Drupal\auto\Adapter\AdapterBase[]
   */
  protected function getMultipleReferencing($entity_id, $referencing_field) {
    $return = [];
    $searchValues = [
      $referencing_field => $this->entity->id(),
    ];
    $entityList = \Drupal::service('entity_type.manager')->getStorage($entity_id)->loadByProperties($searchValues);
    if (!empty($entityList)) {
      foreach ($entityList as $entity) {
        $return[] = $entity->getAdapter();
      }
    }
    return $return;
  }
 
}

Lasciando senza commento l'implementazione dei getter, in quanto ha puro scopo dimostrativo delle funzionalità dell'adattatore, faccio notare quanto segue:

  1. le relazioni dell'entità sono dichiarate nel metodo relationships; ciò consente di non fare discovery delle definizioni delle entità tutte le volte che si deve accedere, tramite una relazione, ad entità riferite o referenzianti;
  2. i tipi di relazioni sono definiti nelle costanti; una relazione può essere singola o multipla, e tramite essa è possibile reperire un'entità referenziata o referenziante;
  3. il metodo magico __get dell'adattatore astratto è in grado di restituire anche i campi di tipi base(singoli o multipli);
  4. i getter restituiscono sempre l'adattatore dell'entità, in modo da poter invocare gli adattatori in sequenza (eventualmente se si volesse ottenere l'entità, si può procedere a ritroso, invocando $adapter->entity);
  5. il metodo attributes al momento serve solo per dichiarare i campi di tipo base multipli.

Un tentativo di risoluzione: le classi concrete implementate

A questo punto possiamo analizzare le entità e gli adattatori concreti. Il codice completo del modulo è scaricabile qui. Commentiamo rapidamente solo uno snippet tratto da un'entità concreta e un'adattatore concreto, per poi muoverci rapidamente all'applicazione di quanto sviluppato. Partiamo dall'entità concreta (Auto):

<?php
...
* @property \Drupal\auto\Adapter\AutoAdapter $adapter
 */
class Auto extends ContentEntityExtended implements AutoInterface {
  use EntityChangedTrait;
 
  /**
   * {@inheritdoc}
   */
  protected function adapterClass(): string {
    return '\Drupal\auto\Adapter\AutoAdapter';
  }
...

Nello snippet precedente possiamo vedere le modifiche fatte al codice generato da Drupal Console(oltre all'ovvia aggiunta della definizione dei campi):

  • ContentEntityExtended al posto della classe base predefinita;
  • viene implementato il metodo adapterClass;
  • viene aggiunta l'annotazione @property \Drupal\auto\Adapter\AutoAdapter $adapter (essenziale, altrimenti non funzionerebbe il suggeritore dell'IDE).

Vediamo anche un esempio di implementazione dell'adattatore concreto dell'Auto:

<?php
namespace Drupal\auto\Adapter;
 
/**
 * Class AutoAdapter.
 * 
 * @property string $marca
 * @property string $modello
 * @property string[] $date_tagliandi
 * @property \Drupal\auto\Adapter\VolanteAdapter $volante
 * @property \Drupal\auto\Adapter\RuotaAdapter[] $ruote
 * @property \Drupal\auto\Adapter\ConducenteAdapter[] $conducenti
 */
class AutoAdapter extends AdapterBase {
 
  /**
   * {@inheritdoc}
   */
  protected function attributes() {
    return [
      'date_tagliandi' => [
        'multiple' => TRUE,
      ],
    ];
  }
 
  /**
   * {@inheritdoc}
   */
  protected function relationships() {
    return[
      'volante' => [
        static::HAS_ONE_REFERENCING,
        'auto',
      ],
      'ruote' => [
        static::HAS_MANY_REFERENCED,
      ],
      'conducenti' => [
        static::HAS_MANY_REFERENCED,
      ],
    ];
  }
 
}

Anche qui faccio notare rapidamente:

  • il metodo attributes viene utilizzato per dichiarare staticamente i campi di tipo base multipli;
  • le relazioni REFERENCING hanno un parametro aggiuntivo rispetto alle REFERENCED; nell'esempio stiamo cercando le entità che hanno nome macchina volante il cui campo auto si riferisce all'auto dell'adattatore;
  • le annotazioni @property sono il fulcro del meccanismo implementato del suggeritore dell'IDE.

Ma sviluppare gli adattatori costa?

Ancora un attimo di pazienza prima di passare all'applicazione di quanto sviluppato, perché possiamo già concludere quanto segue:

  1. esistono sicuramente altri modi per implementare gli adattatori e le entità concrete, ad esempio si potrebbe totalmente evitare di implementare nuovi metodi ed adottare un approccio basato solo su annotazioni;
  2. qualunque approccio si segua, il codice dell'adattatore può essere generato facilmente definendo e implementando un nuovo comando ad hoc di Drupal Console;
  3. qualunque approccio si segua, il codice dell'entità può essere generato facilmente estendendo le funzionalità del comando di generazione delle entità di Drupal Console.

Evidentemente le ultime due conclusioni sono particolarmente importanti, visto che affermano, in pratica, che l'adozione dell'adattatore sviluppato non richiede nessun effort aggiuntivo da parte del programmatore, ma solo l'utilizzo di due nuovi comandi molto semplici da sviluppare.

Mettiamo al lavoro l'IDE!

E finalmente possiamo passare all'applicazione! In questo prototipo abbiamo implementato solo la Controller, ma dovrebbero essere evidenti i vantaggi dell'approccio seguito in tutti gli strati seguenti:

  1. logica di business;
  2. logica della controller;
  3. strato di view.

Vediamo l'implementazione della controller, solo per ribadire che tutti gli accessi a relazioni o campi di tipi base semplici o multipli sono suggeriti dalla funzione di autocompletamento dell'IDE:

<?php
namespace Drupal\auto\Controller;
 
use Drupal\auto\Entity\Auto;
use Drupal\auto\Entity\Volante;
use Drupal\auto\Entity\Conducente;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Response;
 
/**
 * Class AutoController.
 */
class AutoController extends ControllerBase {
 
  /**
   * Una semplice vista dell'auto.
   * 
   * @param \Drupal\auto\Entity\Auto $auto
   * @return \Symfony\Component\HttpFoundation\Response
   */
  public function viewAuto(Auto $auto) {
    $content = [];
 
    $content[] = 'Marca: ' . $auto->adapter->marca;
    $content[] = 'Modello: ' . $auto->adapter->modello;
    $content[] = 'Date tagliandi: ' . implode(', ', $auto->adapter->date_tagliandi);
 
    $content[] = '';
 
    $content[] = 'Questa auto monta il volante:';
    $content[] = 'Rivestimento: ' . $auto->adapter->volante->rivestimento;
 
    $content[] = '';
 
    $content[] = 'Questa auto monta le ruote:';
    foreach ($auto->adapter->ruote as $ruotaAdapter) {
      $content[] = 'Nome ruota: ' . $ruotaAdapter->name . ', raggio: ' . $ruotaAdapter->raggio;
    }
 
    $content[] = '';
 
    $content[] = 'Questa auto è guidata dai seguenti conducenti:';
    foreach ($auto->adapter->conducenti as $conducenteAdapter) {
      $content[] = 'Nome: ' . $conducenteAdapter->nome . ' Cognome: ' . $conducenteAdapter->cognome;
    }
 
    $content = implode('<br>', $content);
    return new Response($content);
  }
 
  /**
   * Una semplice vista del volante.
   * 
   * @param \Drupal\auto\Entity\Volante $volante
   * @return \Symfony\Component\HttpFoundation\Response
   */
  public function viewVolante(Volante $volante) {
    $content = [];
 
    $autoAdapter = $volante->adapter->auto;
 
    $content[] = 'Il volante è montato su questa auto: ';
    $content[] = 'Marca: ' . $autoAdapter->marca;
    $content[] = 'Modello: ' . $autoAdapter->modello;
    $content[] = 'Date tagliandi: ' . implode(', ', $autoAdapter->date_tagliandi);
 
    $content[] = '';
 
    $content[] = 'Rivestimento volante: ' . $volante->adapter->rivestimento;
 
    $content = implode('<br>', $content);
    return new Response($content);
  }
 
  /**
   * Una semplice vista del conducente.
   * 
   * @param \Drupal\auto\Entity\Conducente $conducente
   * @return \Symfony\Component\HttpFoundation\Response
   */
  public function viewConducente(Conducente $conducente) {
    $content = [];
 
    $content[] = 'Conducente: ';
    $content[] = 'Nome: ' . $conducente->adapter->nome;
    $content[] = 'Cognome: ' . $conducente->adapter->cognome;
    $content[] = 'Codice fiscale: ' . $conducente->adapter->codice_fiscale;
 
    $content[] = '';
 
    $content[] = 'Questa conducente guida le seguenti auto:';
    foreach ($conducente->adapter->auto as $autoAdapter) {
      $content[] = 'Marca: ' . $autoAdapter->marca . ', modello: ' . $autoAdapter->modello;
    }
    $content = implode('<br>', $content);
    return new Response($content);
  }
 
}

Conclusioni

Gli sviluppi futuri dell'adattatore, oltre a tutti i refactoring necessari al miglioramento delle performance e alla corretta scrittura (e testabilità) delle classi sviluppate, potrebbero riguardare principalmente, come già detto in precedenza:

  1. creazione di un componente specifico in cui racchiudere il codice riutilizzabile;
  2. estensione del comando di Drupal Console per la generazione delle entità;
  3. creazione del comando di Drupal Console per la generazione degli adattatori concreti.

Chi (?) mi ha sopportato fin qui, avrà certamente notato che abbiamo parlato solo di recupero nel senso di retrieve di entità relazionate tra loro, non abbiamo parlato di metodi di ricerca e neppure dell'implementazione del metodo magico __set dell'adattatore (ma di questo magari parleremo in un'altro articolo, che so Drupal 8: consigli per salvare una relazione), né dei metodi __isset e __clone...

Uhm, solo nel finale mi accorgo però di aver scelto dei titoli che tradiscono il misto di dolore dell'uomo e di sofferenza del programmatore e mi sorge spontanea la domanda: se il core di Drupal 8 in un futuro (magari non lontano) implementerà strumenti di supporto come quello qui prototipato, potrà aiutarci a tenere a mente più facilmente anche l'importanza delle nostre (umane) relazioni ed aiutarci dunque a viverle meglio? Ai posteri l'ardua sentenza. :)

Wellnet
Wellnet

Wellnet è una nuova realtà nel panorama delle agenzie digitali italiane: 70 expertise complementari e integrate, con sedi a Milano, Torino Cuneo e Modena, frutto della fusione di tre realtà di successo preesistenti.

Potrebbe interessarti: