Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Il Framework MVC Taste: routing delle richieste

Il processo di routing delle richieste in un framework PHP: come associare ad un indirizzo Web un controller in PHP che potrà essere eseguito dal server
Il processo di routing delle richieste in un framework PHP: come associare ad un indirizzo Web un controller in PHP che potrà essere eseguito dal server
Link copiato negli appunti

Questo articolo fa parte di una serie dedicata alla programmazione di un framework MVC personalizzato in PHP. L'autore ha chiamato questo framework "Taste". Gli altri articoli della serie sono disponibili nella categoria Taste framework di php.html.it.

Dopo aver introdotto e descritto la parte del framework che sarà essenziale per implementare un sistema di bootstrap che si occupi di instradare correttamente le richieste HTTP ad i nostri controller scritti in PHP, iniziamo ad addentrarci in modo pratico e teorico ai concetti base di un framework MVC.

In questo articolo ci occuperemo della fase di routing di una richiesta, che in pratica è il processo che permette di associare ad un determinato URL un controller PHP che verrà recuperato ed eseguito da un'applicazione eseguita dal server.

Potete scaricare l'ultima versione del framework dal link donwload all'inizio dell'articolo.

Il processo di routing delle richieste

Il processo di routing delle richieste è composto da una serie di operazioni che permettono al framework MVC di interpretare una richiesta HTTP in modo da associarvi l'esecuzione di un controller. Come ben sappiamo, quando viene richiesto un URL specifico dal browser viene composta una richiesta HTTP che viene inviata al server; nel nostro caso questa richiesta è intercettata dallo script di bootstrap che la impacchetta all'interno di un oggetto Request che viene instradato dal Server verso una o più implementazioni di Application. È a questo punto che interviene il processo di routing: l'applicazione recupera l'URL richiesto, lo analizza e recupera il controller corrispondente alla richiesta. Dopo averlo recuperato lo esegue, genera un'istanza di Response e la restituisce al flusso del Server.

Al fine di mantenere separato e modulare il framework ho implementato due entità separate per il router e per l'applicazione che lo utilizza; in questo modo è possibile cambiare implementazioni del router in modo trasparente al funzionamento del core MVC.

I sistemi di routing variano moltissimo in base alle esigenze della piattaforma MVC che si utilizza e, perché no, dell'applicazione che vi viene sviluppata sopra. Tra le varie opzioni possibili possiamo identificare comunque due gruppi principali:

  • I router espliciti, che hanno una serie di regole che, applicate all'URL in entrata, identificano più o meno univocamente un controller da eseguire;
  • I router che rimappano direttamente un path del filesystem all'URL richiesto, eseguendo il controller che si trova in quella posizione;

Nelle implementazioni del primo tipo di router vengono solamente utilizzate delle espressioni regolari che, applicate all'URL, permettono di identificare il controller da eseguire. Queste espressioni regolari contengono spesso dei raggruppamenti, che permettono di definire delle parti variabili di URL che andranno a fungere da parametri per le azioni dei controller richiamati.

Come esempio forniremo l'implementazione della prima versione, che a mio parere è sicuramente la più configurabile e riadattabile alla maggior parte delle esigenze.

Dettagli sull'implementazione

I file che forniscono l'implementazione del sistema di routing si trovano all'interno della cartella taste/mvc/routing. Per rendere modulare il sistema forniremo un'implementazione astratta di una classe (Router) che fornirà la base per le varie tipologie di router che si vorrà implementare.

La classe astratta Router cerca di raggruppare tutte quelle operazioni che possono essere identificate come comuni per ogni tipologia di router, come valutare i controller recuperati o ripulire l'URL in entrata da eventuali dati in eccesso:

abstract class Router extends Configurable
{
    public function __construct()
    {
        parent::__construct();
    }
    
    abstract public function route($url);
    
    public function findController($url)
    {
        $url = $this->cleanUrl($url);
        
        if(is_null($url))
            throw new RouterException("Invalid URL");
        
        $cpath = $this->route($url);
        if(is_null($cpath))
            return null;
        
        list($filepath, $class, $action, $params) = $cpath;
        
        require_once $filepath;
        
        if(!class_exists($class))
            throw new RouterException("Class ".$class." not defined in ".$path);
            
        return new $class($action, $params);
    }
    
    protected function cleanUrl($url)
    {
        $url = str_replace("", "/", $url);
        $chunks = explode("/", $url);
        $cleaned = array();
        
        foreach($chunks as $chunk)
        {
            $chunk = trim($chunk);
            if(strlen($chunk) == 0 || $chunk == ".")
                continue;
            
            if($chunk == "..")
            {
                if(count($cleaned) == 0)
                    return null;
                array_pop($cleaned);
                continue;
            }
            
            array_push($cleaned, $chunk);
        }
        
        return implode("/", $cleaned);
    }
}

Un Router è un oggetto configurabile (e di conseguenza lo saranno tutte le sue implementazioni) che come unico presupposto ha quello che la classe che rappresenta il controller debba accettare come parametri del costruttore l'azione da eseguire (la funzione del controller da richiamare) ed i parametri da passare. L'implementazione del controller la vedremo nel prossimo articolo.

