WordPress è un CMS che ha ormai qualche anno, ma è anche il più utilizzato. C’è il tema del supporto di versioni PHP obsolete e del codice datato, e mancano anche certe prassi di programmazione moderna: l’astrazione di WordPress ne è un esempio.

Sarebbe molto meglio dividere la base di codice del core di WordPress in pacchetti gestiti da Composer. O magari, auto-caricare le classi di WordPress dai percorsi dei file.

Questo articolo vi spiegherà come astrarre il codice di WordPress manualmente e utilizzare le capacità astratte dei plugin di WordPress.

Problemi con l’Integrazione di WordPress e gli Strumenti PHP

A causa della sua architettura datata, a volte sorgono problemi quando integriamo WordPress con strumenti per il codice PHP, come l’analizzatore statico PHPStan, la libreria di test unitari PHPUnit e la libreria di namespace-scoping PHP-Scoper. Per esempio, considerate i seguenti casi:

Il codice di WordPress all’interno dei nostri progetti sarà solo una frazione del totale; il progetto conterrà anche codice aziendale agnostico rispetto al CMS sottostante. Eppure, solo per il fatto di avere del codice WordPress, il progetto potrebbe non integrarsi correttamente con gli strumenti.

Per questo, potrebbe avere senso dividere il progetto in pacchetti, alcuni dei quali contengono codice WordPress e altri hanno solo codice aziendale che usano PHP “vanilla” e nessun codice WordPress. In questo modo, questi ultimi pacchetti non saranno affetti dai problemi descritti sopra ma potranno essere perfettamente integrati con gli strumenti.

Cosa Si Intende per Astrazione del Codice?

L’astrazione del codice rimuove le dipendenze fisse dal codice, producendo pacchetti che interagiscono tra loro tramite contratti. Questi pacchetti possono poi essere aggiunti a diverse applicazioni con diversi stack, massimizzando la loro usabilità. Il risultato dell’astrazione del codice è una codebase disaccoppiato in modo pulito e basato sui seguenti pilastri:

  1. Prograzione sulle interfacce, non sulle implementazioni.
  2. Creazione di pacchetti e distribuzione tramite Composer.
  3. Unione di tutte le parti insieme tramite dependency injection.
Vuoi sapere tutto sull'astrazione del codice di WordPress? 👩‍💻 Dalle best practice ai plugin consigliati, tutto quello che devi sapere è a portata di clic ⬇️Click to Tweet

Programmare Sulle Interfacce, Non Sulle Implementazioni

Programmare sulle interfacce significa utilizzare contratti per far interagire tra loro frammenti di codice. Un contratto è semplicemente un’interfaccia PHP (o qualsiasi altro linguaggio) che definisce le funzioni disponibili e le loro firme, cioè l’input che ricevono e il loro output.

Un’interfaccia dichiara l’intento della funzionalità senza spiegare come la funzionalità sarà implementata. Accedendo alle funzionalità attraverso le interfacce, la nostra applicazione può contare su pezzi di codice autonomi che realizzano un obiettivo specifico senza sapere, o preoccuparsi, di come lo fanno. In questo modo, l’applicazione non ha bisogno di essere adattata per passare a un altro pezzo di codice che realizza lo stesso obiettivo (provenendo, per esempio, da un fornitore diverso).

Esempio di Contratti

Il seguente codice usa il contratto di Symfony CacheInterface e il contratto PHP Standard Recommendation (PSR) CacheItemInterface per implementare la funzionalità di caching:

use Psr\Cache\CacheItemInterface;
use Symfony\Contracts\Cache\CacheInterface;

$value = $cache->get('my_cache_key', function (CacheItemInterface $item) {
    $item->expiresAfter(3600);
    return 'foobar';
});

