Tomasz Tomczyk - Programista PHP, Kraków - Strona główna > php, programowanie > PHP 5.4 – nowości, nowości część druga – Traits

PHP 5.4 – nowości, nowości część druga – Traits

Luty 22nd, 2013

Jest to chyba największa modyfikacja który została dodana w php 5.4
Z jednej strony można się bez niej całkowicie obejść, ale z drugiej strony przemyślane użycie z pewnością usprawni pracę.

Jednak do rzeczy. Poniżej trywialny przykładzik:

Użycie bezpośrednio w klasie

trait Hello {
	public function hello() {
		return "Hello";
	}
} 

trait City {
	public function city($name) {
		return $name;
	}
} 

class Welcome {
	use Hello, City;
} 

$c = new Welcome();
echo $c->hello(), ' ', $c->city("Sandomierz");
// prints "Hello Sandomierz" 

Powyżej możemy zauważyć, że jedna klasa może jednocześnie używać więcej niż jednej cechy.

Cechy cech :)

Użycie trait’s w wewnątrz innej deklaracji trait

trait Hello {
	public function hello() { return "Hello"; }
} 

trait City {
	public function city($name) { return $name; } 

}
trait Greeting {
	use Hello, City;
	public function greeting($name) {
		echo $this->hello(), ' ', $this->city($name);
	}
} 

class Welcome { use Greeting; }
(new Welcome())->greeting("Ozarow");
// prints "Hello Ozarow"

Przesłanianie metod: Class vs Traits

Sprawa z przesłanianiem wygląda następująco:
- metoda zdefiniowana wewnątrz klasy która używa cech jest najmocniejsza i nadpisuje metodę wstrzykniętą przez cechę,
- metoda wstrzyknięta przez cechę nadpisuje metodę odziedziczoną po rodzicu

Z poziomu metody wstrzykniętej za pomocą cechy nic nie stoi na przeszkodzie wywołać metodę rodzica klasy, czyli odwołać się do metody która została przesłonięta.

< ?php
trait MyTrait {
    public function hello() {
        echo 'Jestesmy w MyTrait!';
    }
}

class MyClass {
    use MyTrait;
    public function hello() {
        echo 'Jestesmy w MyClass!';
    }
}

$o = new MyClass();
$o->hello();
// wynik: Jestesmy w MyClass!
?>

< ?php
class MyClass {
    public function hello() {
        echo 'Hello ';
    }
}

trait SayWorld {
    public function hello() {
        parent::hello();
        echo 'Ozarow!';
    }
}

class MyHello extends MyClass {
    use SayWorld;
}

$o = new MyHello();
$o->hello();
// wynik: Hello Ozarow
?>

Konflikty nazewnictwa – czyli kiedy dwie cechy mają metody o tej samej nazwie.

Może się tak zdarzyć, że w będziemy mieli dwie cechy posiadające metody o tej samej nazwie. Użycie obu w tej samej klasie spowoduje wygenrowanie błędu (fatal), jeśli odpowiednio nie rozwiążemy konfliktu.
Aby rozwiązać problem musimy w klasie jednoznacznie zaznaczyć z której cechy ma pochodzić metoda.

< ?php
trait A {
    public function smallTalk() {
        echo 'a';
    }
    public function bigTalk() {
        echo 'A';
    }
}

trait B {
    public function smallTalk() {
        echo 'b';
    }
    public function bigTalk() {
        echo 'B';
    }
}

class Talker {
    use A, B {
        // używamy metody smallTalk zdefiniowanej w Trait B
        B::smallTalk insteadof A;
        // używamy metody bigTalk zdefiniowanej w Trait A
        A::bigTalk insteadof B;
    }
}
?>

Istnieje jednak sposób umożliwiający jednoczesne (znaczy się w tej samej klasie) użycie metody bitTalk zarównio zdefiniowanej w Trait A jaki i w Trait B. Musimy dla jednej z nich zdefiniować alias, dzięki temu będzie widoczna pod nową nazwą i pozbędziemy się konfliktu nazw.

< ?php
class Aliased_Talker {
    use A, B {
        // używamy metody smallTalk zdefiniowanej w Trait B
        B::smallTalk insteadof A;
        // używamy metody bigTalk zdefiniowanej w Trait A
        A::bigTalk insteadof B;
        // metodzie z bigTalk z Trait B nadajemy alias talk
        B::bigTalk as talk;
    }
}
?>

