Un semplice Controller Reflective in php.


Home Page | Comments | Articles | Faq | Documents | Search | Archive | Tales from the Machine Room | Contribute | Set language to:en it | Login/Register


Circa un annetto fa mi sono trovato a lavorare come consulente programmatore su una gigantesca web application php usata da $noto_ente_certificatore_lingua_inglese, siccome praticamente tutta la mia esperienza come programmatore era al di fuori delle applicazioni web, le due cose su cui inizialmente ho trovato il maggior ribrezzo le maggiori difficolta' sono state la necessita' di produrre l'html all'interno del software e il problema di collegare la Response corretta alla determinata Request.

Nel primo caso per fortuna ho quasi sempre potuto usare inizialmente un template engine chiamato Smarty e successivamente, passati ad AJAX, javascript per la generazione dell'HTML dinamico (per fortuna l'applicazione e' ad uso interno e non dobbiamo preoccuparci di browser non javascript).

Per quanto riguarda il secondo problema la questione e' stata subito piu' complessa: il sistema "degli antichi" di usare script diversi per le varie pagine proprio non mi piaceva: troppo codice necessitava di essere ripetuto e anche includendo i file il risultato come compattezza, leggibilita' e chiarezza del codice lasciava molto a desiderare. A questo punto ho iniziato a cercare soluzioni alternative.

Il problema di associare l'azione corretta a una determinata richiesta e' quello che nel paradigma MVC, che al momento e' quello che va per la maggiore nella scrittura di web application, e' contenuto nella parte C, ovvero Controller, ed esistono ottime librerie che si occupano di questo. Tuttavia, come nel mio caso, puo' capitare che non si possa o non si voglia usarle.

Il primo metodo che ho esplorato e' quello di usare un unico index.php a cui vengono passate due variabili nel get: page e action. Queste variabili sono usate all'interno di un mastodontico case che si occupa di associare alle coppie di page e action la corretta semantica.



//switch sulle page
switch( $_REQUEST[ 'page' ] ){

    case 'news':

       //switch sulle action della page news
        switch( $_REQUEST['action'] ){
        
            case 'add':
                $news = get_news( $_REQUEST );
                add_news( $news );
                write_output();
                break;
                
            case 'del':
                
                del_news();
                write_output();
                break;
            
        }
        
        break;
        
    case 'guesbook':
    
        //switch sulle action della page guestbook
        switch( $_REQUEST['action'] ){
        
            case 'add':
            
                $gb = get_guestbook_entry( $_REQUEST )
                add_guestbook_entry( $gb );
                write_output();
                break;
                
            case 'del':
                
                del_guestbook_entry( $_REQUEST[ 'id' ] );
                write_output();
                break;
            
        }    

}

