martedì 24 dicembre 2013

La OOP in PHP spiegata in un esempio - Parte 16 - Preservare gli oggetti per la durata della sessione


Il PHP è di norma utilizzato come linguaggio per applicazioni web. Ciò significa che il client (un browser) tenta di accedere ad un pagina, il server web riconosce che la pagina è uno script php e lo da in pasto al motore php, quindi l'output prodotto dallo script è trasmesso dal server web al client.

Cosa succede agli oggetti generati dalla nostra applicazione tra una chiamata e l'altra?
Dato che lo script è eseguito dalla prima all'ultima riga e terminato lo script corrente tutta la memoria occupata dallo stesso è rilasciata, anche gli oggetti vengono distrutti e perduti.

Non a caso quando occorre trattenere delle informazioni tra chiamate successive, l'area tampone per eccellenza in cui memorizzarle è la variabile superglobal $_SESSION, ossia un'area di archiviazione temporanea esistente per la durata della sessione del client (da quando è aperto il browser sul primo script php e fino a quando il browser non è chiuso).

Gli oggetti però non possono essere direttamente assegnati alla superglobal $_SESSION come si è abituati a fare per le comuni variabili. Ciò perchè, di norma, la variabile $_SESSION è un semplice file di testo sul server avente per nome un id di sessione memorizzato sul client sotto forma di cookie. In altri termini, quando si invoca session_start(), PHP accede al cookie memorizzato e lo utilizza per accedere al file di testo e popolare la variabile $_SESSION. Dato che un oggetto memorizza un riferimento alla locazione di memoria che custodisce l'istanza di una classe contiene dati e metodi, in cui i dati a loro volta possono essere oggetti, occorre un sistema che trasformi in testo un oggetto salvandone il solo stato (i dati) e i nomi delle classi necessarie. Tale compito è affidato alla funzione serialize(), capace di trasformare l'area dati di un oggetto in un stringa di testo. Prima di poter essere riutilizzati, gli oggetti serializzati devono essere sottoposti al  processo inverso tramite unserialize().

Alla luce di quanto detto proviamo a scrivere uno script di test che ci permetta di aggiungere e rimuovere a piacimento degli oggetti da un'area di disegno.

Per prima cosa avremo bisogno di un classe che permetta di collezionare delle figure per poi mandarle in stampa su un'area di disegno. A tal fine definiamo la classe cCollezioneFigure.

<?php

class cCollezioneFigure extends aFigura implements Iterator, ArrayAccess{
    private $figure=[];
         
    //Definizione del metodo astratto di aFigura
    public function disegna(\iAreaDiDisegno $oAreaDiDisegno) {
        foreach($this as $figura)
            $figura->disegna($oAreaDiDisegno);
    }
 
    //Interfaccia ArrayAccess
    public function offsetSet($offset,$value) {
        if(is_a($value, 'aFigura')){
            if (is_null($offset))
                $this->figure[] = clone $value;
            else
                $this->figure[$offset] = clone $value;      
        }else
            throw new Exception("La classe ".__CLASS__." colleziona solo elementi di tipo aFigura ");
    }
 
    public function offsetExists($offset) {
        return isset($this->figure[$offset]);
    }
 
    public function offsetUnset($offset) {
        if(isset($this->figure[$offset]))
            unset($this->figure[$offset]);
    }
 
    public function offsetGet($offset) {
        if(isset($this->figure[$offset]))
            return $this->figure[$offset];
        else
            throw new Exception("Si tenta di accedere ad un elemento aFigura inesistente");
    }
 
    //Interfaccia Iterator
    public function current() {
        return current($this->figure);
    }

    public function key() {
        return key($this->figure);
    }

    public function next() {
        return next($this->figure);
    }

    public function rewind() {
        return reset($this->figure);
    }

    public function valid() {
        return current($this->figure) !== false;
    }
}
?>

