Może kogoś zdziwić ten tytuł. Co transmutacja, czyli znana z alchemii (i Diablo 2) zamiana jednego przedmiotu w inny ma wspólnego z PHP ? Ma wspólnego i to wiele. Wystarczy sobie uświadomić, że przedmioty to obiekty. To czym jest obiekt, determinuje jego klasa (a raczej podobieństwo obiektów tworzy klasę ale nie w tym rzecz). Jak wszyscy wiemy, w języku PHP, rzutowanie możliwe jest tylko pomiędzy typami prostymi oraz object
i array
. Odnosząc to na świat rzeczywisty, rzutowanie jest właśnie ową „transmutacją”.Jak już wspominałem, rzutowanie jest mocno ograniczone. Może rzadko, ale zdarza się, że chcielibyśmy zmienić typ jakiegoś obiektu. Ostatnio natrafiłem się na poniekąd podobną sytuację. Chciałem zaimplementować prosty DataMapper
. Tak dla małego przypomnienia jest to wzorzec projektowy, dzięki któremu możemy zapisywać obiekty w relacyjnej bazie danych oraz je stamtąd wyciągać. Czyli koniec końców jest to część składowa ORM-a. Jednak nie jest to mechanizm w stylu ActiveRecord
(Doctrine 1) – zapisywane klasy nie dziedziczą z żadnej klasy bazowej. DataMapper
(Doctrine 2), dzięki refleksji, potrafi sam dowiedzieć się wszystkiego o dowolnym obiekcie i zapisać go w bazie a później odtworzyć, bez konieczności „dostosowywania” obiektu do jego wymagań.
O ile jeszcze zapisanie obiektu w bazie to pikuś, to odtworzenie go jest w pewien sposób problematyczne. Problemem jest to, że jeżeli obiekt ma w sobie prywatne pola, które mają być zapisane w bazie, natomiast nie ma żadnych metod na ustawianie ich, to obiekt zawsze będzie odtworzony w sposób niekompletny. Mały przykład dydaktyczny:
class Example {
private $_foo = 5;
private $_bar = 6;
public function __get($name){
$sField = '_'. $name;
return $this->$sField;
}
public function __set($name, $value){
throw new LogicException('This object is readonly!');
}
public function doSomething(){
//...
}
}
Jak widać, możemy pobierać zawartość pól, jednak nie możemy ich ustawiać. Po krótkim namyślę jak rozwiązać problem nie zmieniając klasy Example, można się rozmarzyć, że w innym języku może dało by się zrzutować array
z danymi pobranymi z bazy na obiekt klasy Example
i było by po sprawie. Jak wiadomo, w PHP się nie da. Ale od czego mamy zakazane techniki ? Ta krótka funkcja zrealizuje to, czego nam potrzeba:
function transmutate(array $data, $className) {
$oReflector = new ReflectionClass($className);
$aDefaultProperties = $oReflector->getDefaultProperties();
$aInstanceVars = array_merge($aDefaultProperties, $data);
$aObjectPrototype = array();
foreach ($aInstanceVars as $field => $value) {
$sField = "\0" . $className . "\0" . $field;
$aObjectPrototype[$sField] = $value;
}
$oContainer = (object) $aObjectPrototype;
$sContainer = serialize($oContainer);
$aContainer = explode(':', $sContainer);
$aContainer[1] = strlen($className);
$aContainer[2] = '"' . $className . '"';
$sContainer = implode(':', $aContainer);
$oResult = unserialize($sContainer);
return $oResult;
}
Oraz przykład użycia:
$aData = array('_foo' => 7, '_bar' => 8);
$oExample = transmutate($aData, 'Example');
var_dump($oExample);
/* efekt:
object(Example)#3 (2) {
["_foo:private"]=>
int(7)
["_bar:private"]=>
int(8)
}
*/
Co do samej funkcji, to pomijając aspekt wyciągania informacji za pomocą refleksji. To używa ona funkcji serialize
i unserialize
. Jest to spowodowane tym, że PHP, odtwarzając zserializowany obiekt po pierwsze nie uruchamia jego konstruktora, a po drugie nie przejmuje się widocznością zmiennych i jest to główny klucz do sukcesu.
Podsumowując, jest to ciekawy trik, jednak jeżeli nie piszecie DataMappera
, lub nie jest to wasza ostatnia deska ratunku, nie używajcie go. Nie bez powodu jest to „zakazana” technika
P.S
Od bodajże PHP 5.3.2 albo 5.3.3 można uzyskać podobny efekt (zmiana wartości zmiennych prywatnych/chronionych z zewnątrz obiektu) poprzez metodę ReflectionProperty::setAccessible
.