Паблик Морозов на собеседовании
На собеседованиях каких вопросов только не встретишь. Матерые волки, собеседуя php-гуру, могут спрашивать разные нетривиальные вещи. Одна из таких вещей: паттерн “Паблик Морозов”.
Паблик Морозов — антипаттерн, позволяющий получить доступ к закрытым полям класса.
Встречаются как-то два программиста. Один — тимлид, другой php-гуру, оба с опытом дохреналет (писали на PHP, когда его еще не было). Начинается беседа, аки битва двух богатырей. Один задает вопросы коварнее другого. Другой парирует и отбивается, как ниндзя.
Вопрос: бла бла, ООП… Бла бла бла, инкапсуляция, бла бла бла… А можно ли получить значение из private поля экземпляра класса?
Конечно можно, для этого есть Reflection API. Допустим есть класс:
class PublicMorozov {
public $woo = 1;
private $foo = 2;
protected $bar = 3;
public function foo() { return $this->foo; }
}
Чтобы считать значения из инкапсулированных свойств (и даже изменить) мы можем использовать такой код:
$class = new ReflectionClass("PublicMorozov");
$property = $class->getProperty("foo");
$property->setAccessible(true);
$pm = new PublicMorozov();
var_dump( $property->getValue($pm) );
$property->setValue($pm, 456);
var_dump( $pm->foo() );
— Да, все верно, мы можем читать и писать в защищенные свойства через Reflection API. А еще способы знаешь?
— Ммм, ну можно это переписать так:
$pm = new PublicMorozov();
$property = new ReflectionProperty("PublicMorozov", "foo");
$property->setAccessible(true);
var_dump( $property->getValue($pm) );
$property->setValue($pm, 456);
var_dump( $pm->foo() );
— Да это же то же самое, ну немного короче. А что если… А если без использования Reflection API, м?
— Хм, ну есть несколько хаков…
— Не стенсяйся, показыавай!
— Ну мы можем написать кложуру, забиндить ее контекст на экземпляр класса и внутри вызвать значение, после чего вернуть его (значение):
$pm = new PublicMorozov();
$foo = Closure::bind(
function(PublicMorozov $pm){return $pm->foo;},null,$pm)($pm);
var_dump($foo);
— Нормалды, нормалды… Но я бы сократил запись:
$foo = Closure::bind(
function(){return $this->foo;},$pm,'PublicMorozov')($pm)
;
// или даже так
$foo = Closure::bind(function(){return $this->foo;},$pm,$pm)($pm);
Эту же запись можно еще укоротить:
$foo = ((function(){return $this->foo;})->bindTo($pm,$pm))();
— Ух ты! Да, прикольно.
— А поменять значение можем? Опять же без Reflection API.
— В принципе, да. Почему бы и нет. Мы можем вернуть не значение, а ссылку на свойство:
$foo = & Closure::bind(
function & (PublicMorozov $pm){return $pm->foo;},null,$pm)($pm)
;
$foo = 456;
var_dump($foo); // 456
var_dump( $pm->foo() ); // 456
— Круто! Все верно. И опять же я бы сделал это короче:
$foo = &((function & (){return $this->foo;})->bindTo($pm,$pm))();
А еще таким образом можно сделать простую универсальную отмычку для манкипатчинга, используя твой метод:
function & crackprop(object $obj, string $prop) {
return ( Closure::bind
(
function & () use ($prop) { return $this->$prop; }
, $obj, $obj
)
)();
}
$foo = &crackprop($pm, 'foo');
$foo = 456;
var_dump($foo);
var_dump($pm->foo());
Или, опять же, короче, как бы я написал:
function & crackprop(object $obj, string $prop) {
return (
(function & () use ($prop) { return $this->$prop; })
->bindTo($obj, $obj))()
;
}
— Все что выше было проделано для private свойств сработает для protected?
— Да, все то же самое можно проделать и с protected.
— Напоследок по этой теме, чисто по фану, можешь еще назвать способ получить доступ к защищенным свойствам?
— Да, есть еще один способ, я не стал его озвучивать так как это, вроде бы, уже давно всем известный хак, работавший еще с давних версий PHP. По сути это к вопросу как формируются имена защищенных свойств. Мы можем получить значения так:
function _protected(object $obj, string $prop) {
return ((array) $obj)["*$prop"];
}
function _private(object $obj, string $prop) {
return ((array) $obj)["".get_class($obj)."$prop"];
}
$foo = _private($pm, 'foo');
$bar = _protected($pm, 'bar');
Имя приватного свойства формируется как:
ClassNameproperty
а protected имеет формат:
*property
Раз заговорили про фан, то мы можем написать универсальную функцию на этом механизме с удобным способом через чейнинг:
function crack(object $obj) {
return new class ($obj) {
public function __construct(object $obj) {
$this->o = (array) $obj;
$this->c = get_class($obj);
}
public function __get(string $p) {
$r = @$this->o["*$p"];
is_null($r) or $r = @$this->o["{$this->c}$p"];
return $r;
}
};
}
$pm = new PublicMorozov();
var_dump( crack($pm)->foo ); // 2
var_dump( crack($pm)->bar ); // 3
— Но производительность такой красоты сильно проседает, естественно. Кстати, по такой же схеме можно создать экземпляр класса stdClass с приватными и протектекд полями:
$obj = (object) [
"stdClassfoo" => 1,
"*bar" => 2,
];
var_dump($obj);
object(stdClass)#1 (2) {
["foo":"stdClass":private]=>
int(1)
["bar":protected]=>
int(2)
}
— Вообще вы задаете такие интересные вопросы… А чем мне придется тут заниматься?
— Оу, ну у нас все хорошо, новые технологии, модный стек. Просто есть немного легаси кода, который нужно сапортить и, иногда, патчить. Но, судя по твоим ответам, у тебя все получится, даже не сомневайся. Мы делаем тебе оффер ?