Plugin API

La struttura iniziale di un block plugin può essere generata con il comando Drush drush generate block.

Plugin Type

Sono esempi di plugin:

  • blocchi
  • field type
  • field widget
  • field formatter
  • image effect
  • action (esegue un'operazione)
  • condition (valuta se una condizione è soddisfatta)
  • queue worker (elabora gli elementi di una queue)
  • migration source, process e destination
  • Views handler
  • CKEditor plugin

Tutti i plugin appartenenti allo stesso plugin type svolgono lo stesso genere di funzione e rispettano un contratto comune, ma possono implementare comportamenti differenti.

Per esempio, tutti i block plugin:

  • possono essere individuati dal Block Plugin Manager
  • implementano BlockPluginInterface
  • devono produrre un render array attraverso build()
  • possono avere una configurazione
  • possono definire regole di accesso
  • possono dichiarare cache metadata

Plugin interface

Ogni plugin type dispone di un'interfaccia che definisce i comportamenti comuni, ad esempio i block plugin implementano Drupal\Core\Block\BlockPluginInterface e quindi hanno un metodo build() che restituisce il render array del blocco.

Di solito per creare un blocco si estende la classe Drupal\Core\Block\BlockBase.

Base class

La base class contiene il comportamento comune del plugin type, ad esempio per un blocco la classe base da estendere è BlockBase, che mette a disposizione i metodi:

  • build() obbligatorio nella classe figlia
  • defaultConfiguration()
  • blockForm()
  • blockValidate()
  • blockSubmit()
  • blockAccess()
  • getCacheTags()
  • getCacheMaxAge()

Plugin implementation

E' la classe concreta appartenente a un determinato plugin type.

Plugin definition

E' l'insieme dei metadata che definiscono il plugin e che vengono dichiarati con un attributo della classe:

#[Block( 
  id: 'nome_modulo_forecast',  // plugin ID
  admin_label: new TranslatableMarkup('Weather forecast'), 
  category: new TranslatableMarkup('Nome Modulo'), 
)]

Plugin discovery

La discovery è il processo attraverso il quale il plugin manager individua le implementazioni disponibili.

I plugin possono essere scoperti attraverso meccanismi differenti, tra cui:

  • PHP attributes (metodo moderno)
  • annotation (metodo precedente ma ancora usato)
  • YAML
  • hook
  • definizioni statiche
  • meccanismi custom

La modalità dipende dal plugin type, ad esempio i block plugin moderni vengono individuati cercando classi:

  1. nel namespace Plugin\Block
  2. all'interno della directory src/Plugin/Block
  3. contrassegnate con l'attributo #[Block]

Plugin manager

Il plugin manager:

  • individua le implementazioni
  • legge le plugin definition
  • memorizza le definition in cache
  • permette di elencare i plugin disponibili
  • crea le istanze
  • applica eventuali alter hook
  • gestisce la factory del plugin type

Custom block content

I blocchi di contenuto creati dall'interfaccia alla pagina /admin/content/block sono entità di contenuto del tipo block_content, non sono la stessa cosa di un block plugin scritto in PHP.

Esempio completo nel file src/Plugin/Block/ForecastBlock.php:

<?php

declare(strict_types=1);

namespace Drupal\nome_modulo\Plugin\Block;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\nome_modulo\Service\ForecastClientInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/** 
 * Provides a configurable weather forecast block. 
 */ 
#[Block(
    id: 'nome_modulo_forecast', 
    admin_label: new TranslatableMarkup('Weather forecast'), // nome mostrato nella pagina per piazzare i blocchi nelle regioni
    category: new TranslatableMarkup('Nome Modulo'), //per raggruppare i plugin nella UI
)] 
final class ForecastBlock extends BlockBase implements ContainerFactoryPluginInterface {

    /** 
     * Default number of days displayed by an extended block. 
     */ 
    private const DEFAULT_NUMBER_OF_DAYS = 3;

    /** 
     * * Maximum number of days displayed by a block. 
     */ 
    private const MAX_NUMBER_OF_DAYS = 5;

    /** 
     * Forecast cache lifetime in seconds. 
     */ 
    private const CACHE_MAX_AGE = 1800; //1800 secondi cioè 30 minuti

    /** 
     * Constructs a ForecastBlock object. 
     */ 
    public function __construct( // la base class richiede che nel costruttore vengano passati i primi tre parametri
        array $configuration, 
        $plugin_id, 
        $plugin_definition, 
        private readonly ForecastClientInterface $forecastClient, // il servizio per recuperare le previsioni
    )
    {
        parent::__construct(
            $configuration, 
            $plugin_id, 
            $plugin_definition,
        );
    }

    /** 
     * {@inheritdoc} 
     */ 
    public static function create(
        ContainerInterface $container, 
        array $configuration, 
        $plugin_id, 
        $plugin_definition,): static
    {
        return new static(
            $configuration, 
            $plugin_id, 
            $plugin_definition, 
            $container->get(ForecastClientInterface::class),
        );
    }

    /** 
     * {@inheritdoc} 
     */ 
    public function defaultConfiguration(): array {
        return [
            'display' => 'summary', 
            'number_of_days' => self::DEFAULT_NUMBER_OF_DAYS, 
            'show_link' => TRUE,
        ] + parent::defaultConfiguration(); // è importante conservare anche la configurazione della base class
    }