Questa classe estende aFigura, ossia è essa stessa interpretabile come figura implementando il metodo disegna(). Tale metodo non fa altro che iterare attraverso gli oggetti di tipo aFigura caricati invocando per ognuno il metodo disegna(). Implementa per comodità le interfacce predefinite Iterator e ArrayAccess. La prima la conosciamo già e ci permette di utilizzare un oggetto in un ciclo foreach. Il secondo invece permette di utilizzare l'oggetto come se fosse un array per ciò che attiene l'aggiunta e la rimozione di elementi. In questo caso è stato definito offsetSet(), metodo invocato quando si tenta di memorizzare un elemento nell'array, affinché accetti solo oggetti di tipo aFigura. Ciò garantisce la correttezza logica del metodo disegna() della classe.

Ora che abbiamo questo oggetto che colleziona le nostre immagini, e che dovremo preservare durante la sessione, non resta che implementare un script di test
Esempio dello script di test precedente in esecuzione
Lo script di test, come si vede dall'immagine fa un po' di cose, e quindi risulta un po' lungo.

<?php
session_start();

require_once './cAreaDiDisegnoHTML.php';
require_once './cPoligono.php';
require_once './cColore.php';
require_once './cCoordinate.php';
require_once './cPunto.php';
require_once './cPoligonoRettangolo.php';
require_once './cRettangolo.php';
require_once './cTriangoloEquilatero.php';
require_once './cFabbricaPoligoni.php';
require_once './aFigura.php';
require_once './aFiguraTrasformabile.php';
require_once './cCollezioneFigure.php';

//funzona d'utilità atta a riproporre i valori del post precedetne nei relativi campi
function Riproponi($nome, $default) {
    if (!is_array($nome))
        $nome = [$nome];

    $ultimoValore = $_POST;
    $nome = array_reverse($nome);

    while (!empty($nome))
        if (isset($ultimoValore[$key = array_pop($nome)]))
            $ultimoValore = $ultimoValore[$key];
        else
            return htmlspecialchars($default);

    return htmlspecialchars($ultimoValore);
}

//Inizializzazione
if (!isset($_SESSION['CollezioneFigure']))
    $collezione = new cCollezioneFigure ();
else  
    $collezione = unserialize($_SESSION['CollezioneFigure']);

$ad = new cAreaDiDisegnoHTML(50, 50);

//Esecuzioni richieste da visualizzazione precedente
try {  
    //Rimozione
    if (isset($_POST['Rimuovi']) and isset($_POST['id']))
        unset($collezione[(int) $_POST['id']]);
    //Clonazione
    if (isset($_POST['Clona']) and isset($_POST['id']))
        $collezione[]=clone $collezione[(int) $_POST['id']];  
    //Traslazione
    $direzioni = ['Su','Giu','Destra','Sinistra'];
    if(isset($_POST['Direzione']) and in_array($_POST['Direzione'], $direzioni)){
        echo 'Ciao';
        $elemento = $collezione[$_POST['id']];
        switch ($_POST['Direzione']) {
            case $direzioni[0]:
                $elemento->trasla(0, -1);
                break;
            case $direzioni[1]:
                $elemento->trasla(0, 1);
                break;
            case $direzioni[2]:
                $elemento->trasla(1,0);
                break;
            case $direzioni[3]:
                $elemento->trasla(-1,0);
                break;
        }              
    }
    //Rotazione
    if (isset($_POST['Ruota']) and isset($_POST['id']) and isset($_POST['angolo'])){      
        $_POST['angolo'] = ((int)$_POST['angolo'] - 1) % 360 + 1;
        $elemento = $collezione[$_POST['id']];
        $CentroRotazione = new cCoordinate($elemento->getOrigine()->getX(), $elemento->getOrigine()->getY());      
        $elemento->ruota($CentroRotazione,$_POST['angolo']);
    }
    //Aggiunta Rettangolo
    if (isset($_POST['AggRet'])) {
        $annulla = '';
        if (isset($_POST['c']) and is_array($_POST['c']) and count($_POST['c'] == 3)) {
            foreach ($_POST['c'] as $k => $c)
                $_POST['c'][$k] = ((int) $_POST['c'][$k] - 1) % 255 + 1;
        }
        else
            $annulla .= 'Ci sono problemi con i codici colori inseriti';

        if (isset($_POST['base']))
            $_POST['base'] = (int) $_POST['base'];
        else
            $annulla .= 'Ci sono problemi con la base. ';
        if (isset($_POST['altezza']))
            $_POST['altezza'] = (int) $_POST['altezza'];
        else
            $annulla = 'Ci sono problemi con l\'altezza. ';
        if (isset($_POST['x']) and isset($_POST['y'])) {
            $_POST['x'] = (int) $_POST['x'];
            $_POST['y'] = (int) $_POST['y'];
        }
        else
            $annulla = 'Ci sono problemi con le coordinate dell\'origine';
        if (empty($annulla))
            $collezione[]=new cRettangolo(new cCoordinate($_POST['x'], $_POST['y']), $_POST['base'], $_POST['altezza'], new cColore($_POST['c'][0], $_POST['c'][1], $_POST['c'][2]), new cPoligono(new cColore(0, 0, 0)));
    }
    //Aggiunta Triangolo Equilatero
    if (isset($_POST['AggTriEqui'])) {
        $annulla = '';
        if (isset($_POST['cte']) and is_array($_POST['cte']) and count($_POST['cte'] == 3)) {
            foreach ($_POST['cte'] as $k => $c)
                $_POST['cte'][$k] = ((int) $_POST['cte'][$k] - 1) % 255 + 1;
        }
        else
            $annulla .= 'Ci sono problemi con i codici colori inseriti';

        if (isset($_POST['lato']))
            $_POST['lato'] = abs((int) $_POST['lato']);
        else
            $annulla .= 'Ci sono problemi con la base. ';

        if (isset($_POST['xte']) and isset($_POST['yte'])) {
            $_POST['xte'] = (int) $_POST['xte'];
            $_POST['yte'] = (int) $_POST['yte'];
        }
        else
            $annulla = 'Ci sono problemi con le coordinate dell\'origine';
        if (empty($annulla))
            $collezione[]=new cTriangoloEquilatero(new cCoordinate($_POST['xte'], $_POST['yte']), $_POST['lato'], new cColore($_POST['cte'][0], $_POST['cte'][1], $_POST['cte'][2]), new cPoligono(new cColore(0, 0, 0)));
    }
} catch (Exception $e) {
    $messaggio = str_replace("'", '\\\'',$e->getMessage());
    echo <<<EOT
    <script>alert('$messaggio');</script>
EOT;
}