Zmaina typu widoczności

Metoda wewnątrz „trait” może być prywatna, jednak podczas „wstrzykiwania” metody do klasy możemy przedefiniować ją na publiczną.

trait Hello {
	private function hello($city) {
		echo "Hello " , $city;
	}
} 

class Welcome {
	use Hello {
		hello as public;
	}
} 

(new Welcome())->hello("Tarnobrzeg");
// prints "Hello Tarnobrzeg" 

// możemy też jednocześnie ze zmianą modyfikatora widoczności nadać metodzie alias pod którą będzie widoczna
class Welcome2 {
	use Hello {
		hello as protected protectedHello;
	}
}

Właściwości

Możemi definiować właściwości które będą widoczne w klasach.

   < ?php
   trait PropertiesTrait {
       public $traitProp = 1;
   }

   class MyClass {
       use PropertiesTrait;
   }

   (new MyClass())->traitProp;
   ?>

W przypadku gdy w klasie istnieje juz właściwość o takiej samej nazwie jak w „PropertiesTrait” to zgłoszony zostanie odpowiedni komunikat błędu. Będzie to E_STRICT jeśli właściwość w klasie i w Trait posiada tą samą widoczność lub „fatal error” jeśli modyfikatory widocznści są różne

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

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

Metody abstrakcyjne

W traitach możemy tworzyć deklaracje metod abstrakcyjnych, dzięki temu możemy wymusić ich definicję w konkretnej klasie.

< ?php
trait Hello {
    abstract public function getWorld();
    public function sayHelloWorld() {
        echo 'Hello'.$this->getWorld();
    }
}

class MyClass {

    private $world;
    use Hello;

    public function getWorld() {
        return $this->world;
    }

    ...
}
?>

Static, static, static …

Istnieje możliwość zdefiniowania zmiennej statycznej w metodzie zdefiniowanej w „Trait”.

