martedì 28 maggio 2013

Caricare automaticamente i file contenenti le classi utilizzate dall'applicazione PHP

Qualunque progettino, che miri anche a tenere ordinato il codice, fa inevitabilmente uso delle include e delle require.   Queste due istruzioni fanno in modo che il codice presente in altri file sia incorporato nello script come se ne facesse parte integrale. In questo modo è possibile smembrare in più parti le componenti di uno script alleggerendo e rendendo più chiaro il contenuto di ogni singolo file. Di norma invocare include e require è la prima cosa che si fa, affinché sia possibile utilizzare elementi non direttamente presenti nel file senza ulteriori problemi.


Personalmente preferisco utilizzare la require_once che offre i seguenti vantaggi:

  • blocca l'esecuzione dello script, generando un errore, qualora il file da incorporare non sia trovato
  • fa in modo che uno stesso file non sia incorporato più di una volta evitando il problema della duplicazione delle definizioni

Se si utilizza la OOP, di norma ciò che si incorpora nel proprio script sono le classi necessarie al suo funzionamento. E' possibile definire un sistema automatico di caricamento delle classi per mezzo della funzione spl_autoload_register() di PHP. Tale funzione accetta come argomento una callable ossia una funzione di callback invocata qualora si renda necessario eseguire il caricamento di una classe non disponibile nel codice. 

Sfruttando i namespace, che di fatto creano una struttura logica per il software, e facendo in modo che il filesystem rispecchi la parte logica dei namespace utilizzati, è possibile implementare una funzione che carichi le classi necessarie allo script. Affinché ciò sia possibile è anche necessario che la classe sia contenuta in un file avente lo stesso nome ed estensione php.

Iniziamo quindi con il definire questa nostra classe che consenta l'autoload di tutte le altre:

<?php

namespace CiP\Utilita;


class cAutoload {}


?>

Questa classe appartiene al mio personalissimo namespace CiP\Utilita. Scopo della classe è di memorizzare i percorsi in cui sbirciare per verificare la presenza del file da incorporare. In particolare il percorso completo per il file è un percorso iniziale preso fra quelli registrati nella classe stessa, seguito dal namespace della classe a cui vanno cambiati i simboli \ in / per maggiore compatibilità, ancora seguito dal nome della classe e da .php.

Avremo quindi un membro privato per la memorizzazione dei percorsi registrati, ed uno allo scopo di debug con le classi caricate automaticamente. Dati tali elementi occorre definire i relativi metodi d'accesso:

<?php

namespace CiP\Utilita;


class cAutoload {

    
    private $percorsi = [];
    private $classi = [];    

    public function AggiungiPercorso($percorso) {
        $percorso = str_replace('\\', '/', $percorso);

        if ($percorso{strlen($percorso) - 1} != '/')
            $percorso.='/';
        if (!file_exists($percorso))
            throw new \Exception("La cartella $percorso non esiste");
        $this->percorsi[$percorso] = $percorso;
    }

    public function DammiPercorsi() {

        return $this->percorsi;
    }

    protected function AggiungiClasseCaricataAutomaticamente($classe) {

        $this->classi[] = $classe;
    }

    public function DammiClassiCaricateAutomaticamente() {

        return $this->classi;
    }

}


?>

$percorsi
e $classi sono degli array che memorizzano il relativo elenco. In particolare $percorsi ha lo stesso valore sia per la chiave che per il valore corrispondente a tale chiave. Ciò perché, caricando più volte uno stesso percorso questo sia memorizzato una e una sola volta. In più la funzione AggiungiPercorso() normalizza il valore facendo in modo che abbia sempre in fondo il carattere di chiusura / oltre a sollevare una eccezione qualora il percorso specificato non esista. La funzione che aggiunge i nomi delle classi caricate automaticamente all'elenco è protetta, poichè non ha senso che sia chiamata dall'esterno e potrebbe essere ridefinita da una classe figlia per intercettare il momento immediatamente successivo al caricamento di una nuova classe. Ovviamente la figlia deve chiamare il metodo della classe madre.

Non ha senso l'esistenza di più di un oggetto della classe cAutoload. Il metodo costruttore è quindi protetto (ridefinibile per ereditarietà) ed una funzione statica permette l'accesso all'unica istanza possibile.


<?php

namespace CiP\Utilita;


class cAutoload {


    private static $istanza = null;

    private $percorsi = [];
    private $classi = [];