//Operazioni conclusive
$collezione->disegna($ad);
$_SESSION['CollezioneFigure'] = serialize($collezione);
?>

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Forme Geometriche</title>    
        <style>
            td{
                vertical-align: top;
            }
            form{
                display: inline;
                float: left;
            }
            .clear{
                clear: both;
            }          
        </style>
    </head>
    <body>
        <h1>Forma Geometriche</h1>
        <table>
            <tr>
                <th>Area di Disegno (50x50 caratteri)</th>
                <th>Elenco Figure</th>
            </tr>
            <tr>
                <td>
                    <!-- Visualizzazione dell'area di disegno -->
                    <?php echo $ad->output(); ?>
                </td>
                <td style="width: 80em;">
                    <!-- Visualizzazione dell'ellenco delle figure aggiunte e delle operazioni disponibili -->
                    <?php
                    foreach ($collezione as $id => $figura) {
                        $class = get_class($figura);
                     
                        echo "<fieldset><legend>id: $id - Oggetto $class</legend>";
                     
                        if(is_a($figura, "iTrasformabile")){
                            echo <<<EOT
                            <form method="post" action="$_SERVER[PHP_SELF]">
                              <input type="hidden" name="id" value="$id">                            
                              <label title="Angolo di rotazione">Angolo:<input type="text" name="angolo" value="0" size="2"></label>
                              <input type="submit" name="Ruota" value="Ruota">
                            </form>
                            <form method="post" action="$_SERVER[PHP_SELF]">
                              <input type="hidden" name="id" value="$id">                            
                              <input type="submit" name="Direzione" value="Su">
                            </form>
                            <form method="post" action="$_SERVER[PHP_SELF]">
                              <input type="hidden" name="id" value="$id">                            
                              <input type="submit" name="Direzione" value="Giu">
                            </form>
                            <form method="post" action="$_SERVER[PHP_SELF]">
                              <input type="hidden" name="id" value="$id">                            
                              <input type="submit" name="Direzione" value="Destra">
                            </form>
                            <form method="post" action="$_SERVER[PHP_SELF]">
                              <input type="hidden" name="id" value="$id">                            
                              <input type="submit" name="Direzione" value="Sinistra">
                            </form>
EOT;
                        }
                     
                        echo <<<EOT
                        <form method="post" action="$_SERVER[PHP_SELF]">
                            <input type="hidden" name="id" value="$id">
                            <input type="submit" name="Rimuovi" value="Rimuovi">
                        </form>
                        <form method="post" action="$_SERVER[PHP_SELF]">
                            <input type="hidden" name="id" value="$id">
                            <input type="submit" name="Clona" value="Clona">
                        </form>
EOT;
                     
                        echo '</fieldset><div class="clear"></div>';
                    }
                    ?>
                </td>
            </tr>          
        </table>
        <!-- Form di aggiunta immagini -->
        <h2>Area di Aggiunta delle Immagini</h2>
        <form method="post" action="<?php echo$_SERVER['PHP_SELF'];?>">          
            <div style="width:15em; margin-bottom: 2em;">
                <fieldset>
                    <legend>Rettangolo</legend>
                    <label><div class="eticehtta">Origine X:</div><input type="text" name="x" value="<?php echo Riproponi('x', 0); ?>"/></label><br>              
                    <label>Origine Y:<input type="text" name="y" value="<?php echo Riproponi('y', 0); ?>"/></label><br>
                    <label>Base:<input type="text" name="base" value="<?php echo Riproponi('base', 0); ?>"/></label><br>
                    <label>Altezza:<input type="text" name="altezza" value="<?php echo Riproponi('altezza', 0); ?>"/></label><br>                                          
                    <label>Colore R:<input type="text" name="c[]" value="<?php echo Riproponi(['c', 0], 0); ?>" /></label><br>              
                    <label>Colore G:<input type="text" name="c[]" value="<?php echo Riproponi(['c', 1], 0); ?>" /></label><br>              
                    <label>Colore B:<input type="text" name="c[]" value="<?php echo Riproponi(['c', 2], 0); ?>" /></label><br>              
                </fieldset>
            </div>          
            <div>
                <input type="submit" name="AggRet" value="Aggiungi Rettangolo" />
            </div>                
        </form>
        <form method="post" action="<?php echo$_SERVER['PHP_SELF'];?>">          
            <div style="width:15em; margin-bottom: 2em;">
                <fieldset>
                    <legend>Triangolo Equilatero</legend>
                    <label>Origine X:<input type="text" name="xte" value="<?php echo Riproponi('xte', 0); ?>"/></label><br>              
                    <label>Origine Y:<input type="text" name="yte" value="<?php echo Riproponi('yte', 0); ?>"/></label><br>
                    <label>Lato:<input type="text" name="lato" value="<?php echo Riproponi('lato', 0); ?>"/></label><br>                                                    
                    <label>Colore R:<input type="text" name="cte[]" value="<?php echo Riproponi(['cte', 0], 0); ?>" /></label><br>              
                    <label>Colore G:<input type="text" name="cte[]" value="<?php echo Riproponi(['cte', 1], 0); ?>" /></label><br>              
                    <label>Colore B:<input type="text" name="cte[]" value="<?php echo Riproponi(['cte', 2], 0); ?>" /></label><br>              
                </fieldset>
            </div>          
            <div>
                <input type="submit" name="AggTriEqui" value="Aggiungi Triangolo Equilatero" />
            </div>                
        </form>
        <div class="clear"></div>    
        <h2>Debug</h2>
        <p><?php      
        var_dump($_POST);
        var_dump($_SESSION);
        ?></p>      
    </body>
