Categorie
PHP

PHP 5.4 – I Traits guida completa

Il problema della duplicazione del codice è sicuramente alla base delle migliori tecniche di programmazione. Il codice duplicato rende la manutenzione del software più difficile, perché ogni codice duplicato deve essere mantenuto costante, e un bug che è stato trovato sul codice duplicato non può essere fixato in un solo punto.

Tuttavia nell’ambito delle programmazione Object Oriented alcuni limiti strutturali di PHP possono rendere difficoltosa la minimizzazione della duplicazione di codice.
PHP non supporta l’ereditarietà multipla (a differenza di linguaggi come Java, C++ e Python), e questo potrebbe portare all’impossibilità di poter riutilizzare determinati metodi su classi che ereditano da un’altra classe e che invece vorrebbero poterli utilizzare.

L’ereditarietà multipla, permette ad una classe di ereditare funzionalità e caratteristiche da più di una classe base. Questa tecnica si contrappone all’ereditarietà singola, in cui una classe può ereditare da una, e solo una, classe base. (Wikipedia)

[box type=”note”]Il fatto che PHP non supporta l’ereditarietà multipla tuttavia può anche essere visto positivamente, dato che permette di limitare complicazioni e problematiche tipiche di questa caratteristica (vedi Diamond Problem).[/box]

I Traits

A partire da PHP 5.4.0, PHP implementa un metodo di riutilizzo del codice chiamato Traits.

I Traits, detti anche Riutilizzo Orizzontale, mirano a ridurre alcune limitazioni sull’ereditarietà singola di PHP, consentendo agli sviluppatori di riutilizzare insiemi di metodi in una sorta di composizione orizzontale del comportamento, senza richiedere ereditarietà.
Un trait non può essere istanziato da solo, per certi versi lo possiamo paragonare ad una classe astratta.

Vediamo un esempio di implementazione, dove due classi Hello e User ereditano rispettivamente da BaseHello e BaseUser:

class BaseHello 
{
    public function hello()
    {
        return 'Hello';
    }        
}
class BaseUser 
{
    public function name($name)
    {
        return $name;
    }        
}
class Hello extends BaseHello
{
}
class User extends BaseUser
{
}

Supponiamo ora, che sia Hello che User hanno bisogno di utilizzare una funzionalità comune, cioè devono implementare il Pattern Singleton. Come abbiamo detto PHP non supporta l’ereditarietà multipla, quindi non potremmo incapsulare la funzionalità all’interno di un’ulteriore classe visto che poi sia Hello che User non la potranno ereditare.
Attraverso un trait possiamo evitare di duplicare il codice di implementazione di tale funzionalità. Vediamo:

trait Singleton {
    private static $_instance;
    public static function getInstance() {
        if (!isset(self::$_instance)) {
            $class = __CLASS__;
            self::$_instance = new $class;
        }

        return self::$_instance;
    }       
}
class Hello extends BaseHello
{
    use Singleton;
}
class User extends BaseUser
{
    use Singleton;
}

$h = Hello::getInstance();
$u = User::getInstance();
echo $h->hello() . ' ' . $u->name('Simone'); //Hello Simone

L’esempio parla da solo, il trait Singleton esegue una banale implementazione del Pattern Singleton. Richiamandolo all’interno di una qualsiasi classe (attraverso la parola chiave use) possiamo agilmente sfruttarne i metodi, senza quindi essere costretti a duplicare il codice di implementazione del pattern in entrambe le classi.

Questo esempio mostra anche come la costante magica __CLASS__ utilizzata all’interno del trait, restituisca il nome della classe. A tal proposito probabilmente il modo migliore per capire i traits e come usarli è quello di guardarli come un semplice copia incolla all’interno della classe. L’esempio seguente può dimostrare maggiormente il concetto:

trait Hello
{
  public function sayHello()
  {
    echo 'Hello ' . $this->name;
  }

}

class User
{
  use Hello;

  private $name = 'Simone';
}

$u = new User();
$u->sayHello(); // Hello Simone

