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.
Sono passate ormai alcune settimane da quando ho iniziato questa serie di articoli. Siamo partiti da un'introduzione generale al pattern MVC per procedere con la descrizione pratica e teorica di come Taste, il framework MVC che utilizzo d'esempio, sia stato implementato. L'ultima volta mi sono congedato ad un passo dal terminare l'applicazione MVC; abbiamo implementato il sistema di routing con il compito di recuperare i controller nel filesystem e restituirli all'applicazione chiamante, ed oggi ci occuperemo di implementare questa applicazione e la classe astratta Controller
che sta alla base di tutti i controller che andremo ad implementare, indipendentemente dal tipo di router che andrà ad identificarne la posizione nel filesystem.
Per prima cosa descriverò brevemente il Controller base, per poi passare alla classe MVCApplication, che è per l'appunto l'implementazione della classe Application
che si occupa di trasformare una richiesta in un controller e di eseguirlo.
Implementazione del Controller
Il Controller
è una classe astratta molto semplice che ha come obiettivo quello di esporre una serie di metodi pubblici che verranno invocati dall'applicazione in base alle informazioni fornite dal Router
. Espone alcuni metodi di utilità che servono per le operazioni più comuni come il redirect delle richieste (sia interno che http, utilizzando il sistema delle eccezioni descritto negli articoli precedenti) ed il recupero dei parametri passati in fase di costruzione.
Oltre a questo ho voluto aggiungere una semplice funzionalità che permette di definire, nella docstring di un metodo, una serie di proprietà che possono essere utilizzate da implementazioni specifiche di Controller per guidare il comportamento (per esempio potrebbero contenere informazioni sui permessi per un ipotetico PrivateController). Una proprietà è definita dal carattere @
seguito dal nome della proprietà e dal suo valore, separati con i due punti (:
).
Come al solito potete scaricare l'ultima versione del framework dal link donwload all'inizio dell'articolo.
Vediamo l'implementazione del controller (che potete trovare nel file taste/mvc/Controller.php
):
require_once 'taste/config/Configurable.php'; require_once 'taste/InternalRedirectException.php'; require_once 'taste/HttpRedirectException.php'; abstract class Controller extends Configurable { private $action; private $params; private $property_cache; private $available_actions; public $file_path; public function __construct($action, $params=array()) { parent::__construct(); $ref = new ReflectionClass($this); $this->action = $action; $this->params = $params; $this->property_cache = array(); $this->available_actions = array(); $this->file_path = $ref->getFileName(); foreach($ref->getMethods() as $method) { if($method->getDeclaringClass() != $ref or !$method->getDocComment()) continue; $cache = array(); $comment = $method->getDocComment(); preg_match_all("/@(([a-zA-Z_][a-zA-Z_0-9]*)(.[a-zA-Z_][a-zA-Z_0-9]*)*):s*(.*)/m", $comment, $matches); for($i = 0; $i < count($matches[1]); ++$i) $cache[$matches[1][$i]] = $matches[4][$i]; $this->property_cache[$method->name] = $cache; $this->available_actions[] = $method->name; } } public function getProperty($action, $name, $default=null) { if(isset($this->property_cache[$action])) if(isset($this->property_cache[$action][$name])) return $this->property_cache[$action][$name]; return $default; } public function getAction() { return $this->action; } public function listActions() { return $this->available_actions; } public function getParams() { return $this->params; } public function redirect($url, $internal=false) { if($internal) throw new InternalRedirectException($url); else throw new HttpRedirectException($url); } }
Un controller è una classe configurabile che espone come azioni disponibili per l'applicazione i metodi pubblici definiti nelle sue sottoclassi. Il costruttore accetta come argomenti l'azione da eseguire ed i parametri estratti da Router, e li salva al suo interno in modo che possano essere utilizzati successivamente; oltre a salvare questi dati viene analizzata la docstring di tutti i metodi definiti nella sottoclasse, cercando ogni definizione di proprietà e salvandola all'interno di una cache interna. La cache può essere interrogata tramite il metodo getProperty
che accetta come argomenti l'azione da interrogare, il nome della proprietà richiesta ed eventualmente il valore di default da restituire nel caso in cui non venga trovata la proprietà richiesta.
L'applicazione per la gestione del pattern MVC
L'implementazione dell'applicazione MVC è contenuta all'interno del file taste/applications/MVCApplication.php
; la classe MVCApplication è per l'appunto quella che si occupa di invocare il router per ricercare il controller corrispondente alla richiesta inoltrata, ed eseguire il controller nel caso in cui venga restituito correttamente. Il codice è molto breve e molto semplice, dato che la maggior parte delle operazioni essenziali sono contenuto nell'ExplicitRouter che si occupa di trovare il Controller adeguato.
require_once 'taste/Application.php'; require_once 'taste/mvc/Response.php'; require_once 'taste/mvc/routing/Router.php'; require_once 'taste/ControllerNotFoundException.php'; class MVCApplication extends Application { public function __construct(Router $router) { parent::__construct(); $this->router = $router; } public function run(Request $request) { $controller = $this->router->findController($request->getPathInfo()); if(!is_null($controller)) { $action = $controller->getAction(); $params = $controller->getParams(); array_unshift($params, $request); $response = call_user_func_array(array($controller, $action), $params); if(is_string($response)) $response = new Response($response); return $response; } throw new ControllerNotFoundException($request); } }
Il costruttore accetta come parametro un'istanza della classe Router
, rappresentante per l'appunto il router che verrà utilizzato per valutare la richiesta. Il metodo run
viene invocato dal server e si occupa di trovare il Controller adatto attraverso il router ed invocarlo. L'invocazione non è altro che la chiamata al metodo specificato come azione in fase di costruzione del Controller; il risultato di questo metodo può essere un oggetto Response
oppure una stringa: nel secondo caso viene creato un oggetto Response automaticamente. Nel caso in cui non venga trovato un controller corrispondente alla richiesta, viene generata un'eccezione ControllerNotFoundException
che verrà opportunamente gestita dal Server.
Un controller d'esempio
Il sistema è pronto per poter essere utilizzato anche per la produzione di semplici applicativi. Non abbiamo ancora trattato la persistenza degli oggetti e i template, ma possiamo già notare come il sistema risulti abbastanza modulare da coprire molte delle esigenze che si possono presentare quando si decide di organizzare seguendo il pattern MVC un'applicazione.
Implementiamo quindi un controller d'esempio (che trovate in controllers/test.php
) che espone un metodo show
il quale visualizza informazioni su una data passata come argomento:
require_once 'taste/Request.php'; require_once 'taste/mvc/Controller.php'; class TestController extends Controller { /** @prop: valore */ public function show(Request $request, $year=2006, $month=12, $day=1) { return "Anno: ".$year.", Mese: ".$month.", Giorno: ".$day; } }
Questo semplicissimo controller accetta tre argomenti opzionali, che verranno estratti automaticamente dal router durante l'analisi delle richieste HTTP. Per istruire correttamente il router e per assegnare al server la classe MCVApplication come applicazione di base, dobbiamo modificare il file di bootstrap utilizzato fin'ora:
require_once 'taste.php'; require_once 'taste/Server.php'; require_once 'taste/mvc/routing/ExplicitRouter.php'; require_once 'taste/applications/MVCApplication.php'; $router = new ExplicitRouter(array( "^list/?$" => "test.TestController.show", "^list/(d{4})/?$" => "test.TestController.show", "^list/(d{4})/(d{2})/?$" => "test.TestController.show", "^list/(d{4})/(d{2})/(d{2})/?$" => "test.TestController.show", )); $server = new Server(new MVCApplication($router)); $server->run();
Istruiamo il router con delle semplici regole, che in pratica fanno corrispondere a richieste del tipo:
list/2007 list/2007/03 list/2007/03/12
delle chiamate al metodo show della classe TestController
contenuta nel file test.php
passandogli come argomenti i numeri specificati dopo la parola list.
Conclusioni
Abbiamo ora a disposizione qualcosa di abbastanza completo per poter iniziare a fare degli esperimenti seri con il pattern e soprattutto con il framework. Nel prossimo articolo, prima di iniziare a parlare delle viste e del template engine che andremo ad implementare, discuteremo di un concetto molto importate - il caching - e di come risulta semplice aggiungerlo al nostro framework senza dover modificare minimamente il codice già scritto (salvo apportare un paio di modifiche al file di boostrap).