Wpisy otagowane ‘__call()’

Metoda, która nie istnieje…

poniedziałek, 13 Grudzień 2010

Kilka lat temu, gdy nie znałem jeszcze wszystkich tajników programowania obiektowego, zlecono mi modyfikację pewnego skryptu napisanego właśnie obiektowo. Klasy, obiekty i metody nie były dla mnie zupełną nowością, więc zabrałem się do pracy z nastawieniem, że szybko wykonam zlecenie, wezmę kasę i… Już teraz nie pamiętam na co chciałem wydać te pieniądze ;) .

Zlecenie wydawało się dziecinnie proste. Przynajmniej do czasu, za nim nie wgłębiłem się w kod. Skrypt był podobny do tego poniżej: na początku definicje kilku klas, a później program, który ich używał. Klasy nie wzbudziły moich podejrzeń. Za to później trafiłem, na coś, czego zupełnie nie mogłem zrozumieć.

W poniższym kodzie, symulującym kilka kolejnych zakupów i sprzedaży złota przy zmieniającym się kursie, umieściłem coś podobnego. Chodzi mi o metody kup() i sprzedaj() wywoływane na obiekcie $zloto klasy Gold. Tyle że w klasie Gold nie ma definicji metod kup() i sprzedaj(). Klasa Gold nie rozszerza też żadnej innej klasy, po której mogłaby te metody odziedziczyć. O co więc chodzi?

Najdziwniejsze było to, że ten kod działał. Nie zgłaszał żadnych błędów. W pewnym momencie zacząłem podejrzewać mój komputer o magię i jak się okazało, w pewnym sensie trafiłem. Rozwiązaniem mojej zagadki była metoda _call() należąca do grupy tzw. metod magicznych, które odpowiadają za właśnie takie „niezwykłe” zachowanie programu.

Zajmijmy się jednak poniższym przykładem.

<?php
class Gold
{
	const NAZWA='zloto';
	private $cena=128.4;
 
	public function kurs()
	{
		return $this->cena;
	}
 
	public function __call($n, $p)
	{
		$this->zmien_kurs();
		return array('koszt' => $p[0]*$this->cena, 'towar' => array('nazwa' => self::NAZWA, 'ilosc' => $p[0]));
	}
 
	private function zmien_kurs()
	{
		$zmiana=rand(1, 200)/100;
 
		switch(rand(1, 2))
		{
			case '1': $this->cena+=$zmiana;
			break;
			case '2': $this->cena-=$zmiana;
			break;
		}
	}
}
 
class Portfel
{
	private $kasa=0;
	private $inwestycje=array();
 
	public function __construct($kasa)
	{
		$this->kasa=$kasa;
	}
 
	public function sprawdz()
	{
		return array_merge(array('pieniadze' => $this->kasa), $this->inwestycje);
	}
 
	public function transakcja($inwestycja)
	{
		$this->kasa-=$inwestycja['koszt'];
		$this->inwestycje[$inwestycja['towar']['nazwa']]+=$inwestycja['towar']['ilosc'];
	}
}
 
$portfel= new Portfel(10000);
$zloto= new Gold;
 
echo '<br />Sprawdzenie:<br />';
foreach($portfel->sprawdz() as $key => $value)
{
	echo $key.': '.$value.'<br />';
}
 
//kupuję 24 sztabki złota
$portfel->transakcja($zloto->kup(24));
echo '<br />Aktualny kurs: ';
echo $zloto->kurs();
 
echo '<br />Sprawdzenie:<br />';
foreach($portfel->sprawdz() as $key => $value)
{
	echo $key.': '.$value.'<br />';
}
 
//sprzedaję 32 sztabki złota
$portfel->transakcja($zloto->sprzedaj(-32));
echo '<br />Aktualny kurs: ';
echo $zloto->kurs();
 
echo '<br />Sprawdzenie:<br />';
foreach($portfel->sprawdz() as $key => $value)
{
	echo $key.': '.$value.'<br />';
}
 
$portfel->transakcja($zloto->kup(53));
echo '<br />Aktualny kurs: ';
echo $zloto->kurs();
 
echo '<br />Sprawdzenie:<br />';
foreach($portfel->sprawdz() as $key => $value)
{
	echo $key.': '.$value.'<br />';
}
 
$portfel->transakcja($zloto->sprzedaj(-20));
echo '<br />Aktualny kurs: ';
echo $zloto->kurs();
 
echo '<br />Sprawdzenie:<br />';
foreach($portfel->sprawdz() as $key => $value)
{
	echo $key.': '.$value.'<br />';
}
 
$portfel->transakcja($zloto->sprzedaj(-25));
echo '<br />Aktualny kurs: ';
echo $zloto->kurs();
 
echo '<br />Sprawdzenie:<br />';
foreach($portfel->sprawdz() as $key => $value)
{
	echo $key.': '.$value.'<br />';
}
?>

Metody sprzedaj() lub kup() są wywoływane na obiekcie $zloto z przekazanym do nich argumentem typu int. Dla metody kup() argument ma zawsze wartość dodatnią i oznacza ilość kupionego towaru (dodatnia wartość oznacza, że zyskaliśmy tyle sztuk tego towaru), natomiast dla metody sprzedaj() argument ma wartość ujemną (pozbywamy się towaru). Wynik działania tych metod przekazywany jest do metody transakcja() wywołanej na obiekcie $portfel klasy Portfel.

Ponieważ metody sprzedaj() i kup() nie zostały zdefiniowane w klasie Gold, obsługą tych wywołań zajmie się metoda magiczna __call() Metoda przyjmuje dwa argumenty $n i $p. W pierwszym argumencie, w tym akurat przypadku nieużywanym, jest dostępna nazwa wywoływanej metody. Natomiast drugi argument jest tablicą argumentów, z jakimi metoda została wywołana. Metody sprzedaj() i kup() są wywoływane tylko z jednym argumentem więc $p jest tablicą jednoelementową. Wywołując metody sprzedaj() lub kup() robimy praktycznie to samo – zmieniamy ilość pieniędzy i złota w portfelu. To czy ta zmiana jest na plus czy na minus zależy od wartości argumentu, więc nazwy metod są zbędne.

Na dobrą sprawę moglibyśmy metodę __call() zastąpić np. metodą kupno_sprzedaz(), którą wywoływalibyśmy zupełnie jawnie w kodzie i nie byłoby tego całego zamieszania. Jednak dzięki użyciu metod sprzedaj() i kup() kod jest bardziej intuicyjny (pod warunkiem, że zna się metody magiczne). Poza tym dzięki różnym nazwom moglibyśmy zmodyfikować metodę __call() w taki sposób, by przy transakcji sprzedaży doliczana byłaby marża, a przy kupnie nie. Wystarczyłoby sprawdzić wówczas wartość argumentu $n. Zresztą możliwości jest dużo więcej, a __call() to tylko jedna z metod magicznych. Warto znać je wszystkie.

Pisząc ten artykuł postawiłem sobie zadanie przybliżenia pojęcia metod magicznych na przykładzie metody __call(). Gdyby jednak inne elementy przykładowego programu były dla Ciebie nie do końca zrozumiałe, napisz o tym w komentarzu poniżej. Chętnie wszystko wyjaśnię.