Un semplice Controller Reflective in php.


Home Page | Commenti | Articoli | Faq | Documenti | Ricerca | Archivio | Storie dalla Sala Macchine | Contribuire | Imposta lingua: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.


I commenti sono aggiunti quando e soprattutto se ho il tempo di guardarli e dopo aver eliminato le cagate, spam, tentativi di phishing et similia. Quindi non trattenete il respiro.

1 messaggio this document does not accept new posts

stecolna

Ottimo articolo Di stecolna postato il 08/09/2009 13:19

Bravo Riccardo,
la metodologia nei progetti di grosse dimensioni serve.
Ancora complimenti -- stecolna

Precedente Successivo

Volete contribuire? Leggete come!.
 
 

Il presente sito e' frutto del sudore della mia fronte (e delle mie dita), se siete interessati a ripubblicare uno degli articoli, documenti o qualunque altra cosa presente in questo sito per cortesia datemene comunicazione (o all'autore dell'articolo se non sono io), cosi' il giorno che faccio delle aggiunte potro' avvisarvi e magari mandarvi il testo aggiornato.


Questo sito era composto con VIM, ora e' composto con VIM ed il famosissimo CMS FdT.

Questo sito non e' ottimizzato per la visione con nessun browser particolare, ne' richiede l'uso di font particolari o risoluzioni speciali. Siete liberi di vederlo come vi pare e piace, o come disse qualcuno: "Finalmente uno dei POCHI siti che ancora funzionano con IE5 dentro Windows 3.1".

Web Interoperability Pleadge Support This Project
Powered By Gojira