$cache implementa CacheInterface, che definisce il metodo get per recuperare un oggetto dalla cache. Accedendo a questa funzionalità attraverso il contratto, l’applicazione può ignorare dove si trova la cache sia essa nella memoria, nel disco, nel database, nella rete o in qualsiasi altro posto. Tuttavia, deve eseguire la funzione.
CacheItemInterface definisce il metodo expiresAfter per dichiarare per quanto tempo l’elemento deve essere tenuto nella cache. L’applicazione può invocare questo metodo senza preoccuparsi di cosa sia l’oggetto in cache; si preoccupa solo di quanto tempo debba essere tenuto in cache.

Programmare Sulle Interfacce in WordPress

Poiché stiamo astraendo il codice di WordPress, il risultato sarà che l’applicazione non farà direttamente riferimento al codice di WordPress, ma sempre attraverso un’interfaccia. Per esempio, la funzione get_posts di WordPress presenta questa firma:

/**
 * @param array $args
 * @return WP_Post[]|int[] Array of post objects or post IDs.
 */
function get_posts( $args = null )

Invece di invocare direttamente questo metodo, possiamo accedervi tramite il contratto Owner\MyApp\Contracts\PostsAPIInterface:

namespace Owner\MyApp\Contracts;

interface PostAPIInterface
{
  public function get_posts(array $args = null): PostInterface[]|int[];
}

Notate che la funzione get_posts di WordPress può restituire oggetti della classe WP_Post, che è specifica di WordPress. Quando si astrae il codice, dobbiamo rimuovere questo tipo di dipendenza fissa. Il metodo get_posts nel contratto restituisce oggetti del tipo PostInterface, permettendovi di fare riferimento alla classe WP_Post senza essere esplicito al riguardo.

La classe PostInterface dovrà fornire accesso a tutti i metodi e attributi di WP_Post:

namespace Owner\MyApp\Contracts;

interface PostInterface
{
  public function get_ID(): int;
  public function get_post_author(): string;
  public function get_post_date(): string;
  // ...
}

L’esecuzione di questa strategia può cambiare la nostra comprensione del punto in cui WordPress si inserisce nel nostro stack. Invece di pensare a WordPress come all’applicazione stessa (su cui installiamo temi e plugin), possiamo pensarlo semplicemente come un’altra dipendenza all’interno dell’applicazione, sostituibile come qualsiasi altro componente. (Anche se non sostituiremo WordPress nella pratica, è comunque sostituibile da un punto di vista concettuale.)

Creare e Distribuire i Pacchetti

Composer è un gestore di pacchetti per PHP. Permette alle applicazioni PHP di recuperare i pacchetti (cioè pezzi di codice) da un repository e installarli come dipendenze. Per disaccoppiare l’applicazione da WordPress, dobbiamo distribuire il suo codice in pacchetti di due tipi diversi: quelli che contengono codice WordPress e gli altri che contengono la logica di business (cioè nessun codice WordPress).

Infine, aggiungiamo tutti i pacchetti come dipendenze nell’applicazione e li installiamo tramite Composer. Dato che gli strumenti saranno applicati ai pacchetti di codice di business, questi devono contenere la maggior parte del codice dell’applicazione; più alta è la percentuale, meglio è. Far loro gestire circa il 90% del codice complessivo è un buon obiettivo.

Estrarre il Codice WordPress nei Pacchetti

Seguendo l’esempio di prima, i contratti PostAPIInterface e PostInterface saranno aggiunti al pacchetto contenente il codice aziendale mentre un altro pacchetto includerà l’implementazione di WordPress di questi contratti. Per soddisfare PostInterface, creiamo una classe PostWrapper che recupererà tutti gli attributi da un oggetto WP_Post:

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\PostInterface;
use WP_Post;

class PostWrapper implements PostInterface
{
  private WP_Post $post;
 
  public function __construct(WP_Post $post)
  {
    $this->post = $post;
  }

  public function get_ID(): int
  {
    return $this->post->ID;
  }

  public function get_post_author(): string
  {
    return $this->post->post_author;
  }

  public function get_post_date(): string
  {
    return $this->post->post_date;
  }

  // ...
}