    protected function __construct() {

        $percrosi = $this->DammiPercorsi();
        if (empty($percorsi)) {
            $this->AggiungiPercorso($_SERVER['DOCUMENT_ROOT']);
            $this->AggiungiPercorso(dirname($_SERVER['SCRIPT_FILENAME']));
        }
        spl_autoload_register();
    }    

    public static function OttieniIstanza() {

        if (is_null(self::$istanza))
            self::$istanza = new \CiP\Utilita\cAutoload();
        return self::$istanza;
    }

    public function AggiungiPercorso($percorso) {
        $percorso = str_replace('\\', '/', $percorso);

        if ($percorso{strlen($percorso) - 1} != '/')
            $percorso.='/';
        if (!file_exists($percorso))
            throw new \Exception("La cartella $percorso non esiste");
        $this->percorsi[$percorso] = $percorso;
    }

    public function DammiPercorsi() {

        return $this->percorsi;
    }

    protected function AggiungiClasseCaricataAutomaticamente($classe) {

        $this->classi[] = $classe;
    }

    public function DammiClassiCaricateAutomaticamente() {

        return $this->classi;
    }

}


?>


La funzione statica, atta ad ottenere l'istanza della classe, verifica se un'istanza è già stata generata. Se non è stata generata ancora alcuna istanza ne crea una. L'istanza memorizzata nella variabile di classe è restituita al richiedente. In fase di costruzione di una nuova istanza, sono immediatamente caricati all'interno dei percorsi sia il path di document_root che il path dello script in esecuzione. In tal modo è soddisfatto, nella maggior parte dei casi, il path iniziale a cui far seguire quello specifico della classe come definito dai namespace.

Non resta che implementare la funzione che sarà richiamata affinché realizzi l'autoload passandola come argomento ad spl_autoload_register:



<?php

namespace CiP\Utilita;


class cAutoload {


    private static $istanza = null;

    private $percorsi = [];
    private $classi = [];

    protected function __construct() {

        $percrosi = $this->DammiPercorsi();
        if (empty($percorsi)) {
            $this->AggiungiPercorso($_SERVER['DOCUMENT_ROOT']);
            $this->AggiungiPercorso(dirname($_SERVER['SCRIPT_FILENAME']));
        }
        spl_autoload_register(array($this,'Autoload'));
    }

    public function Autoload($classe) {        

        $file = str_replace('\\', '/', $classe) . '.php';
        $percorsi = $this->DammiPercorsi();
        foreach ($percorsi as $percorso) {
            if (file_exists($percorso . $file)) {
                require_once $percorso . $file;
                $this->AggiungiClasseCaricataAutomaticamente($classe);
                return;
            }
        }
        throw new \Exception("Impossibile caricare $file");
    }

    public static function OttieniIstanza() {

        if (is_null(self::$istanza))
            self::$istanza = new \CiP\Utilita\cAutoload();
        return self::$istanza;
    }

    public function AggiungiPercorso($percorso) {
        $percorso = str_replace('\\', '/', $percorso);

        if ($percorso{strlen($percorso) - 1} != '/')
            $percorso.='/';
        if (!file_exists($percorso))
            throw new \Exception("La cartella $percorso non esiste");
        $this->percorsi[$percorso] = $percorso;
    }

    public function DammiPercorsi() {

        return $this->percorsi;
    }

    protected function AggiungiClasseCaricataAutomaticamente($classe) {

        $this->classi[] = $classe;
    }

    public function DammiClassiCaricateAutomaticamente() {

        return $this->classi;
    }

}


?>



Come si vede la funzione membro Autoload accetta un parametro chiamato $classe, al cui interno c'è il nome assoluto della classe (nome della classe preceduto dalla struttura dei namespace cui appartiene). La funzione quindi cambia il \ in / affinchè sia interpretabile come path sia in sistemi windows che linux, aggiungendo l'estensione .php. Quindi cerca il file in ogni percorso registrato nella classe appendendo al percorso quanto ottenuto prima. Se trova il file lo incorpora nello script, altrimenti è sollevata una eccezione. Con l'errore è visualizzata la classe che lo ha prodotto e i percorsi in cui la classe è ricercata.

A scopo di esempio ho generato un progettino PHP, con NetBeans, come mostrato nella immagine a sinistra. Tutti i file sono quasi vuoti contenendo definizioni vuote di classi. Gli unici che hanno contenuto sono cAutoload.php che contiene il precedente codice e i file index.php che contengono il codice di test. Seguono i sorgenti di ogni file (si noti come ogni file contenga la classe avente lo stesso nome del file e il namespace che rispecchia il path parziale che può essere quello di un proprio modulo o libreria):


