Cechy. Podstawy
PHP jest językiem obiektowym, w którym możliwe jest dziedziczenie tylko po jednej klasie. Jest to pewne ułatwienie, samo pojedyncze dziedziczenie jest nadużywane, a wielokrotne dziedziczenie tylko potęguje ten problem. Z drugiej strony, co zrobić z pewną częścią kodu, którą należałoby, mimo wszystko, kopiować do wielu klas?
Cechy
Od PHP 5.4 mamy dodatkowy mechanizm, który stanowią cechy (ang. traits). Cechy umożliwiają nam wielokrotne użycie pewnych metod i zmiennych, dając możliwość dołączenia tego kodu do wielu klas.
Cecha jest czymś pośrednim pomiędzy interfejsem a klasą. Cecha nie może posiadać takiej samej nazwy jak klasa (przestrzeń nazw rozwiązuje problem), ale w przeciwieństwie do klasy, nie da się utworzyć jej instancji. Interfejs jest polimorficzną konstrukcją dziedziczenia, która narzuca danej klasie implementację określonych przez nią metod. Natomiast cecha to prosty mechanizm kopiowania gotowej implementacji metod do wielu klas.
Kod cechy dołączany jest podczas procesu budowy klasy. Oznacza to, że klasa wykorzystująca cechę niczym nie różni się od klas, które po prostu zawierają zduplikowany kod. Jeśli w kodzie cechy użyjemy odwołania parent czy $this, zachowanie będzie identyczne, jak gdyby zostało to bezpośrednio zdefiniowane w klasie.
Weźmy pod uwagę prosty przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
trait PersonPL{ public function sayHello($name){ echo 'Witaj, nazywam się '.$name; } } class TestA{ use PersonPL; public function doSomething(){ echo 'TODO'; } } |
Jest on równoważny z poniższym kodem:
1 2 3 4 5 6 7 8 9 |
class TestA{ public function sayHello($name){ echo 'Witaj, nazywam się '.$name; } public function doSomething(){ echo 'TODO'; } } |
Cechy nie zmieniają semantyki klasy, biorą one tylko udział podczas procesu budowania klasy.
Dana klasa może dołączać wiele cech. Co więcej, jedna cecha może dołączać drugą cechę. Niemniej, nie przesadzajmy z tworzeniem dużej ilości cech, ponieważ taki kod może stać się mniej czytelny.
Aliasowanie metod cechy
Aby nadać nową nazwę metody pochodzącej z cechy, możemy wykorzystać operator as. Przyjrzyj się poniższemu przykładowi:
1 2 3 4 5 6 7 8 9 10 11 |
trait Thing{ public function doSomething(){ echo 'I will do it'; } } class TestA{ use Thing{ Thing::doSomething as doit; } } $objTestA = new TestA(); $objTestA->doit(); $objTestA->doSomething(); // also WORKS |
Nadaliśmy nową nazwę dla metody doSomething(). Jednak, kiedy ją wywołamy pod starą nazwą, o dziwo, dalej jest dostępna. Zawsze trzeba mieć to na uwadze!
Zmiana nazwy metody może być przydatna również przy rozwiązywaniu konfliktów. Innym zastosowaniem operatora as jest zmiana widoczności wybranej metody.
Rozwiązywanie konfliktów
Możemy oczywiście dołączyć więcej cech. Załóżmy, że dwie poniższe cechy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
trait PersonPL{ public function sayHello($name){ echo 'Witaj, nazywam się '.$name; } public function whereFrom($country){ echo 'Jestem z '.$country; } } trait PersonENG{ public function sayHello($name){ echo 'Hi, my name is '.$name; } public function whereFrom($country){ echo 'I am form '.$country; } } |
chcemy dołączyć do klasy TestA:
1 2 3 |
class TestA{ // use PersonPL, PersonENG; // Fatal error: Trait method sayHello has not been applied, because there are collisions with other trait methods on TestA } |
Klasa TestA używa cechy PersonPL. Jeśli dołączymy PersonENG do klasy, niestety kod nie będzie działał, gdyż wystąpi konflikt. Klasa TestA nie wie, z jakiej cechy miałaby wykorzystać metody sayHello() i whereFrom().
Aby rozwiązać konflikt, musimy wskazać, którą metodę chcemy używać. Służy do tego operator insteadof. Po jego użyciu tylko wybrana przez nas implementacja będzie widoczna.
Załóżmy, że preferujemy język polski. Wybieramy więc metody z cechy PersonPL. Wyboru interesującej nas implementacji dokonujemy za pomocą operatora insteadof:
1 2 3 4 5 6 |
class TestA{ use PersonPL, PersonENG{ PersonPL::sayHello insteadof PersonENG; PersonPL::whereFrom insteadof PersonENG; } } |
Teraz metody sayHello() oraz whereFrom() będą wywoływane z cechy PersonPL. W przypadku większej ilości cech możemy wymienić je po przecinku:
1 2 3 4 5 6 |
class TestB{ use PersonPL, PersonENG, PersonDE{ PersonPL::sayHello insteadof PersonENG, PersonDE; PersonPL::whereFrom insteadof PersonENG, PersonDE; } } |
Stosując operator insteadof możliwe jest całkowite wykluczenie metody cechy, która powoduje, że występuje konflikt. Aby rozwiązać problem, wystarczy wykluczyć ją z jednej cechy, a następnie z drugiej. W naszym przypadku kod będzie wyglądał tak:
1 2 3 4 5 6 7 8 9 10 11 12 |
class TestA{ use PersonPL, PersonENG{ PersonPL::sayHello insteadof PersonENG; PersonENG::sayHello insteadof PersonPL; PersonPL::whereFrom insteadof PersonENG; } } $objA = new TestA(); $objA->sayHello('Anna'); // Fatal error: Call to undefined method TestA::sayHello() $objA->whereFrom('Polski'); |
Próba wywołania metody sayHello() kończy się błędem. Tylko w taki sposób możemy wykluczyć daną metodę pochodzącą z cech.
W przypadku, gdy interesują nas metody z obu cech, możemy wykorzystać operator aliasu as do nadania metodom nowych nazw:
1 2 3 4 5 6 7 8 |
class TestA{ use PersonPL, PersonENG{ PersonPL::sayHello insteadof PersonENG; PersonPL::whereFrom insteadof PersonENG; PersonENG::sayHello as sayHelloENG; PersonENG::whereFrom as whereFromENG; } } |
Operator aliasu ma również zastosowanie przy zmianie modyfikatora widoczności metod. O tym, parę słów, w dalszej części wpisu.
Cecha może posiadać wiele cech
Zgrupujmy wcześniej zdefiniowane cechy: PersonPL i PersonENG, dołączmy je do cechy SayHello. Rozwiążemy konflikt, aby móc skorzystać z obu wersji posiadanych metod oraz ujednolicimy sposób ich wywołania. Teraz to wybór języka będzie definiował, z której cechy będziemy wykorzystywać metody:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
trait SayHello{ private $language = "PL"; private $listLanguages = ["PL", "ENG"]; use PersonPL, PersonENG{ PersonPL::sayHello as sayHelloPL; PersonPL::whereFrom as whereFromPL; PersonENG::sayHello as sayHelloENG; PersonENG::whereFrom as whereFromENG; } public function sayHello($name){ $this->{'sayHello'.$this->language}($name); } public function whereFrom($country){ $this->{'whereFrom'.$this->language}($country); } public function setLanguage($language){ if(in_array(strtoupper($language), $this->listLanguages)){ $this->language = $language; } } } class TestA{ use SayHello; } $objectA = new TestA(); $objectA->setLanguage("ENG"); $objectA->sayHello("Anna"); $objectA->whereFrom("Poland"); |
W wyniku kod wyświetli: Hi, my name is Anna I am form Poland
Z cech możemy budować hierarchię tak jak w klasach. Jednak poniższy kod może sprawić problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
trait A{ public function a(){} } trait B{ use A; } trait C{ use A; } class D{ // use B, C; // Fatal error: Trait method a has not been applied, because there are collisions with other trait methods on D } |
W wyniku otrzymamy błąd:
Fatal error: Trait method a has not been applied, because there are collisions with other trait methods on D
Bug został oczywiście zgłoszony już dobry czas temu. Oczywiście, może rozwiązać problem kolizji za pomocą operatora insteadof.
Zmiana widoczności metod
Jak już wspomniano, zmianę widoczności metody, można dokonać za pomocą operatora as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
trait TraitA{ public function sayHello(){ echo 'Hello world!'; } } class ClassA{ use TraitA{ sayHello as protected; } } class ClassB{ use TraitA{ sayHello as private; } } |
Wszystko ładnie pięknie, ale co w przypadku, gdy oprócz zmiany widoczności metody, chcemy przy tym zmienić jej nazwę? Przyjrzyjmy się temu przypadkowi:
1 2 3 4 5 6 7 8 9 |
class ClassC{ use TraitA{ sayHello as private sayHelloPL; } } $objC = new ClassC(); $objC->sayHello(); // WORKS !!! |
Niestety, tutaj mamy niespodziankę. Metoda sayHello() jest dalej dostępna publicznie! Jedynym sposób na wykluczenie został przedstawiony w punkcie Rozwiązywanie konfliktów.
__TRAIT__
W raz z dodaniem cech, doszła też nowa magiczna stała __TRAIT__ . Przechowuje ona nazwę cechy.
1 2 3 4 5 |
trait TraitName{ public function check(){ echo ' To jest działanie cechy '.__TRAIT__; } } |
W wyniku wypisze nam: To jest działanie cechy TraitName
Wraz z cechami zostały również dodane funkcje pomocnicze:
- class_uses() – zwraca tablicę zawierającą listę cech podanej klasy,
- mechanizm refleksji dodaje jedynie dwie funkcje:
- trait_exists() – sprawdza, czy podana cecha istnieje
- get_declared_traits() – zwraca tablicę wszystkich zadeklarowanych cech, w tym także rodzica.
W następnej notce porównane zostały cechy z interfejsami. Omówiono również zastosowanie cech.
Czytaj: Cechy a interfejsy.