Quando si implementa PostAPI, poiché il metodo get_posts restituisce PostInterface[], dobbiamo convertire gli oggetti da WP_Post a PostWrapper:

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\PostAPIInterface;
use WP_Post;

class PostAPI implements PostAPIInterface
{
  public function get_posts(array $args = null): PostInterface[]|int[]
  {
    // This var will contain WP_Post[] or int[]
    $wpPosts = \get_posts($args);

    // Convert WP_Post[] to PostWrapper[]
    return array_map(
      function (WP_Post|int $post) {
        if ($post instanceof WP_Post) {
          return new PostWrapper($post);
        }
        return $post
      },
      $wpPosts
    );
  }
}

Usare la Dependency Injection

L’iniezione delle dipendenze è un pattern di progettazione che vi permette di incollare tutte le parti dell’applicazione in modo accoppiato e senza blocchi. Con la dependency injection, l’applicazione accede ai servizi tramite i loro contratti e le implementazioni dei contratti vengono “iniettate” nell’applicazione tramite la configurazione.

Con il semplice cambio della configurazione, possiamo facilmente passare da un fornitore di contratti a un altro. Ci sono diverse librerie di dependency injection tra cui possiamo scegliere. Consigliamo di sceglierne una che aderisca alle raccomandazioni standard di PHP (spesso indicate come “PSR”), così possiamo facilmente sostituire la libreria con un’altra in caso di necessità. Per quanto riguarda l’iniezione delle dipendenze, la libreria deve soddisfare la PSR-11, che fornisce le specifiche per una “interfaccia contenitore”.
Tra le altre, le seguenti librerie sono conformi al PSR-11:

Accesso ai Servizi Attraverso il Container di Servizi

La libreria di dependency injection metterà a disposizione un “container di servizi”, che risolve un contratto nella sua corrispondente classe di implementazione. L’applicazione deve fare affidamento sul contenitore di servizi per accedere a tutte le funzionalità. Per esempio, mentre in genere invochiamo direttamente le funzioni di WordPress:

$posts = get_posts();

…con il container di servizi, dobbiamo prima ottenere il servizio che soddisfa PostAPIInterface ed eseguire la funzionalità attraverso di esso:

use Owner\MyApp\Contracts\PostAPIInterface;

// Obtain the service container, as specified by the library we use
$serviceContainer = ContainerBuilderFactory::getInstance();

// The obtained service will be of class Owner\MyAppForWP\ContractImplementations\PostAPI
$postAPI = $serviceContainer->get(PostAPIInterface::class);

// Now we can invoke the WordPress functionality
$posts = $postAPI->get_posts();

Usare DependencyInjection di Symfony

Il componente DependencyInjection di Symfony è attualmente la libreria di dependency injection più popolare. Vi permette di configurare il contenitore di servizi tramite codice PHP, YAML o XML. Per esempio, per definire che il contratto PostAPIInterface è soddisfatto tramite la classe PostAPI, la configurazione in YAML è la seguente:

services:
  Owner\MyApp\Contracts\PostAPIInterface:
    class: \Owner\MyAppForWP\ContractImplementations\PostAPI

DependencyInjection di Symfony permette anche alle istanze di un servizio di essere automaticamente iniettate (o “autowired”) in qualsiasi altro servizio che dipende da esso. Inoltre, rende facile definire che una classe è un’implementazione del proprio servizio.
Per esempio, considerate la seguente configurazione YAML:

services:
  _defaults:
    public: true
    autowire: true

  GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistryInterface:
    class: '\GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistry'

  GraphQLAPI\GraphQLAPI\Security\UserAuthorizationInterface:
    class: '\GraphQLAPI\GraphQLAPI\Security\UserAuthorization'
    
  GraphQLAPI\GraphQLAPI\Security\UserAuthorizationSchemes\:
    resource: '../src/Security/UserAuthorizationSchemes/*'

Questa configurazione definisce quanto segue:

Vediamo come funziona l’autowiring. La classe UserAuthorization dipende dal servizio con contratto UserAuthorizationSchemeRegistryInterface:

class UserAuthorization implements UserAuthorizationInterface
{
  public function __construct(
      protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry
  ) {
  }

  // ...
}

Grazie a autowire: true, il componente DependencyInjection farà ricevere automaticamente al servizio UserAuthorization la sua dipendenza richiesta, che è un’istanza di UserAuthorizationSchemeRegistry.

Quando Passare all’Astrazione

L’astrazione del codice potrebbe consumare tempo e sforzi considerevoli, quindi dovremmo intraprenderla solo quando i suoi benefici superano i costi. Quelli che seguono sono suggerimenti per capire quando conviene astrarre il codice. Potete farlo usando gli snippet di codice in questo articolo o i plugin WordPress astratti suggeriti qui sotto.

Ottenere l’Accesso agli Strumenti

Come menzionato in precedenza, eseguire PHP-Scoper su WordPress è difficile. Disaccoppiando il codice di WordPress in pacchetti distinti, diventa possibile eseguire direttamente un plugin WordPress.

Ridurre le Tempistiche e il Costo degli Strumenti

L’esecuzione di una suite di test PHPUnit richiede più tempo quando deve inizializzare ed eseguire WordPress rispetto a quando non lo fa. Meno tempo può anche tradursi in meno soldi spesi per eseguire i test: GitHub Actions, per esempio, fa pagare i runner ospitati da GitHub in base al tempo di utilizzo.

Non È Necessaria una Rifattorizzazione Pesante

Un progetto esistente può richiedere un pesante refactoring per introdurre l’architettura richiesta (dependency injection, suddivisione del codice in pacchetti, ecc.), e rendere così difficile l’estrazione. Astrarre il codice quando si crea un progetto da zero lo rende molto più gestibile.

Produrre Codice per Piattaforme Multiple

Estraendo il 90% del codice in un pacchetto CMS-agnostico, possiamo produrre una versione della libreria che funziona per un CMS o un framework diverso sostituendo solo il 10% del codice base complessivo.

Migrare a una Piattaforma Diversa

Se dobbiamo migrare un progetto da Drupal a WordPress, da WordPress a Laravel, o qualsiasi altra combinazione, allora solo il 10% del codice deve essere riscritto, e questo è un risparmio significativo.

Le Migliori Pratiche

Mentre progettiamo i contratti per astrarre il nostro codice, ci sono diversi miglioramenti che possiamo applicare alla codebase.

Adesione al PSR-12

Quando definiamo l’interfaccia per accedere ai metodi WordPress, dovremmo aderire al PSR-12. Questa specifica recente mira a ridurre l’attrito cognitivo quando si analizza il codice di diversi autori. Aderire al PSR-12 implica rinominare le funzioni di WordPress.

WordPress nomina le funzioni usando snake_case, mentre PSR-12 usa camelCase. Quindi, la funzione get_posts diventerà getPosts:

interface PostAPIInterface
{
  public function getPosts(array $args = null): PostInterface[]|int[];
}

…e:

class PostAPI implements PostAPIInterface
{
  public function getPosts(array $args = null): PostInterface[]|int[]
  {
    // This var will contain WP_Post[] or int[]
    $wpPosts = \get_posts($args);

    // Rest of the code
    // ...
  }
}

Metodi Split

I metodi nell’interfaccia non hanno bisogno di essere una replica di quelli di WordPress. Possiamo trasformarli quando ha senso. Per esempio, la funzione di WordPress get_user_by($field, $value) sa come recuperare l’utente dal database tramite il parametro $field, che accetta i valori "id", "ID", "slug", "email" o "login".
Questo design presenta alcuni problemi:

Possiamo migliorare questa situazione dividendo la funzione in diverse parti:

namespace Owner\MyApp\Contracts;