    /** 
     * {@inheritdoc} 
     */ 
    public function blockForm( // la base class fornisce già il form generale del blocco ma vanno aggiunti i campi custom
        $form, 
        FormStateInterface $form_state,
    ): array
    {
        $form = parent::blockForm($form, $form_state); // importante per conservare i valori della base class

        $form['display'] = [ // primo campo custom per la modalità di visualizzazione
            '#type' => 'radios', 
            '#title' => $this->t('Display mode'), 
            '#options' => [
                'summary' => $this->t('Summary'), 
                'extended' => $this->t('Extended'),
            ], 
            '#default_value' => $this->configuration['display'], 
            '#required' => TRUE,
        ];

        $form['number_of_days'] = [ // secondo campo custom per il numero di giorni
            '#type' => 'number', 
            '#title' => $this->t('Number of days'), 
            '#description' => $this->t(
                'Used only when the extended display mode is selected.',
            ), 
            '#default_value' => $this->configuration['number_of_days'], 
            '#min' => 1, 
            '#max' => self::MAX_NUMBER_OF_DAYS, 
            '#required' => TRUE, 
            // il campo number_of:days deve essere mostrato solo se è selezionata la modalità extended
            // gli staes sono visible, invisible, enabled, disabled, required, optional, checked, unchecked, expanded, collapsed
            '#states' => [
                'visible' => [':input[name="settings[display]"]' => [
                    'value' => 'extended',
                    ],
                ],
            ],
        ];

        $form['show_link'] = [ // terzo campo custom per la presenza del link
            '#type' => 'checkbox', 
            '#title' => $this->t('Show a link to the full forecast'), 
            '#default_value' => $this->configuration['show_link'],
        ];

        return $form;
    }

    /** 
     * {@inheritdoc} 
     */ 
    public function blockValidate(
        $form, 
        FormStateInterface $form_state,): void {

        parent::blockValidate($form, $form_state);

        $display = $form_state->getValue('display');

        if (!in_array($display, ['summary', 'extended'], TRUE)) { // verifica che display abbia uno dei valoria mmessi
            $form_state->setErrorByName(
                'display', $this->t('Select a valid display mode.'),
            );
        }

        $numberOfDays = (int) $form_state->getValue(
            'number_of_days',
        );

        if ( // verifica che il numero di giorni sia compreso tra 1 e 5
            $numberOfDays < 1 || 
            $numberOfDays > self::MAX_NUMBER_OF_DAYS
        ) {
            $form_state->setErrorByName(
                'number_of_days', $this->t(
                    'The number of days must be between 1 and @maximum.', 
                    [
                        '@maximum' => self::MAX_NUMBER_OF_DAYS,
                    ],
                ),
            );
        }
    }

    /** 
     * {@inheritdoc} 
     */ 
    public function blockSubmit(
        $form, 
        FormStateInterface $form_state,
    ): void {

        parent::blockSubmit($form, $form_state);

        $this->setConfigurationValue( // salva i valori
            'display', 
            $form_state->getValue('display'),
        );

        $this->setConfigurationValue(
            'number_of_days', 
            (int) $form_state->getValue('number_of_days'),
        );

        $this->setConfigurationValue(
            'show_link', 
            (bool) $form_state->getValue('show_link'),
        );
    }
    /** 
     * {@inheritdoc} 
     */ 
    public function build(): array {

        $forecast = $this->forecastClient->getForecast(); // chiamata al servizio che restituisce le previsioni
 
        $display = (string) $this->configuration['display']; // lettura della configurazione

        $showLink = (bool) $this->configuration['show_link']; // lettura della configurazione

        if (
            $forecast === NULL || 
            empty($forecast['days'])
        ) {
            return [
                '#theme' => 'nome_modulo_forecast_block', 
                '#location' => NULL, 
                '#days' => [], 
                '#display' => $display, 
                '#temperature_unit' => NULL, 
                '#forecast_link' => [], 
                '#error_message' => $this->t(
                    'The weather forecast is temporarily unavailable.',
                ), 
                '#cache' => ['max-age' => 60,
                ],
            ];
        }

        // con modalità summary mostra solo un giorno, con extendended si legge la configurazione
        $numberOfDays = $display === 'summary' ? 1 : (int) $this->configuration['number_of_days'];

        $days = array_slice( //per limitare i dati in base al numero di giorni scelto
            $forecast['days'], 
            0, 
            $numberOfDays,
        );

        $forecastLink = [];

        if ($showLink) {
            $forecastLink = [
                '#type' => 'link', 
                '#title' => $this->t('View full forecast'), 
                '#url' => Url::fromRoute(
                    'nome_modulo.forecast', 
                    [
                        'display' => 'extended',
                    ],
                ),
            ];
        }

        return [
            '#theme' => 'nome_modulo_forecast_block', 
            '#location' => (string) $forecast['location'], 
            '#days' => $days, 
            '#display' => $display, 
            '#temperature_unit' => (string) $forecast['temperature_unit'], 
            '#forecast_link' => $forecastLink, 
            '#error_message' => NULL,
        ];
    }

    /** 
     * {@inheritdoc} 
     */ 
    protected function blockAccess(
        AccountInterface $account,
    ): AccessResultInterface
    {
        return AccessResult::allowedIfHasPermission( //non deve restituire solo true o false ma un AccessResult
            $account, 
            'view weather forecast',
        );
    }

    /** 
     * {@inheritdoc}
     */ 
    public function getCacheTags(): array {
        return Cache::mergeTags(
            parent::getCacheTags(), 
            [
                'nome_modulo:forecast',
            ],
        );
    }

    /** 
    * {@inheritdoc} 
    */ 
    public function getCacheMaxAge(): int
    {
        return Cache::mergeMaxAges(
            parent::getCacheMaxAge(), 
            self::CACHE_MAX_AGE,
        );
    }
}