A questo punto se chiamiamo "index.php?page=news&action=add" effettivamente vengono eseguite le azioni corrette e tutto viene fatto in un singolo file di index per cui posso mettere in un unico punto tutte le funzioni come che si ripetono per ogni pagina (una per tutte: il controllo che l'utente sia loggato). Purtroppo pero' il codice risultante e' quantomeno poco agevole: abbiamo un mega case gigantesco con dentro altri mega case giganteschi nei quali c'e' il codice da eseguire. Pensate in un'applicazione reale che ha decine di pagine con decine di azioni possibili: fare una ricerca per chi fa cosa dove e' un incubo e mettere o togliere una parentesi graffa nei blocchi di case e' sempre a rischio di sbagliare, rovinare la struttura del case e non capirci piu' nulla.

Soprattutto se chi scrivo il codice e' cosi' stitico con i commenti eh??

Piu' o meno in questo periodo mi rendo conto che non c'e' motivo per cui, allo stesso modo di tutto il resto delle applicazioni, questa parte non debba essere anche lei in una classe, fatto questo e in seguito delle considerazioni precedenti la mia struttura diventa:



class Index{

   //properties di page & action
   private $page;
   private $action;
 
 /*
  *Qui va altra roba che puo' essere definita globalmente
  *come le interfacce ai db o l'oggetto per gestire
  *il template engine.
  */
  
    public function Index(){
  
        //inizializzazione di page è action dalla request;
        $this->page = $_REQUEST[ 'page' ];
        $this->action = $_REQUEST[ 'action' ];
  
  
        /*
         * Inizializzazione della suddetta altra roba
         */
  
    }


    /*
      *Funzione che si occupa di capire cosa fare
      *in base a page è action, usa il solito case
      *ma in questo caso chiama un metodo per ogni caso.
      */
    public function displayPage() {
    
        switch ( $this->page ){
            case 'news':
                
                switch( $this->action ){
                
                    case 'add':
                    
                        $this->indexNewsAdd();
                        break;
                        
                    case 'del':
                        
                        $this->indexNewsDel();
                        break;
                    
                }
                
                break;
                
            case 'guesbook':
                
                switch( $this->action ){
                
                    case 'add':
                    
                        $this->indexGuestbookAdd();
                        break;
                        
                    case 'del':
                        
                        $this->indexGuestbookDel();
                        break;
                    
                }
                break;
              
        }
    
    }

    /*
     *Metodi relativi ad ogni coppia di page è action
     */
    private function indexNewsAdd () {
    
        $news = get_news( $_REQUEST );
        add_news( $news );
        write_output();
    
    }
    
    private function indexNewsDel () {
    
        del_news();
        write_output();
    
    }
    
    private function indexGuestbookAdd() {
    
        $gb = get_guestbook_entry( $_REQUEST )
        add_guestbook_entry( $gb );
        write_output();
    
    }
    
    private function indexGuestbookDel() {
    
        del_guestbook_entry( $_REQUEST[ 'id' ] );
        write_output();
    
    }

}

/*
 * Questa parte va in un file a se che e' il vero index.php, nei successivi
 * snippet di codice verra' omessa;
 */

    $index = new Index();
    $index->callPage();

Ok, ora va un po meglio: c'e' una chiara distinzione fra il codice che esegue le azioni e il case che decide che codice eseguire, tutto e' piu' comprensibile e ricerche e modifiche sul codice sono piu' agevoli.

Tuttavia non sono ancora contento: per 2 pagine e 4 azioni ho dovuto scrivere un case di piu' di 30 righe (spaziature incluse). Sono convinto che si puo' fare di meglio, ed'e' a questo punto che mi viene in mente di usare un pizzico di reflection:



class Index{

    //page & action
    private $page;
    private $action;
    
    /*
     *Array che contiene i riferimenti
     *ai metodi da chiamare per pagine/azioni
     */
    private $pages;
    
    public function Index(){
    
        $this->page = $_REQUEST[ 'page' ];
        $this->action = $_REQUEST[ 'action' ];
    
    
        /*
         *riferimento alla classe corrente
         *per chiamare i metodi tramite reflection
         */
         
        $thisRef = new ReflectionClass($this);
        
        /*
          *Creaiamo un array con le pages e per ogni 
          *page un array con le action contenenti, infine,
          *in corrispondenta delle varie action i riferimenti
          *reflective dei metodi da chiamare.
          */
        $this->pages=array(
        
            'news'=>array(
		    		    
                'add'=>$thisRef->getMethod("indexNewsAdd"),
                'del'=>$thisRef->getMethod("indexNewsDel"),
            ),
                        
            'guestbook'=>array(
 		    		      
                'add'=>$thisRef->getMethod("indexGuestbookAdd"),
                'del'=>$thisRef->getMethod("indexGuestbookDel"),
 		    		      
            ),
        );
    
    }
    
    public function displayPage() { 
    
        if(
        
            isset($this->pages[$this->page])
            &&
            isset($this->pages[$this->page][$this->action])
           
            )
            
          $this->pages[$this->page][$this->action]->invoke($this);
          
        else
        
          $this->catchAll();
    
    }
    
    private function catchAll(){
    
        die("Page not found");
        
    }
    
    /*
      *I metodi ora sono pubblici per permettere alla reflection
      *di accedervi.
      *Con php 5.3.0 e' possibile in realta' usare i privileged
      *accessor per aggirare questa limitazione.
      */

    public function indexNewsAdd () {
    
        $news = get_news( $_REQUEST );
        add_news( $news );
        write_output();
    
    }
    
    public function indexNewsDel () {
    
        del_news();
        write_output();
    
    }
    
    public function indexGuestbookAdd() {
    
        $gb = get_guestbook_entry( $_REQUEST )
        add_guestbook_entry( $gb );
        write_output();
    
    }
    
    public function indexGuestbookDel() {
    
        del_guestbook_entry( $_REQUEST[ 'id' ] );
        write_output();
    
    }
    
}

Finalmente comincio ad essere un abbastanza soddisfatto. La parte di codice che definisce la struttura delle pagine e' compatta e molto comoda sia da scrivere e da leggere. Certo se avessi php 5.3.0 e le lambda espressioni verrebbe tutto piu' pulito (non sarei costretto a usare una stringa col nome del metodo per ottenere il riferimento) ma tutto sommato mi sembra che le cose vadano. Ora pero' vorrei non dover riscrivere tutto questo codice ogni volta che faccio una nuova applicazione, quindi faccio una classe che io possa estendere per ottenere questa struttura:



class BaseIndex {

    private $page;
    private $action;
    
    private $thisRef;

    public function BaseIndex() {
    
        $this->pages=array();

        $this->page = $_REQUEST[ 'page' ];
        $this->action = $_REQUEST[ 'action' ];
        
        $this->thisRef = new ReflectionClass( get_class($this) );

    }

    /*
     *E' possibile ridefinire la funzione
     *catchAll() in modo da utilizzare qualcosa
     *di piu' raffinato.
     */
    protected function catchAll() {
  
        die( "Page not found");
    
    }

    public function callPage() {
  
        if(
        
            isset( $this->pages[ $this->page ] )
            &&
            isset( $this->pages[ $this->page ][ $this->action ] )
            
        )
        
            $this->pages[ $this->page ][ $this->action ]->invoke( $this );

        else
        
            $this->catchAll();
            
    }

    public function addPage( $page, $action ) {
    
        /*
         * Add page permette alla classe erede
         * di aggiungere una nuova pagina.
         * Il metodo e' automaticamente aggiunto
         * calcolando il nome che deve avere.
         */
    
        if( isset( $this->pages[ $page ] ) )
        
            $this->pages[ $page ][ $action ] = $this->thisRef->getMethod( "index".ucfirst($page).ucfirst($action) );
            
        else
        
            $this->pages[$page]=array( $action=>$this->thisRef->getMethod( "index".ucfirst($page).ucfirst($action) ) );
    
    }

}

Ora per ottenere il nostro nuovo index non dobbiamo fare altro che estendere questa classe, implementare i metodi che rappresentano pagine e azioni e aggiungerle:



class Index extends BaseIndex {

    public function Index() {
    
        //inizializziamo la classe parent.
        parent::__construct();
        
        //aggiungiamo le pagine con l'apposito metodo


        //news
        $this->addPage( 'news', 'add' );
        $this->addPage( 'news', 'del' );
        
        //bookmark
        $this->addPage( 'guestbook', 'add' );
        $this->addPage( 'guestbook', 'del' );
    
    }

    //metodi delle pagine
    
    public function indexNewsAdd () {
    
        $news = get_news( $_REQUEST );
        add_news( $news );
        write_output();
    
    }
    
    public function indexNewsDel () {
    
        del_news();
        write_output();
    
    }
    
    public function indexGuestbookAdd() {
    
        $gb = get_guestbook_entry( $_REQUEST )
        add_guestbook_entry( $gb );
        write_output();
    
    }
    
    public function indexGuestbookDel() {
    
        del_guestbook_entry( $_REQUEST[ 'id' ] );
        write_output();
    
    }

}

Ed eccoci giunti alla fine. Finalmente possiamo specificare la struttura delle pagine con pochissimo codice e separando la loro implementazione. Per quanto questa soluzione non possa minimanete essere paragonata a librerie piu' raffinate come Zend_Controller in termini di potenza e flessiblita', fornisce comunque una buon modo di gestire la struttura delle pagine senza dover dipendere da giganteschi framework e senza costringere l'intera applicazione in rigide strutture.

A rigor di logica, comunque, sarebbe possibile eliminare l'array $pages e il metodo addPage e usare direttamente la reflection per chiamare il metodo corrispondente alle variabili page e action se definito o intercettare l'eccezione sollevata nel caso il metodo non esista, richiamando catchAll(). Non ho usato questo approccio perche' mi pare utile ai fini della leggibilita' e della robustezza del codice che ci sia un punto dove sono elencate tutte le pagine/azioni che fanno parte dell'applicazione.


Comments are added when and more important if I have the time to review them and after removing Spam, Crap, Phishing and the like. So don't hold your breath. And if your comment doesn't appear, is probably becuase it wasn't worth it.

1 message  this document does not accept new posts

stecolna

Ottimo articolo By stecolna posted 08/09/2009 13:19

Bravo Riccardo,

la metodologia nei progetti di grosse dimensioni serve.

Ancora complimenti -- stecolna


Previous Next

Do you want to contribute? read how.  
 


This site is made by me with blood, sweat and gunpowder, if you want to republish or redistribute any part of it, please drop me (or the author of the article if is not me) a mail.


This site was composed with VIM, now is composed with VIM and the (in)famous CMS FdT.

This site isn't optimized for vision with any specific browser, nor it requires special fonts or resolution.
You're free to see it as you wish.

Web Interoperability Pleadge Support This Project Powered By Gigan