interface UserAPIInterface
{
  public function getUserById(int $id): ?UserInterface;
  public function getUserByEmail(string $email): ?UserInterface;
  public function getUserBySlug(string $slug): ?UserInterface;
  public function getUserByLogin(string $login): ?UserInterface;
}

Il contratto viene risolto per WordPress in questo modo (assumendo che abbiamo creato UserWrapper e UserInterface, come spiegato in precedenza):

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\UserAPIInterface;

class UserAPI implements UserAPIInterface
{
  public function getUserById(int $id): ?UserInterface
  {
    return $this->getUserByProp('id', $id);
  }

  public function getUserByEmail(string $email): ?UserInterface
  {
    return $this->getUserByProp('email', $email);
  }

  public function getUserBySlug(string $slug): ?UserInterface
  {
    return $this->getUserByProp('slug', $slug);
  }

  public function getUserByLogin(string $login): ?UserInterface
  {
    return $this->getUserByProp('login', $login);
  }

  private function getUserByProp(string $prop, int|string $value): ?UserInterface
  {
    if ($user = \get_user_by($prop, $value)) {
      return new UserWrapper($user);
    }
    return null;
  }
}

Rimuovere i Dettagli di Implementazione dalla Firma della Funzione

Le funzioni in WordPress possono fornire informazioni su come sono implementate nella loro firma. Queste informazioni possono essere rimosse quando si valuta la funzione da una prospettiva astratta. Per esempio, per ottenere il cognome dell’utente in WordPress si chiama la funzione get_the_author_meta, rendendo esplicito che il cognome di un utente è memorizzato come un valore “meta” (sulla tabella wp_usermeta):

$userLastname = get_the_author_meta("user_lastname", $user_id);

Non dovete trasmettere questa informazione al contratto. Le interfacce si preoccupano solo del cosa, non del come. Quindi, il contratto può invece avere un metodo:

Tutti i piani di hosting Kinsta includono supporto 24/7 da parte dei nostri sviluppatori e tecnici veterani di WordPress. Sei in chat con lo stesso team che supporta i nostri clienti Fortune 500. Dai un’occhiata ai nostri piani!

interface UserAPIInterface
{
  public function getUserLastname(UserWrapper $userWrapper): string;
  ...
}

Aggiungere Tipi Più Rigidi

Alcune funzioni di WordPress possono ricevere parametri in modi diversi, portando all’ambiguità. Per esempio, la funzione add_query_arg può ricevere una singola chiave e un valore:

$url = add_query_arg('id', 5, $url);

… o un array di chiave => valore:

$url = add_query_arg(['id' => 5], $url);

La nostra interfaccia può definire un intento più comprensibile dividendo tali funzioni in diverse funzioni separate, ognuna delle quali accetta una combinazione unica di input:

public function addQueryArg(string $key, string $value, string $url);
public function addQueryArgs(array $keyValues, string $url);

Eliminare il Debito Tecnico

La funzione get_posts di WordPress restituisce non solo “post” ma anche “pagine” o qualsiasi entità di tipo “post personalizzati”, e queste entità non sono intercambiabili. Sia i post che le pagine sono post personalizzati, ma un post personalizzato non è un post né una pagina. Pertanto, l’esecuzione di get_posts può restituire delle pagine. Questo comportamento è una discrepanza concettuale.

Per correggerlo, get_posts dovrebbe invece essere chiamato get_customposts, ma non è mai stato rinominato nel core di WordPress. Questo è un problema comune con la maggior parte dei software di lunga data ed è chiamato “debito tecnico”, cioè un codice che ha dei problemi, ma che non viene mai corretto perché introdurrebbe dei cambiamenti di rottura.

Quando creiamo i nostri contratti, però, abbiamo l’opportunità di evitare questo tipo di debito tecnico. In questo caso, possiamo creare una nuova interfaccia ModelAPIInterface che può trattare con entità di diversi tipi, e creiamo diversi metodi, ognuno per trattare un tipo diverso:

interface ModelAPIInterface
{
  public function getPosts(array $args): array;
  public function getPages(array $args): array;
  public function getCustomPosts(array $args): array;
}