</html>

Per comodità ho colorato in verde i commenti che ne suddividono la logica. Soffermiamoci sulle istruzioni in rosso, che permettono di memorizzare l'oggetto in sessione e ci permettono di recuperarlo che si trovano nello script PHP prima del codice HTML. Per comodità le riporto sotto.

...
//Inizializzazione
if (!isset($_SESSION['CollezioneFigure']))
    $collezione = new cCollezioneFigure ();
else  
    $collezione = unserialize($_SESSION['CollezioneFigure']);

$ad = new cAreaDiDisegnoHTML(50, 50);
...
//Operazioni conclusive
$collezione->disegna($ad);
$_SESSION['CollezioneFigure'] = serialize($collezione);
 ...

Come si può osservare è creato un oggetto cCollezioneFigure se non disponibile in sessione. Dopo tutte le attività di script tale oggetto e memorizzato in sessione. Tra una chiamata e l'altra è recuperato per poter essere ulteriormente utilizzato e aggioranto. Niente di più, quindi un serialize($collezione) prima di assegnarlo a $_SESSION e un unserialize() prima di recuperarlo da $_SESSION.

Lo script fa uso di una classe cTriangoloEquilatero, realizzata sulla falsa riga di cRettangolo per arricchire l'editor. Per completezza si inserisce anche il codice di tale classe:
<?php
require_once './aFiguraTrasformabile.php';
require_once './iColorabile.php';
require_once './iColore.php';
require_once './tColorabile.php';