iInterfacciaB.php

<?php
namespace FoldA\FoldB\InterfaceB;
interface iInterfacciaB {}
?>


CalsseB.php

<?php
namespace FoldA\FoldB;
class ClasseB implements \FoldA\FoldB\InterfaceB\iInterfacciaB {}
?>


ClasseA.php

<?php
namespace FoldA;
class ClasseA {}
?>


ClasseC.php

<?php
namespace FoldC;
class ClasseC {}
?>


ClasseD.php

<?php
namespace FoldD;
class ClasseD {}
?>


ClasseSub1.php

<?php
namespace Sub\Sub1;
class ClasseSub1 {}
?>


index.php dentro la cartella Sub1

<?php
    require_once '../cAutoload.php';
    $autoload = CiP\Utilita\cAutoload::OttieniIstanza();
    //Questo altro path è necessario perchè il namespace di ClasseSub1 è
    //definito come Sub\Sub1 ossia a partire dalla cartella di progetto,
    //mentre quelle automaticeh sono htdocs e htdocs/Autoload/Sub con
    //Autoload cartella di progetto in NetBeans
    $autoload->AggiungiPercorso($_SERVER['DOCUMENT_ROOT'].'/Autoload');
?>
<!DOCTYPE html>
<html>
    <head>
        <title>Test</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    </head>
    <body>
        <?php
         $a = new FoldA\ClasseA();
         $b = new FoldA\FoldB\ClasseB();
         $c = new \FoldC\ClasseC();
         $d = new \FoldD\ClasseD();
         $z = new Class0();
         $s = new Sub\Sub1\ClasseSub1;
         var_dump($autoload->Dammipercorsi());
         var_dump($autoload->DammiClassiCaricateAutomaticamente());         
        ?>
    </body>
</html>


index.php nella root del progetto

<?php
require_once './cAutoload.php';
$autoload = CiP\Utilita\cAutoload::OttieniIstanza();
?>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Test</title>
    </head>
    <body>
        <?php
         $a = new FoldA\ClasseA();
         $b = new FoldA\FoldB\ClasseB();
         $c = new \FoldC\ClasseC();
         $d = new \FoldD\ClasseD();
         $z = new Class0();
         var_dump($autoload->Dammipercorsi());
         var_dump($autoload->DammiClassiCaricateAutomaticamente());       
        ?>
    </body>
</html>


cAutoload.php


<?php

namespace CiP\Utilita;


class cAutoload {


    private static $istanza = null;

    private $percorsi = [];
    private $classi = [];

    protected function __construct() {

        $percrosi = $this->DammiPercorsi();
        if (empty($percorsi)) {
            $this->AggiungiPercorso($_SERVER['DOCUMENT_ROOT']);
            $this->AggiungiPercorso(dirname($_SERVER['SCRIPT_FILENAME']));
        }
        spl_autoload_register(array($this,'Autoload'));
    }

    public function Autoload($classe) {        

        $file = str_replace('\\', '/', $classe) . '.php';
        $percorsi = $this->DammiPercorsi();
        foreach ($percorsi as $percorso) {
            if (file_exists($percorso . $file)) {
                require_once $percorso . $file;
                $this->AggiungiClasseCaricataAutomaticamente($classe);
                return;
            }
        }
        throw new \Exception("Impossibile caricare $file");
    }

    public static function OttieniIstanza() {

        if (is_null(self::$istanza))
            self::$istanza = new \CiP\Utilita\cAutoload();
        return self::$istanza;
    }

    public function AggiungiPercorso($percorso) {
        $percorso = str_replace('\\', '/', $percorso);

        if ($percorso{strlen($percorso) - 1} != '/')
            $percorso.='/';
        if (!file_exists($percorso))
            throw new \Exception("La cartella $percorso non esiste");
        $this->percorsi[$percorso] = $percorso;
    }

    public function DammiPercorsi() {

        return $this->percorsi;
    }

    protected function AggiungiClasseCaricataAutomaticamente($classe) {

        $this->classi[] = $classe;
    }

    public function DammiClassiCaricateAutomaticamente() {

        return $this->classi;
    }

}


?>


L'esecuzione dei file index.php produce quanto mostrato nelle immagini seguenti in cui, nel primo array sono presenti i percorsi utilizzati e nel secondo le classi caricate (diciamo JIT - Just In Time) solo nel momento in cui servono e solo se necessarie.
Esecuzione di index.php nella root del progetto
Esecuzione di index.php nella cartella Sub