A differenza dell’ereditarietà i metodi definiti nei traits possono accedere a tutti i metodi e le proprietà della classe, compresi quelli privati. E’ come ne se copiassimo ed incollassimo il codice all’interno della classe.
[box type=”note”]I Traits non possono avere lo stesso nome delle classi altrimenti sarà mostrato un: Fatal error: Cannot redeclare class[/box]

Traits multipli

Possiamo inserire più traits all’interno della stessa classe, in questo caso la parola chiave use, dovrà essere seguita dai nomi dei traits separati da una virgola:

trait Hello 
{
    public function sayHello() {
        echo 'Hello ';
    }
}
trait User 
{
    public function sayName() {
        echo ' Simone';
    }
}

class Greeting 
{
    use Hello, User;
}

$g = new Greeting();
$g->sayHello() . $g->sayName(); //Hello Simone

Traits composti da altri Traits

Quando definiamo un trait possiamo includerne all’interno altri, incorporando quindi la funzionalità di più traits in uno:

trait Hello 
{
    public function sayHello() {
        echo 'Hello ';
    }
}
trait User 
{
    use Hello;
    public function sayName() {
        echo ' Simone';
    }
}

class Greeting 
{
    use User;
}

$g = new Greeting();
$g->sayHello() . $g->sayName(); //Hello Simone

Questo esempio è moto simile al precedente, tuttavia ora la classe utilizzando soltanto il secondo trait potrà usufruire dei metodi di entrambi i traits.

Metodi astratti

All’interno di un trait, proprio come avviene per le classi, possiamo definire dei metodi astratti, imponendone così l’implementazione sulle classi che utilizzeranno il trait:

trait Greeting
{
    public function sayHello() {
        echo 'Hello ';
    }
    abstract public function sayName();
}

class MyGreeting 
{
    use Greeting;
    public function sayName() {
        echo ' Simone';
    }

}

$g = new MyGreeting();
$g->sayHello() . $g->sayName(); //Hello Simone

Precedenza

Nei casi in cui sia abbiamo metodi con le stesso nome tra un trait e la classe che lo utilizza il comportamento sarà il seguente

  • I metodi della classe corrente sovrascrivono i metodi del trait.
  • I metodi ereditati da una classe base sono sottoposti ad override dai metodi inseriti da un trait.
class Base
{
    public function  sayHello()
    {
        echo "Ciao ";
    }       
}

trait Greeting
{
    public function  sayHello()
    {
        echo "Hello ";
    }

    public function sayName()
    {
        echo 'Samuele';
    }         
}

class MyGreeting extends Base
{
    use Greeting;

    public function sayName()
    {
        echo 'Simone';
    }  

    public function sayBaseHello()
    {
        echo parent::sayHello() . $this->sayName();
    }     
}

$g = new MyGreeting();
$g->sayHello() . $g->sayName(); //Hello Simone
$g->sayBaseHello(); //Ciao Simone

Nell’esempio abbiamo la classe MyGreeting che eredita dalla classe Base e che usa il trait Greeting. Come possiamo notare il metodo sayHello() della classe Base è sottoposto ad overidde da quello omonimo definito nel trait.
Il metodo sayName() invece, definito nella classe corrente avrà la precedenza con l’omonimo del trait.
Il metodo sayBaseHello() è stato inserito unicamente per mostrare come possiamo forzare la chiamata al metodo della classe Base anzichè a quello del trait che avrebbe invece avuto la precedenza.

Gestione di conflitti

Quando si lavora con traits multipli si possono generare conflitti di denominazione con conseguente errore fatale.
[box type=”alert”]Fatal error: Trait method XXX has not been applied, because there are collisions with other trait methods on…[/box]Questo avviene quando i traits contengono uno o più metodi con lo stesso nome.
PHP prevede la possibilità di sanare tali conflitti in due modi:

  • Attraverso l’operatore insteadof, in pratica si sceglie il metodo di un trait escludendo l’omonimo dell’altro trait.
  • Attraverso l’operatore as siamo invece in grado di consentire l’inserimento di uno dei metodi in conflitto con un altro nome.
trait Login
{
    function log() 
    {
        echo "Accesso al sistema";
    }
}

trait Logout
{
    function log() 
    {
        echo "Uscita dal sistema";
    }
}

class User
{
    use Login, Logout {
        Login::log insteadof Logout;
        Logout::log as logLogout;
    }
}

