Magiczne metody

Ostatnio przygotowując artykuł dla kolegi, na temat obiektowości, dowiedziałem się, że już rozumie większość rzeczy i z chęcią dowiedziałby się czegoś o magicznych metodach i interfejsach. Co prawda słyszałem już o nich, ale nigdy nie zagłębiałem się w tym temacie. Postanowiłem poświęcić parę godzin w tygodniu na edukację, zrobiłem kilka przykładów i przełożyłem to na bardziej zrozumiały język, żeby korzystając z jednego artykułu, można było się wszystkiego dowiedzieć. Podczas czytania tego artykułu, zachęcam Cię drogi czytelniku do korzystania z php sandbox-a, który nie tylko przydaję się w pracy podczas szybkiego sprawdzania różnych rzeczy, ale również w nauce.

Czym są magiczne metody?

Charakteryzują się tym, że nazwy tych metod zaczynają się od podwójnego podkreślenia (__nazwa)
Każdy z nas zapewne kojarzy już 2 takie metody, są to:
__construct() oraz __destruct()
które są wywoływane automatycznie przy tworzeniu  i niszczeniu obiektu.

Istnieją jeszcze kolejne:

__get() i __set() 

__get() – funkcja jest uruchamiana gdy próbujemy dostać się do nieistniejącego pola. W normalnym przypadku, gdy pole nie istnieje otrzymamy Notice-a. Dzięki skorzystaniu z tej magicznej metody, możemy taki przypadek obsłużyć customowo, wywałając np. Exception. Funkcja ta przyjmuje tylko jeden parametr.

class Person 
{
    public $age;

    public function __get($name) {
        throw new Exception(sprintf('Variable %s has not been declared', $name));
    }
}

$person = new Person();
echo $person->siblings;

__set() – funkcja jest uruchamiana w przypadku kiedy próbujemy ustawić nieistniejącą wartość. Przyjmuje ona 2 parametry ($name oraz $value). Pod $name, będziemy mieli wartość którą próbowaliśmy ustawić, a pod $value – zawartość. Podczas próby ustawienia wartości bez magicznej metody, nic się nie stanie (nie będzie Notice-a oraz Errora).

class Person 
{
    public $age;

    public function __set($name, $value) {
        echo 'Variable '.$name.' has not been declared and You can\'t set '.$value.' to it!';
    }
}

$person = new Person();
$person->lastname = 'Andrew';

_call()

Działanie jest analogiczne do funkcji __get(). Jeżeli wywołamy getAgeFromBirthday() w naszym obiekcie Person, a taka metoda nie będzie istnieć lub dostęp będzie niemożliwy (modyfikator private lub protected) interpreter PHP sprawdzi, czy istnieje magiczna metoda __call. Jeżeli nie ma takiej metody, otrzymamy: „Uncaught Error: Call to undefined method Person::getAgeFromBirthday() in …”, możemy się przed tym zabezpieczyć, używając metody _call, która przyjmuje 2 parametry: nazwę którą probowaliśmy wywołać oraz przekazywane argumenty w postaci tablicy.

class Person 
{
    public $age;

    public function __call($method, $params) {
        echo 'unknown method "'.$method.'" Passed arguments:';
        print_r($params);
    }     
}

$person = new Person();
$person->getAgeFromBirthday('12.12.1990');

__callStatic()

Takie samo działanie jak _call, tylko że dla wywołania statycznego

class Person 
{
    public $age;

    public static function __callStatic($method, $params) {
        echo 'unknown method "'.$method.'" Passed arguments:';
        print_r($params);
    }     
}

$Person = new Person();
$Person::getAgeFromBirthday('12.12.1990');

__toStrnig()

Pozwala nam wywołać obiekt jako string. W normalnym przypadku, podczas wywołania

echo $broker = new Person(), orzymalibyśmy:
Recoverable fatal error</b>:  Object of class Person could not be converted to string in… , możemy jednak sprawić, że będziemy mogli wywołać obiekt jako string. Magiczna metoda __toString nie przyjmuje żadnych parametrów. W momencie wywołania obiektu jako string, wykonywane jest ciało funkcji.