class cTriangoloEquilatero extends aFiguraTrasformabile implements iColorabile {
 
    use tColorabile;
 
    private $oPoligono, $lato = 1, $oOrigine;
    private $trasform=array();

    public function __construct(\iCoordinate $oOrigine, $lato, \iColore $oColore, \iPoligono $oPoligono) {
        $this->setOrigine($oOrigine);
        $this->setLato($lato);      
        $this->setColore($oColore);
        $this->setPoligono($oPoligono);
    }

    public function setPoligono(\iPoligono $oPoligono) {
        $this->oPoligono = clone $oPoligono;
    }

    private function getPoligono() {
        return $this->oPoligono;
    }

    public function setOrigine(\iCoordinate $oOrigine) {
        $this->oOrigine = clone $oOrigine;
    }

    public function getOrigine() {
        return $this->oOrigine;
    }

    public function getAltezza() {
        return $this->altezza;
    }

    public function setLato($lato) {
        $lato = (int) $lato;
        if ($lato > 0)
            $this->lato = $lato;
        else
            throw new Exception("La base di un rettangolo deve essere un intero maggiore di 0");
    }

    public function getBase() {
        return $this->lato;
    }

    private function calcoloVertici() {
        $this->getPoligono()->delVertici();
        $this->getPoligono()->setVertice($puntoA = new cCoordinateTrasforamabile($this->getOrigine()->getX(), $this->getOrigine()->getY()));
        $this->getPoligono()->setVertice($puntoB = new cCoordinateTrasforamabile($this->getOrigine()->getX()+$this->getBase(), $this->getOrigine()->getY()));
        $puntoC = clone $puntoB;
        $puntoC->ruota($puntoA, -60);
        $this->getPoligono()->setVertice($puntoC);
     
        foreach ($this->trasform as $func)
            for($v=0; $v<$this->getPoligono()->numVertici();$v++)
                switch ($func[0]) {
                    case "ruota":
                        $this->getPoligono()->getVertice($v)->ruota($func[1][0], $func[1][1]);
                        break;
                    case "trasla":
                        $this->getPoligono()->getVertice($v)->trasla($func[1][0], $func[1][1]);
                        break;
                    case "scala":
                        $this->getPoligono()->getVertice($v)->scala($func[1][0], $func[1][1], $func[1][2]);
                        break;
                }
    }

    //Interfaccia iFigura
    public function disegna(\iAreaDiDisegno $oAreaDiDisegno) {
        $this->calcoloVertici();      
                                 
        $oAreaDiDisegno->tracciaSegmento($this->getPoligono()->getVertice(0), $this->getPoligono()->getVertice(1), $this->getColore());
        $oAreaDiDisegno->tracciaSegmento($this->getPoligono()->getVertice(1), $this->getPoligono()->getVertice(2), $this->getColore());
        $oAreaDiDisegno->tracciaSegmento($this->getPoligono()->getVertice(2), $this->getPoligono()->getVertice(0), $this->getColore());      
    }
 
    public function trasla($dx, $dy) {
        $this->trasform[]=["trasla",[$dx,$dy]];
    }
 
    public function scala(\iCoordinate $oPuntoDiFuga, $scalaX, $scalaY) {
        $this->trasform[]=["scala",[$oPuntoDiFuga, $scalaX, $scalaY]];      
    }
 
    public function ruota(\iCoordinate $oCentroRotazione, $angolo) {
        $this->trasform[]=["ruota",[$oCentroRotazione,$angolo]];              
    }
}

?>