$u = new User();
$u->log(); //Accesso al sistema
$u->logLogout(); //Uscita dal sistema

Nell’esempio la classe User usa i traits Login e Logout che hanno un metodo con lo stesso nome.

  • Con la prima riga attraverso l’operatore insteadof si fa in modo che il metodo log di Logout non sia utilizzato, evitando così il conflitto.
  • Con la seconda riga (opzionale) si permette allo stesso metodo di essere chiamato attraverso l’alias logLogout permettendoci così di utilizzare entrambi i metodi in conflitto, il primo con il nome originale, il secondo con uno pseudonimo.

Proprietà

All’interno dei traits possiamo anche definire delle proprietà. In questo caso tuttavia dobbiamo essere consapevoli che non possiamo definire proprietà con lo stesso nome all’interno delle classi che li utilizzeranno altrimenti sarà generato un E_STRICT in caso la variabile abbia la stessa visibilità e valore iniziale, altrimenti un errore fatale:

trait PropertiesTrait {
    public $same = true;
    public $different = false;
}

class PropertiesExample {
    use PropertiesTrait;
    public $same = true; // Strict Standards
    public $different = true; // Fatal error
}

Inoltre, se un trait ha proprietà statiche, ogni classe usando quel trait avrà istanze indipendenti di tali proprietà:

trait TestTrait
{
    public static $foo;
}
class Hello
{
    use TestTrait;
}
class User
{
    use TestTrait;
}

Hello::$foo = 'Hello';
User::$foo = 'Simone';   
echo Hello::$foo . ' ' . User::$foo; // Hello Simone

Riflessione

Con PHP 5 il linguaggio è stato dotato di un’API di riflessione che offre la capacità ad un programma di eseguire elaborazioni che hanno per oggetto il programma stesso, ricavando così informazioni su una classe attraverso l’analisi del suo codice sorgente.
Con PHP 5.4 sono stati introdotti quattro nuovi metodi sulla ReflectionClass che riguardano proprio i traits:

  • ReflectionClass::getTraits() ottiene un array di tutti i traits usati nella classe.
  • ReflectionClass::getTraitNames() ottiene semplicemente un array con i nomi dei traits usati nella classe.
  • ReflectionClass::isTrait() può essere utile per sapere se qualcosa è un trait oppure no.
  • ReflectionClass::getTraitAliases() ottiene un array di eventuali alias per i nomi dei metodi.
trait Login
{
    function log() 
    {
        echo "Accesso al sistema";
    }
}

trait Logout
{
    function log() 
    {
        echo "Uscita dal sistema";
    }
}

class User
{
    use Login, Logout {
        Login::log insteadof Logout;
        Logout::log as logLogout;
    }
}

$rc = new ReflectionClass('User');
if (!$rc->isTrait()) {
    echo $rc->getTraitNames()[0]; //Login
    echo $rc->getTraitAliases()['logLogout']; // Logout::log
    foreach ($rc->getTraits() as $v) {
        echo $v->getMethods()[0]->class . ' ' . 
                $v->getMethods()[0]->name; // Login log Logout log
    }
}

Conclusioni

Nell’articolo abbiamo discusso di una delle più potenti caratteristiche introdotte da PHP 5.4. I traits rappresentano uno strumento molto importante per contribuire a migliorare la progettazione delle applicazioni rimuovendo la duplicazione del codice attraverso un riutilizzo orizzontale. Inoltre, essi possono contribuire a migliorare la manutenibilità e la pulizia del codice.

A mio avviso i traits possono rappresentare, assieme ad altre caratteristiche, la conferma che PHP sta lentamente divenendo un linguaggio in grado di abbracciare sempre più tecniche di programmazione standard la cui mancanza fino ad ora le veniva contestata.

La differenza principale tra ereditarietà multipla e traits sta nel fatto che un trait non viene ereditato ma incluso, mixato all’interno della classe difatti lo abbiamo paragonato ad un compia incolla. Inoltre i traits offrono mezzi per risolvere i conflitti che inevitabilmente sorgono quando si lavora con l’ereditarietà multipla rendendo quindi più semplice lavorare con questa caratteristica.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.