class Person {

  public function __construct($name, $lastName, $age) {
      $this->name = $name;
      $this->age = $age;
      $this->lastName = $lastName;
  }
  
  public function __toString() {
      return $this->name.' '.$this->lastName.' '.$this->age;
  }

}

$broker = new Person('Andres', 'Smith', 50);
echo $broker;

__invoke()

Funkcja ta jest bardzo podobna do __toString – wywoływana, kiedy obiekt używany jest w kontekście funkcji

class Person {

  public function __construct($name, $lastName, $age) {
      $this->name = $name;
      $this->age = $age;
      $this->lastName = $lastName;
  }
  
  public function __invoke($value) {
      echo $value;
  }

}

$broker = new Person('Andres', 'Smith', 50);
echo $broker(55);

__clone() 

Kopiowanie w PHP odbywa się poprzez referencję. Zmiana jednego z obiektów spowoduje więc jednoczesną zmianę drugiego.

$SeniorBroker = new stdClass();
$SeniorBroker->age = 50;

$JuniorBroker = $SeniorBroker;

$JuniorBroker->age = 19;

echo $SeniorBroker->age;

Jak się domyślacie, bądź nie, w tym przypadku nadpiszemy wiek seniora i pojawi się nam wiek 19. Możemy natomiast skopiować obiekt, tak jak planowaliśmy, aby zmiany w kolejnym obiekcie, nie miały wpływu na oryginalny:

$SeniorBroker = new stdClass();
$SeniorBroker->age = 50;

$JuniorBroker = clone $SeniorBroker;

$JuniorBroker->age = 19;

echo $SeniorBroker->age;

Magiczna funkcja __clone(), pozwala nam na zdefiniowanie tego, co ma się dziać w klasie, jeżeli zostanie wywołana na niej metoda clone().
W poniższym przykładzie, nie chcemy aby ktokolwiek dowiedział się ile Senior miał lat oraz chcemy zabezpieczyć się przed Notice-m, gdy ktokolwiek spróbuje wywołac niezadeklarowane pole:

class Person {

 public $age;
 
 public $title;
  
 public function __clone() {
     unset($this->age);
 }
  
 public function __get($name) {
     echo 'Variable "'.$name.'" has not been declared.';
 }
  
}

$SeniorBroker = new Person();
$SeniorBroker->age = 50;

$Broker = clone $SeniorBroker;
echo $Broker->age;

__sleep() oraz __wakeup()

Serializacja obiektów polega na przetłumaczniu złożonego typu do postaci tekstowej i złożenie z niej z powrotem oryginału. Służy do tego funkcja serialize() a dzięki magicznym metodą, możemy nad serializacją zapanować(serializacja nie będzie w stanie przechować nam uchwytów np. do bazy danych).

_sleep()

Wywoływane podczas używania na obiekcie serialize(). Domyślnie funkcja serialize przekonwertuje nam cały obiekt, dzięki magicznej metodzie, możemy wybrać tylko te pola, które nas interesują.

Przykład – interesuje nas tylko title i age:

class Person {	

 public $age;
 
 public $title = 'Senior Sale Specialist';
 
 public $teamworkId = '34562';
 
 private $bankAccountAmount = 150000;
 
 private $db;
 
 public function __sleep() {
     return ['age', 'title'];
 }
 
 }

$SeniorBroker = new Person();
var_dump($brokerSerialized = serialize($SeniorBroker)); // mamy tutaj wylacznie age i title, bez teamworkId

__wakeup()

Wywoływane podczas używania na obiekcie unserialize(). Przykład – chcemy przywrócić połaczenie z bazą, które podczas stosowania serialize() zostało utracone.

class Person {	

 public $age;
 
 public $title = 'Senior Sale Specialist';
 
 private $bankAccountAmount = 150000;
 
 private $db;

 public function connect() {
     $this->db = 'connected';
 }
 
 public function __wakeup() {
     $this->connect();
 }

}