In questo modo la discrepanza non si verificherà più e vedrete questi risultati:

Vantaggi dell’Astrazione del Codice

I principali vantaggi dell’astrazione del codice di un’applicazione sono:

Problemi con l’Astrazione del Codice

Gli svantaggi di astrarre il codice di un’applicazione sono:

Opzioni dei Plugin WordPress Astratti

Anche se è generalmente più saggio estrarre il vostro codice in un ambiente locale prima di lavorarci sopra, alcuni plugin di WordPress possono aiutarvi a raggiungere i vostri obiettivi di astrazione. Queste sono le scelte migliori secondo noi.

1. WPide

Prodotto da WebFactory Ltd, il popolare plugin WPide estende notevolmente la funzionalità dell’editor di codice predefinito di WordPress. Funziona come un plugin WordPress astratto e vi permette di visualizzare il vostro codice in situ per visualizzare meglio ciò che necessita di attenzione.

Il plugin WPide.

Il plugin WPide.

WPide ha anche una funzione di ricerca e sostituzione per individuare rapidamente il codice obsoleto o scaduto e sostituirlo con una versione aggiornata.

Oltre a questo, WPide fornisce un sacco di funzioni extra, tra cui:

2. Ultimate DB Manager

Il plugin Ultimate WP DB Manager di WPHobby vi offre uno strumento veloce per scaricare completamente i vostri database per l’estrazione e il refactoring.

Screenshot of the Ultimate DB Manager plugin's logo with the words:

Il plugin Ultimate DB Manager.

Naturalmente, i plugin di questo tipo non sono necessari per gli utenti Kinsta, poiché Kinsta offre un accesso diretto al database a tutti i clienti. Tuttavia, se non avete sufficiente accesso al database attraverso il vostro fornitore di hosting, Ultimate DB Manager potrebbe essere utile come plugin astratto per WordPress.

3. Il Vostro Plugin Personalizzato per l’Astrazione di WordPress

Alla fine, la scelta migliore per l’astrazione sarà sempre quella di creare il vostro plugin. Può sembrare una grande impresa, ma se avete dei limiti per la gestione diretta dei vostri file principali di WordPress, questo è buon espediente per realizzare l’astrazione.

Farlo ha dei chiari vantaggi:

Potete imparare come creare il vostro plugin WordPress astratto attraverso il WordPress’ Plugin Developer Handbook.

Impara ad astrarre il tuo codice manualmente sfruttando le funzionalità dei plugin per l'astrazione di WordPress: spieghiamo tutto in questa guida approfondita 🚀⬇️Click to Tweet

Riepilogo

È necessario astrarre il codice nelle nostre applicazioni? Come per ogni cosa, non c’è una “risposta giusta” sempre valida, perché le cose cambiano da progetto a progetto. Quei progetti che richiedono un’enorme quantità di tempo di analisi con PHPUnit o PHPStan possono trarne il massimo beneficio, ma lo sforzo necessario per realizzarlo potrebbe non valerne sempre la pena.

In questo articolo avete imparato tutto quello che c’è sapere per iniziare ad astrarre il codice di WordPress.

Avete intenzione di implementare questa strategia nel vostro progetto? Se sì, userete un plugin WordPress astratto? Fatecelo sapere nella sezione dei commenti!


Risparmia tempo e costi e massimizza le prestazioni del sito con:

  • Aiuto immediato dagli esperti dell’hosting WordPress, 24/7.
  • Integrazione di Cloudflare Enterprise.
  • Copertura globale del pubblico con 28 data center in tutto il mondo.
  • Ottimizzazione del sito con il nostro Monitoraggio delle Prestazioni delle Applicazioni integrato.

Tutto questo e molto altro, in un piano senza contratti a lungo termine, con migrazioni assistite e una garanzia di 30 giorni di rimborso. Scopri i nostri piani o contattaci per trovare il piano che fa per te.