Quando è nato il concetto di pagina web dinamica, i linguaggi di programmazione o di scripting utilizzati per la generazione di pagine web dovevano rispondere ad esigenze molto ridotte, limitate dal fatto che Internet non si era ancora evoluto in quello che è oggi. Attualmente le esigenze nel campo dello sviluppo web si sono evolute portano gli sviluppatori a dover optare per scelte architetturali complesse ed implementazioni software avanzate.
In un panorama come quello attuale risulta spesso utile se non indispensabile fornire un sistema che permetta agli sviluppatori o agli altri utilizzatori del software di estendere agilmente i programmi prodotti aggiungendovi funzionalità e comportamenti. Il concetto di plugin è ormai diffuso ed utilizzato in tutti i campi verso cui la programmazione si è potuta spingere. In questo articolo cercherò di introdurre le operazioni da seguire quando si decide di strutturare un'applicazione in modo che sfrutti i plugin, mostrando una delle possibili implementazioni di un sistema simile.
Strutturare le applicazioni per accettare plugin
Il primo passo fondamentale per poter creare un'applicazione che supporti i plugin è quello di strutturarla già con queste peculiarità in modo da non doversi trovare successivamente a dover aggiungere pezze per poter includere questa funzionalità. Operare su del codice già predisposto rende più semplice la stesura delle API pubbliche e permette allo sviluppatore di pensare ad un'architettura più modulare e riutilizzabile.
Al contrario, operare per adattare una vecchia applicazione affinché supporti i plugin non è sempre un'operazione facile, dato che la difficoltà del compito dipende molto da come l'applicazione è stata strutturata e da quanto complesso sia il punto in cui si vuole aggiungere il supporto ai plugin.
In generale possiamo classificare i plugin in due grossi gruppi: quelli che aggiungono funzionalità utilizzabili dal programmatore nella scrittura del proprio codice e quelle che invece aggiungono comportamenti alla propria applicazione. In base alla tipologia scelta bisognerà operare in modo differente nel design della struttura dell'applicazione.
In entrambi i casi è opportuno scrivere un set di funzioni e classi pubbliche (le cosiddette API) che potranno essere sfruttate dai plugin per interagire in modo controllato con l'applicazione per la quale saranno scritti. Le API devono occuparsi di esporre a coloro che svilupperanno il plugin le funzionalità in modo chiaro e non equivoco, così che lo sviluppatore possa concentrarsi solamente sulla scrittura del codice aggiuntivo senza doversi focalizzare anche sul colmare le mancanze dell'API.
Come regola generale il set di API pubbliche deve essere abbastanza completo da colmare tutte le richieste che potrebbero sorgere nello sviluppo dei plugin stimati, cercando sempre di controllare che il plugin non esegua operazioni errate. In PHP è praticamente impossibile limitare i plugin all'utilizzo di funzionalità specifiche, ma una buona API con un buon sistema di report degli errori compre gran parte dei problemi che potrebbero verificarsi.
Nel caso in cui si vogliano includere plugin per l'aggiunta di funzionalità utilizzabili dal programmatore è opportuno fornire un'interfaccia univoca per poterli caricare e per potervi accedere: si può pensare a delle semplici factory che recuperino le funzionalità richieste da cartelle contenenti i sorgenti dei plugin, oppure a soluzioni più complesse che operino sulle interfacce come COM ex XPCOM. Sicuramente in PHP è più comune (dato che non richiede la stesura di estensioni native in C) il primo approccio, come è possibile notare in moltissime librerie, tra le quali Smarty.
Nel caso in cui si necessiti invece di strutturare l'applicazione per accettare plugin che estendano i comportamenti dell'applicazione, sarà opportuno permettere delle strutture in grado di fornire delle risorse grafiche (spesso accade che i plugin necessitino di icone, template ed altri tipi di risorse). Quando si struttura un'applicazione con queste peculiarità si opera solitamente in modo da scrivere le sezioni dinamiche dipendenti dai plugin affinché interroghino i repository per sapere se ci sono plugin adatti all'esigenza e li restituiscano in modo che ci si possa operare.
Un'applicazione potrebbe avere un grosso numero di plugin installati, ed è quindi opportuno prendere delle contromisure affinché non avvengano sprechi di memoria e risorse; buona norma è quella di avere un repository globale (solitamente implementato con un Singleton) da interrogare per ottenere i plugin necessari. I plugin dovrebbero essere caricati solamente quando esplicitamente richiesti (sempre che questo sia possibile, ovviamente) e le istanze dovrebbero essere condivise il più possibile e rimosse appena risultano inutili.
Infine, per permettere una miglior gestione del sistema è opportuno, anche se non strettamente necessario, soprattutto in caso di strutture semplici, fornire un sistema di configurazione che permetta di fornire informazioni sui plugin senza che questi siano esplicitamente caricati; il sistema di configurazione solitamente prevede file di testo contenenti informazioni sul plugin ed alcuni metadati utili per eventuali aggiornamenti o il riconoscimento degli autori.
Una struttura d'esempio
Dopo aver introdotto teoricamente come dovrebbero essere strutturate le applicazioni che accettano i plugin, passiamo alla pratica mostrando come potrebbe essere implementato un framework per lo sviluppo web che accetta plugin scritti da terze parti per estenderne le funzionalità. L'esempio sarà volutamente semplificato, ma dovrebbe dare un'idea su come operare per rendere le proprie applicazioni estendibili.
Per prima cosa definiamo le interfacce che potranno essere implementate per la stesura dei plugin che lascerò vuota ma utilizzeremo per il controllo della validità del plugin:
<?php
interface FrameworkPlugin
{
}
?>
Il repository dei plugin invece sarà il Singleton tramite il quale sarà possibile accedere ai plugin per poterli utilizzare. Il repository inizialmente caricherà le informazioni sui plugin installati da un file di configurazione e potrà essere interrogato per restituire istanze di un plugin specifico. Il file di configurazione esporrà informazioni sui plugin installati fornendone l'ID univoco per accedervi, il path dei sorgenti ed una breve descrizione:
id = it.darkbard.Test1
description = un plugin di prova fatto da gabriele
path = plugins/test1.php
id = it.darkbard.Test2
description = un plugin di prova fatto da gabriele
path = plugins/test2.php
Nel essempio precedente comunicheremo al repository l'intenzione di esporre due plugin chiamati it.darkbard.Test1
ed it.darkbard.Test2
. Passiamo ora all'implementazione del repository che esporrà una funzionalità per il caricamento del plugin ed una utilizzata per recuperare i metadati aggiuntivi come la descrizione:
<?php
final class PluginRepository
{
private static $instance = null;
private static function getInstance()
{
if(self::$instance == null)
self::$instance = new PluginRepository();
return self::$instance;
}
private $plugins;
private function __construct()
{
$this->plugins = array();
//Carico i plugin
$plugin = null;
foreach(file("plugins.cfg") as $line)
{
$line = trim($line);
if(strlen($line) == 0)
continue;
list($attr, $value) = array_map('trim', explode("=", $line));
if($attr == 'id')
{
if(!is_null($plugin))
$this->plugins[$plugin['id']] = $plugin;
$plugin = array();
}
$plugin[$attr] = $value;
}
if(!is_null($plugin) && array_key_exists('id', $plugin))
$this->plugins[$plugin['id']] = $plugin;
}
public static function loadPlugin($id)
{
$repository = PluginRepository::getInstance();
if(!array_key_exists($id, $repository->plugins))
throw new Exception("Il plugin ".$id." non esiste");
$plugin = $repository->plugins[$id];
if(!array_key_exists("__class__", $plugin))
{
if(!file_exists($plugin['path']))
throw new Exception("Impossibile caricare il plugin ".$id);
require_once($plugin['path']);
$class_name = basename($plugin['path'], '.php');
$reflection = new ReflectionClass($class_name);
$found = false;
foreach($reflection->getInterfaces() as $interface)
$found = $interface->getName() == 'FrameworkPlugin';
if(!$found)
throw new Exception("Il plugin non può essere caricato");
$plugin["__class__"] = $class_name;
}
return new $plugin["__class__"];
}
public static function getMetadata($id, $meta, $default=null)
{
$repository = PluginRepository::getInstance();
if(!array_key_exists($id, $repository->plugins))
throw new Exception("Il plugin ".$id." non esiste");
$plugin = $repository->plugins[$id];
return array_key_exists($meta, $plugin) ? $plugin[$meta] : $default;
}
public static function listPlugins()
{
$repository = PluginRepository::getInstance();
return array_keys($repository->plugins);
}
}
?>
L'inizializzazione avviene solamente la prima volta che vengono esplicitamente richieste informazioni sul plugin oppure la prima volta che un plugin viene caricato. Il costruttore si occupa di caricare dal file di configurazione le informazioni sul plugin e salvarle all'interno di un array per un accesso veloce in futuro. La funzione statica loadPlugin
carica un plugin in base al suo ID, controllando che questo esista e che sia stato implementato in modo corretto. La funzione getMetadata
invece recupera i dati aggiuntivi specificati nel file di configurazione mentre listPlugins
restituisce l'ID di tutti i plugin installati.
Infine definiamo un semplice plugin:
<?php
class Test1 implements FrameworkPlugin
{
public function test()
{
echo "Plugin invocato correttamente";
}
}
?>
e proviamo il funzionamento del sistema implementato:
<?php
// ...
echo "Sono installati i seguenti plugin:<ul>";
foreach(PluginRepository::listPlugins() as $id)
{
echo "<li>".$id."(".PluginRepository::getMetadata($id, 'path', '')."): ".PluginRepository::getMetadata($id, 'description', '')."</li>";
}
echo "</ul>";
$test = PluginRepository::loadPlugin('it.darkbard.Test1');
$test->test();
?>
Conclusioni
Abbiamo visto come strutturare un'applicazione in modo che accetti i plugin e come trasformare la teoria in un po' di semplice codice. La soluzione proposta funge solo d'esempio dato che spesso le esigenze nello sviluppo di sistemi estendibili sono le più differenti; spero comunque di aver introdotto correttamente l'argomento, su cui tornerò in futuro con qualche articolo più specifico e dettagliato.