$SeniorBroker = new Person();
$brokerSerialized = serialize($SeniorBroker);
var_dump((unserialize($brokerSerialized)));

_isset() i __unset()

_isset() 

Wywoływana gdy na właściwości obiektu który nie istnieje, bądź do którego nie ma dostępu,
zostanie użyta fukcja isset() albo empty(). Przykład – sprawdzenie czy prywatne pole zostało ustawione(bez magicznej funkcji otrzymalibyśmy Fatal Error)

class Person {
 	
 public $age;
 
 public $title = 'Senior Sale Specialist';
 
 private $bankAccountAmount = 150000;

 public function __isset($name) {
    echo isset($this->{$name});
 }
    
}

$SeniorBroker = new Person();
isset($SeniorBroker->bankAccountAmount);

__unset()

Wywoływana w identyczny sposób jak __isset(), z tą różnicą, że tylko wtedy, gdy użyta zostanie funkcja unset(). Przykład – zapisywanie do loga każdej usuwanej zmiennej, jeśli miała jakąś wartość.

class Person {
 	
 public $age;
 
 public $title = 'Senior Sale Specialist';
 
 private $bankAccountAmount = 150000;

 public function __unset($name) {
     if (isset($this->{$name})) {
         echo 'Save to log: '.$name.'='.$this->{$name};
     }
 }
 
}

$SeniorBroker = new Person();
unset($SeniorBroker->bankAccountAmount);

__set_state()

Wywoływane podczas używania var_export(var_export różni się od var_dump oraz print_r tym, że zwraca poprawny do wywołania kod).

Przykład działania var_export:

$alphabet = ['a', 'b', 'c'];
var_export($alphabet);

$arrayFromVarExport = array (   // tablica zwrocona z var_export
  0 => 'a',
  1 => 'b',
  2 => 'c',
);

var_dump($alphabet);
var_dump($arrayFromVarExport);   // jak widzimy, sa identyczne

var_export nie do końca dobrze radzi sobie z wyświetlaniem obiektów jako prawidłowego kodu.

Poniższy kod zwróci nam Fatal Error:

class Person {

private $age = 50;

private $title = 'Senior Sale Specialist';

}

$Person = new Person;

var_export($Person);

eval(var_export($Person, true).';');

Rozwiązanie:

class Person {

private $age = 50;

private $title = 'Senior Sale Specialist';

public static function __set_state(array $parameters){

$Person = new Person;

$Person->name = $parameters['age'];

$Person->password = $parameters['title'];

return $Person;

}

}

$Person = new Person;

var_export($Person);

eval(var_export($Person, true).';');

__debugInfo()

Wywoływane podczas używania var_dump() (jeżeli w obiekcie nie ma magicznej metody, wszystkie pola: public, protected i private zostaną wyświetlone)
Przykład użycia: przesyłamy obiekt na inny serwer i nie chcemy, żeby zawartość była widoczna, po użyciu var_dump na obiekcie. W normalnym przypadku użycia var_dump-a zobaczymy całą zawartość obiektu:

class Person {
 	
 public $age;
 
 public $title = 'Senior Sale Specialist';
 
 private $bankAccountAmount = 150000;
 
 private $secretPassword = 'porlakm5ikkdiu4';
 }
 
 $toSend = new Person;
 
var_dump($toSend);

Rozwiązanie:

class Person {
 	
 public $age = 50;
 
 public $title = 'Senior Sale Specialist';
 
 private $bankAccountAmount = 150000;
 
 private $secretPassword = 'porlakm5ikkdiu4';
 
 public function __debugInfo() {
        return [];
    }
 }
 
$toSend = new Person;
 
var_dump($toSend);

echo $toSend->age;

UWAGA !!!
Wywołanie var_export na obiekcie pozwoli nam na zobaczenie całej zawartości

Przykłady wykorzystania były wymyślane na szybko, jeżeli w codziennej pracy korzystacie z tych funkcji, znacie ciekawe przypadki wykorzystania, podzielcie się w komentarzu :).

Dodaj komentarz