Vengono implementati i metodi cleanUrl e findController, mentre viene lasciato astratto il metodo route che dovrà essere implementato dalle sottoclassi:

  • cleanUrl: per evitare che un URL involontariamente (o volontariamente) malformato possa creare dei problemi al processo di routing, questo metodo si occupa di ripulire l'URL contenuto nella richiesta. Il metodo non è sicuramente esente da imprecisioni, ed andrebbe ottimizzato e migliorato nel caso in cui si desideri utilizzarlo in fase di produzione;
  • findController: viene utilizzato dall'applicazione per trasformare un URL in un oggetto Controller. In pratica ripulisce l'URL, richiama il metodo route su di questo e, se tutto è avvenuto in modo corretto, restituisce l'istanza del Controller corretto;
  • route: accetta come unico parametro l'URL da valutare e deve obbligatoriamente restituire un array formato da quattro elementi che rappresenteranno rispettivamente il path del file, la classe da istanziare, l'azione da richiamare ed i suoi parametri;

Nel caso in cui un'operazione non avvenga in modo corretto, viene lanciata l'eccezione RoutingException (implementata in taste/mvc/routing/RoutingException.php) che può essere catturata dall'applicazione e gestita in modo appropriato.

Passiamo ora all'implementazione della classe astratta che fornirà il sistema di routing prima discusso. L'implementazione è contenuta nel file taste/mvc/routing/ExplicitRouter.php. Il funzionamento è molto semplice ma abbastanza potente da permettere svariate soluzioni: nel router vengono registrate una serie di regole che associano ad un'espressione regolare una particolare stringa che identifica il controller da istanziare e l'azione da eseguire su di questo. Quando viene effettuato il routing vengono iterate tutte le regole finché non se ne trova una che corrisponde all'URL: in questo caso viene recuperato il controller associato e vengono estratti i parametri dai match dell'espressione regolare.

class ExplicitRouter extends Router
{
    private $controller_dirs;
    private $map;
    
    public function __construct($rules=array())
    {
        parent::__construct();
        
        $this->map = array();
        $this->controller_dirs = array();
        $dirs = $this->config->get('controllersDirs', "controllers");
        foreach(array_map('trim', explode(",", $dirs)) as $dir)
        {
            if($dir{0} != '/')
                $dir = realpath(TASTE_DIR."/../".$dir);
            
            $this->controller_dirs[] = $dir;
        }
        
        foreach($rules as $regex => $controller)
            $this->connect($regex, $controller);
    }
    
    public function connect($regex, $controller)
    {
        if(substr_count($controller, ".") < 2)
            throw new ExplicitRouterException("Wrong controller format (at least FILE.CLASS.ACTION)");
            
        $this->map["#".$regex."#"] = $controller;
    }
    
    private function createControllerPath($controller)
    {
        $chunks = explode(".", $controller);
        $action = array_pop($chunks);
        $class = array_pop($chunks);
        
        return array(implode("/", $chunks), $class, $action);
    }
    
    public function route($url)
    {
        $dirs = $this->controller_dirs;
        foreach($this->map as $regex => $controller)
        {
            if(!preg_match($regex, $url, $matches))
                continue;
                
            list($filepath, $class, $action) = $this->createControllerPath($controller);
            
            foreach($dirs as $dir)
            {
                $path = realpath($dir."/".$filepath.".php");
                if(!file_exists($path))
                    continue;
                
                return array($path, $class, $action, array_slice($matches, 1));
            }
        }
        
        return null;
    }
}

Il costruttore della classe accetta un array di regole che vengono subito registrate all'interno del sistema di routing; i valori dell'array hanno una sintassi semplice simile a quella utilizzata nella definizione dei package: nel momento della valutazione viene utilizzata come directory di base in cui ricercare il controller il valore dell'opzione di configurazione controllersDir e poi vengono eseguite in serie le seguenti operazioni:

  1. viene suddivisa la stringa corrispondente all'espressione regolare utilizzando come separato il punto, presupponendo che l'ultimo valore corrisponda al nome dell'azione da richiamare, il penultimo alla classe da istanziare e gli altri al path in cui trovare l'implementazione del controller;
  2. viene controllata l'esistenza del path specificato ed in caso negativo viene restituita un'eccezione;
  3. viene composto il path del filesystem che corrisponde alla stringa specificata e vengono restituiti i dati necessari al router per procedere;

Per fare un esempio concreto, la regola:

"^list/(d{4})/(d{2})/?$"  =>  "utils.admin.TestController.show"

verrà invocata nel caso in cui si richiami URL simili:

http://www.example.com/list/2004/01
http://www.example.com/list/2005/01/

E restituirà l'istanza del controller TestController contenuto nella sottocartella utils/admin della root dei controller, su cui verrà invocata l'azione show passandole come parametri 2004 e 01 (nel nostro caso).

In automatico viene rimosso lo slash iniziale dall'URL richiesto.

Conclusioni

Abbiamo concluso la discussione e l'implementazione del sistema base di routing; nel prossimo articolo vedremo come sono definiti ed implementati i controller, come implementare l'applicazione MVC di base e qualche esempio pratico di funzionamento completo.

Ti consigliamo anche