Cechy (ang. Trait) w PHP

Dzisiaj słów kilka na tema cech (ang. trait) czyli sposobu w PHP na wielodziedziczenie. Dzięki nim możemy użycie tych samych metod w wielu klasach. Zobaczmy poniższy przykład.

<?php
trait Cukier { 
   private $zawartosc = 0;

   public function dosyp(int $ilosc) 
   {
       $this->zawartosc += $ilosc;
   }
}

trait Mleko {
 private $zawartosc = 0;

 public function dodaj(int $ilosc) 
   {
       $this->zawartosc += $ilosc;
   }
}

class Herbata {
   use Cukier;
   
   public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

class Kawa {
  use Cukier, Mleko;
}

$herbata = new Herbata();
$herbata->dosyp(2);
echo $herbata->pobierzZawartosc();

Definiujemy tu cechę Cukier, która będzie posiadać prywatną zmienną $zawartosc i metodę dosyp(). Cecha ta zostanie użyta w klasach Kawa i Herbata, czyli obie klasy uzyskają dostęp do metody i jej składowej. Co ciekawe dopuszcza się (gdy nie jest włączony tryb strict) powielenie składowej zarówno w innych cechach jak i klasach z nich korzystających (tak jak w tym przykładzie). Jak również możemy użyć niezdefiniowanej składowej pod warunkiem, że finalnie zostanie ona dostarczona przez inną cechę albo klasę. Oczywiście nic teraz nie stoi na przeszkodzie, aby cecha Cukier była adaptowana również do innych napojów, jak i by napój mógł korzystać z wielu różnych składników (Cukier, Mleko etc.) tak jak w przypadku Kawy.

Co jednak w przypadku konfliktu nazw? Załóżmy, że zarówna klasa jak i cecha, z której korzysta posiadają tą samą nazwę metody. W takim przypadku zostanie użyta metoda z klasy. Natomiast w sytuacji, w której konflikt nazw dotyczy cech (np. Cukier jak i Mleko posiadają wspólnie metodę dodaj()) uzyskamy fatal error. Możemy taką sytuacje naprawić przez określenie, która z metod ma zastosowanie w takim przypadku, tak jak poniżej.

class Kawa {
  use Cukier, Mleko {
     Cukier::dodaj insteadof Mleko;
     Mleko::dodaj as dolej;
   }
   
   public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

W powyższym przykładzie konflikt zostaje rozwiązany przez użycie metody z cechy Cukier, z kolei dla użycia metody dodaj() z cechy Mleka należy użyć wywołania spod aliasu dolej().

Nie mamy też przeszkody by zmieniać zakres widoczności metod w klasach używających danej cechy. Dla przykładu, mleka do kawy sobie nie dodamy 🙁

class Kawa {
 use Mleko { dodaj as protected; }

   
   public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

$kawa = new Kawa();
$kawa->dodaj(2); //wygeneruje Uncaught Error: Call to protected method
echo $kawa->pobierzZawartosc();

Cechy mogą być również użyte do zgrupowania innych (wielu różnych) cech.

trait Dodatki {
  use Cukier, Mleko;
}

class Kawa {
 use Dodatki;
 public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

$kawa = new Kawa();
$kawa->dodaj(2);
echo $kawa->pobierzZawartosc();

Jeszcze inna ciekawa właściwość to stosowanie abstrakcji. Możemy zadeklarować daną metodę jako abstrakcyjną, gdzie klasa używająca cechy dostarczy jej definicje (analogicznie jak dla klas abstrakcyjnych). Zatem poprawiając dzisiejszy przykład, bardziej poprawnie byłoby zapisać go w następujący sposób.

<?php
trait Cukier { 
   protected $zawartosc = 0;

   public function dodaj(int $ilosc) 
   {
       $this->zawartosc += $ilosc;
   }
   
   abstract public function pobierzZawartosc(): int;
}

class Kawa {
 use Cukier;
 public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

$kawa = new Kawa();
$kawa->dodaj(2);
echo $kawa->pobierzZawartosc();

Podsumowując myślę, że możliwości jakie dają nam cechy w języku PHP stanowią lepszą alternatywę do tradycyjnych dziedziczących po sobie klas. Oczywiście w przypadku gdy już na etapie projektowania wiemy, że ich metody mogą zostać wykorzystane w różnych częściach naszej aplikacji.