Zmienne statyczne.
   < ?php
   trait Counter {
       public function inc() {
           static $count = 0;
           $count = $count + 1;
           echo "$count\n";
       }
   }

   class MyClass1 {
       use Counter;
   }

   class MyClass2 {
       use Counter;
   }

   (new MyClass1())->inc(); // echo 1
   (new MyClass2()_->inc(); // echo 1
   ?>
Metody statyczne

Możemy też definiować metody statyczne.

   < ?php
   trait StaticExample {
       public static function doSomething() {
           return 'Doing something';
       }
   }

   class MyClass {
       use StaticExample;
   }

   Example::doSomething();
   ?>
Właściwości statyczne. Class vs Traits

W przypadku gdy cecha posiada zmienną statyczną to każda klasa używająca danej cechy traktuję taką zmienną niezależnie.
Zupełnie inaczej działa to w przypadku gdy zmienna statyczna jest zdefiniowana w rodzicu. Wszystkie klasy potomne posiadają referencję do tej samej zmiennej.

< ?php
class TestClass {
    public static $_bar;
}

class Foo1 extends TestClass { }
class Foo2 extends TestClass { }

Foo1::$_bar = 'Hello';
Foo2::$_bar = 'World';

echo Foo1::$_bar . ' ' . Foo2::$_bar; // World World
?>

Użycie Trait

< ?php
trait TestTrait {
    public static $_bar;
}
class Foo1 {
    use TestTrait;
}
class Foo2 {
    use TestTrait;
}

Foo1::$_bar = 'Hello';
Foo2::$_bar = 'World';

echo Foo1::$_bar . ' ' . Foo2::$_bar; // Hello World
?>

Magia

Dodatkowo otrzymaliśmy nową magiczną stałą __TRAIT__, działa analogicznie jak __CLASS__ dla klasy, czyli zwraca nazwę cechy.

< ?php
trait testClass {
    public function getClassName() {
        echo __CLASS__;
    }
    public function getTraitName() {
	echo __TRAIT__;
    }
}

class MyClass1 {
    use testClass;
}

class MyClass2 {
    use testClass;
}

$mc1 = new MyClass1;
$mc1->getClassName(); //MyClass1
$mc1->getTraitName(); //testClass

$mc2 = new MyClass2;
$mc2->getClassName(); //MyClass2
$mc2->getTraitName(); //testClass
?>

Kolejną „magiczną” sprawą z której trzeba sobie zdawać sprawę (niekoniecznie ją stosować) jest zachowanie metody __call()
Możemy ją zdefiniować w Trait a następnie używać w dowolnej klasie, będzie działać poprawnie.

Podobna sprawa jest z pozostałymi magicznymi metodami, nawet z metodą __construct()

Jak to się sprawdza w boju, czyli przykładowa implementacja: Singleton

W sieci natrafiłem na przykład użycia Trait’s do nowego sposobu implementacji wzorca Singleton.
Poniżej przykład odrobinę przezemnie zmodyfikowany:

< ?php
trait singleton {
        /**
         * za implementacje konstruktora odpowiada już konkretna klasa, więc pozostawiamy tylko odpowiednia notkę
         */
        //private function __construct() {}

        private static $_instance = null;

        public static function getInstance() {
    	// pamiętamy, że __CLASS__ zwróci nazwę klasy w której metoda z Trait jest użyta
            $class = __CLASS__;
            return $class::$_instance ?: $class::$_instance = new $class;
        }

        // blokujemy możliwość tworzenia obiektu poprzez klonowanie
        public function __clone() {
            trigger_error('Cloning '.__CLASS__.' is not allowed.',E_USER_ERROR);
        }

        // blokujemy możliwość tworzenia obiektu przez unserializację
        public function __wakeup() {
            trigger_error('Unserializing '.__CLASS__.' is not allowed.',E_USER_ERROR);
        }
    }

    class foo {
        use singleton;
        private function __construct() {
            $this->name = 'foo';
        }
    }

    class bar {
        use singleton;
        private function __construct() {
            $this->name = 'bar';
        }
    }

    $foo = foo::getInstance();
    echo $foo->name;
    $foo->name = 'zet';

    $bar = bar::getInstance();
    echo $bar->name;

    $foo = foo::getInstance();
    echo $foo->name;
?>

Jeszcze nie miałem okazji stosować powyższej konstrukcji. Trudno mi w tej chwili powiedzieć jak będzie się sprawdzała w praktyce i czy ma głębszy sens tworzenie Singletonów w ten sposób (o ile samo używanie Singletonów ma sens – ale to temat na zupełnie inną dyskusję).

Parę ciekawostek odnośnie cech

- działają z mechanizmem przestrzeni nazwach
- nie mogą mieć tej samej nazwy co klasa, powoduje to wygenerowanie fatala

Dobrym sposobem na znalezienie miejsca gdzie będziemy mogli użyć mechanizmu Trait jest sytuacja gdy w kilku różnych klasach używana jest ta sama funkcjonalność i nie dziedziczą one po jednym rodzicu. Taką funkcjonalność możemy wynieść do Trait.

Trait możemy też używac jako uzupełnienei interfejsów. W interfejsie definiujemy sobie pewne Api np MyIterator który będzie implementowany przez kilka klas, który chechą wspólną jest jedynie ten interfejs. Zakładając że mechanizm iteracji będzie wszędzie taki sam to obsługę interfejsu możemy napisać w powiedzmy MyIteratorTrait a następnie użyć go w każdej z klas bez niepotrzebnego powielania kodu.

Podsumowanie

Używanie „traits” niesie ze sobę niebezpieczeństwo, że nasz kod stanie się mniej czytelny. Musimy dobrze przemyślec gdzie i kiedy ich używać. Podejście na „hurrrra” może się skończyć wielkim bałaganem w naszym kodzie. W klasach będą się pojawiać metody, które nie powinny mieć w nich racji bytu. Pamietajmy aby ich używać z rozmysłem a nie tylko dlatego że istnieją.
To znaczy implementowanie funkcjonalności która będzie występować tylko w jednej klasie przy uzyciu Trait rozmija się z celem. Użycie Traits ma serns w tedy gdy dana funkcjonalność ma być używana w wielu klasach.

Wpis ten ma za zadanie przedstawić możliwości jakie otrzymujemy wraz z mechanizmem Traits. W żadnym wypadku go nie ocenia.

Komentarze są zamknięte