Получаем доступ к приватным свойствам объектов в PHP без рефлексии

http://ocramius.github.io/blog/accessing-private-php-class-members-without-reflection/
  • Перевод
Несколько недель назад я работал над проблемой в ProxyManager. Проблема была проста: ReflectionClass и ReflectionProperty очень, очень, и ооочень медленные!
Причиной этого исследования является моя попытка оптимизировать "hydrator" для работы с большими объемами данных без накладных расходов на инициализацию.

PHP 5.4 выручай!



PHP 5.4 дал нам новое API для замыканий и метод Closure#bind().
Closure#bind() в принципе позволяет получить экземпляр замыкания с областью видимости данного класса, или объекта. Изящно! Вот так можно добавить API к существующим объектам!
Давайте же нарушим инкапсуляцию объектов в соответствии с нашими потребностями.
Методы доступа к приватным свойствам уже объяснялись в документации, но я все равно приведу пример.
Украдем приватное свойство Kitchen#yummy:
<?php

class Kitchen
{
    private $yummy = 'cake';
}

Определим замыкание для получения этого поля:
<?php

$sweetsThief = function (Kitchen $kitchen) {
    return $kitchen->yummy;
}

А теперь украдем yummy из экземпляра Kitchen:
<?php

$kitchen = new Kitchen();

var_dump($sweetsThief($kitchen));

К сожалению, мы получим фатальную ошибку в $sweetsThief:
Fatal error: Cannot access private property Kitchen::$yummy in [...] on line [...]

Сделаем нашего вора умнее Closure#bind():
<?php

$kitchen = new Kitchen();

// Closure::bind() на самом деле создает новое замыкание
$sweetsThief = Closure::bind($sweetsThief, null, $kitchen);

var_dump($sweetsThief($kitchen));

Удача!

Closure::bind vs Reflection: быстродействие


Я сделал простой бенчмарк для 100000 итераций инициализации:
<?php

for ($i = 0; $i < 100000; $i += 1) {
    $sweetsThief = Closure::bind(function (Kitchen $kitchen) {
        return $kitchen->yummy;
    }, null, 'Kitchen');
}

<?php

for ($i = 0; $i < 100000; $i += 1) {
    $sweetsThief = new ReflectionProperty('Kitchen', 'yummy');
    $sweetsThief->setAccessible(true);
}

На только что скомпилированном PHP 5.5 (Ubuntu 13.04 amd64 box), первый тест занял 0.325 секунд, а второй 0.658.

Рефлексия здесь гораздо медленнее.(На 49%)

Но это совсем не интересно, так как никому не потребуется инициализировать 100000 раз одно и то же, по крайней мере мне точно. Что на самом деле интересно — так это доступ к приватным свойствам. Я протестировал и это тоже:
<?php

$kitchen = new Kitchen();

$sweetsThief = Closure::bind(function (Kitchen $kitchen) {
    return $kitchen->yummy;
}, null, 'Kitchen');

for ($i = 0; $i < 100000; $i += 1) {
    $sweetsThief($kitchen);
}

<?php

$kitchen = new Kitchen();

$sweetsThief = new ReflectionProperty('Kitchen', 'yummy');
$sweetsThief->setAccessible(true);

for ($i = 0; $i < 100000; $i += 1) {
    $sweetsThief->getValue($kitchen);
}

Первый тест занял ~ 0.110 секунд, а второй ~ 0.199!
Это гораздо быстрее рефлексии, впечатляет!(На 55%)

Доступ к приватным свойствам по ссылкам


Есть еще одно преимущество, используя замыкания вместо рефлексии мы можем работать с свойствами объекта по ссылкам!
<?php

$sweetsThief = Closure::bind(function & (Kitchen $kitchen) {
    return $kitchen->yummy;
}, null, $kitchen);


$cake = & $sweetsThief($kitchen);
$cake = 'lie';

var_dump('the cake is a ' . $sweetsThief($kitchen));


Универсальный метод доступа


Учитывая выше сказанное, мы можем написать простую обертку для получения любого свойства любого объекта:
<?php

$reader = function & ($object, $property) {
    $value = & Closure::bind(function & () use ($property) {
        return $this->$property;
    }, $object, $object)->__invoke();

    return $value;
};

$kitchen = new Kitchen();
$cake    = & $reader($kitchen, 'cake');
$cake    = 'sorry, I ate it!';

var_dump($kitchen);

Рабочий пример.
У нас есть доступ к любому свойству, в любом месте, и даже по ссылке. Ура! Мы нарушили правила еще раз!

Заключение


В очередной раз PHP показал себя с хорошей и плохой стороны одновременно. Это ужасный язык, с ужасным синтаксисом, но он позволяет нам обходить любые языковые ограничения радуя нас новой функциональностью с каждым релизом.
Я не буду использовать эту технику сам, но если мне потребуется получить приватное свойство объекта, да еще и по ссылке, я сделаю это именно так.

Дисклеймер: используйте данную возможность с осторожностью!
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 34
  • +2
    >… очень, очень, и ооочень медленные!
    > Рефлексия здесь гораздо медленнее.(На 49%)
    На 100000 интеракциях? Зачем автору столько?
    • +3
      Более того, написанный код на порядок сложнее читается чем рефлексии, и синтаксис языка тут совершенно ни при чем.
      • 0
        Посмотрите комментарии к статье. Там поднимаются те же вопросы, автор указывает на то, что подобные оптимизации стоит использовать там, где их никто не увидит, в классах хелперах, с обертками — UnitOfWork в doctrine, hydrator в zend.
        А по поводу синтаксиса — это мне кажется крик души на счет постоянных упреков в сторону php.
      • 0
        Затем, что эта штука применяется в Symfony2 и ZF2 для реализации Lazy-сервисов в контейнере. И производительность должна быть очень хорошей, иначе профита было бы мало в случае массового применения в приложении.
      • 0
        Кхм, эта статья — результат моего обсуждения на гитхабе с Marco и Matthieu )) Но, пожалуйста, сделайте правки в тексте: Marco — это он, а то как-то неудобно даже это видеть.
        • +2
          Есть старый проверенный способ безо всяких замыканий :)
          • 0
            По моему суть не в получении значения поля, а скорее установки, а с этим способом прийдется провернуть хак с serialize/unserialize, что медленнее
          • +21
            Сразу вспомнилось

            #define private public
            

            От php я далек, но если уж свойство делают закрытым, то как раз ради ограничения доступа к нему, потому что прямые обращения автор класса счел нежелательными или даже опасными. Подобные нарушения реально нужны или рассматриваются только ради спортивного интереса?
            • –6
              Нет. Это очень удобно при реализации ОРМ. В php есть нативные средства чтобы создавать объекты сразу из данных из БД, но они не позволяют сортировать их по объектам.
              Также все это можно сделать через рефлексию. Да и судя по документации Closure::bind был придуман именно для таких целей
              • +4
                Интересно конечно, но выглядит как приемчики старого хакера :) Заламывать руки чтобы достучаться до того, что в общем для такого доступа не задумано. В разрезе самого орм производительность ощутимо выросла? В доке действительно пример показывает такой хитрый доступ к приватным переменным… атас. Ломают сами себя.
                • –4
                  Лично я считаю, что все эти private и protected должны носить строго рекомендательный и справочный характер: сказываться только на подсказках, выдаваемых IDE. Объясню почему. Вот нужно, допустим, расширить класс, написанный не мной… а автор заприватил, остатся только форк делать (если не учитывать возможность подобных хаков), что нежелательно — его приходится поддерживать, сливать и т.д… Короче, если используешь private свойства (читай недокументированные функции), то будь готов, что после очередного рефакторинга, произведенного автором, твое расширение работать не будет. Почему же нельзя получить доступ к протектед извне вообще не доходит: к ним можно получить доступ просто унаследовав класс.

                  Короче, хороший язык должен давать возможность изменить чужой класс так, как разработчику нужно, не делая форка.

                  Да, и вот ещё ссылка в тему про #define private public
                  www.gotw.ca/gotw/076.htm
                  • +3
                    Я стараюсь все переменные делать protected, Это дает возможность наследования без проблем.
                    • +14
                      Извините, но это какой-то бред. Если автор заприватил и надо расширить, то надо либо понять почему заприватил и соответственно отрефакторить, либо (если надо экстремально быстро расширить) сделать дополнительный геттер в крайнем случае, основанный на существующих методах с задуманным автором доступом к ним. Слепые хаки доступов ведут к трудно диагностируемым проблемам там где их особо никто не ждет.
                      Про форки и «хорошие языки которые дают возможность изменить чужой класс..» не готов к ответу :)
                    • 0
                      Это скрипты, они не статически типизированы, они платят за это скоростью, но все это должно бы приносить дополнительные удобства, вот на пример в виде подмены обычных ассоциативных массивов объектами.
                      • +2
                        Ээ… не понял про скрипты. Выигрыш 0,3 секунды на 100к выполнений даже таких простых операций — выигрыш довольно сомнительный. Есть способы проще и эффективнее увеличить производительность. Похоже на экономию на спичках имхо. Про удобства перевода массива в объект… В чем именно это удобство?
                        Мне на самом деле интереснее где эту фичу ещё можно применить. Как-то сходу ничего в голову не приходит.
                        • –4
                          Почитайте про ORM
                          • +5
                            Я бы предложил вам, почитать Маконнела, и других интересных авторов, чтобы не писать подобное…
                    • +2
                      Представьте, например, ситуацию, когда в классе есть закрытое поле, в котором хранится количество внешних неуправляемых объектов (скажем, записей в таблице какой-нибудь БД). Весь механизм синхронизации основан на том, что поле со значением ноль считается неинициализированным. Затем это поле вскрывается и сохраняется в базу данных. Потом класс восстанавливается. Значение поля больше не актуально (внешнюю таблицу могли отредактировать), но его значение не равно нулю — поэтому механизмы синхронизации класса «спят». Если класс достаточно сложный, поиск проблемы займет немало времени.

                      Это все похоже на предложение редактировать содержимое баз данных с помощью обычных текстовых редакторов, чтобы обойти ограничения целостности.
                    • 0
                      #define private public
                      Такая конструкция, по-моему, используется для того чтобы в модуле, запускающем unit-тесты был доступ к приватным полям.
                      • –3
                        На самом деле, во многих динамических языках это хорошая практика. Доступиться до приватного свойства сложно, но можно.
                        И это хорошо, я считаю. Мы не настолько криворукие как могут считать разработчики либ, зачем нас искусственно ограничивать?
                        • 0
                          Это не ограничение, что доказывает возможность достучаться. Это disclamer — мол, если полез в приватное свойство, то пеняй на себя в случае чего.
                          • –3
                            Все продемонстрированные возможности достучаться явно слишком сложны. Неужели вам недостаточно просто подчеркивания в перед названием свойства, чтобы понять, что «пеняй на себя в случае чего»? Вы настолько себе не доверяете?
                            • +1
                              Все продемонстрированные возможности достучаться явно слишком сложны

                              Сложны для чего? Если они настолько сложны, может, проще написать автору и попросить добавить getter, или переделать самому, если лицензия позволяет?

                              Неужели вам недостаточно просто подчеркивания в перед названием свойства, чтобы понять, что «пеняй на себя в случае чего»? Вы настолько себе не доверяете?


                              Посмотрим: private — универсален, есть в большинстве языков. Автоматически отлавливается компилятором или интерпретатором. Подчерк перед названием свойства — это просто дополнительное синтаксическое соглашение.

                              Более того, насколько я знаю, подчерк добавляется только ради того, чтобы не занимать обычные имена: $_point вместо $point чтобы можно было делать $place->point и обрабатывать его магическими методами. Индикация тут второстепенна.

                              Итак: проверенный, универсальный метод с автоматическим отловом ошибок VS синтаксическое соглашение, несущее попутно со своим основным назначением роль индикатора.
                              Кто же победит?
                              • –2
                                > Если они настолько сложны, может, проще написать автору и попросить добавить getter,
                                > или переделать самому, если лицензия позволяет?
                                Очените пожалуйста трудозатраты и абсолютное время, которое пройдет на написание автору, его реакцию, доказательство необходимости, уговоры, что автору на самом деле нужно поддерживать еще один публичный интерфейс, ожидание когда у него до этого дойдут руки, исправления, выпуску новой версии.

                                В языках, где нет private это делается намного проще. На свой страх и риск, конечно, и с некоторой вероятностью, что все сломается в следующей версии.

                                > Посмотрим: private — универсален, есть в большинстве языков.
                                Я не знаю, что вы подразумеваете под универсальностью, но в большинстве более менее распространенных языков его точно нет.

                                > Автоматически отлавливается компилятором или интерпретатором.
                                Куда отлавливаются?
                                • –1
                                  Очените пожалуйста трудозатраты и абсолютное время, которое пройдет на написание автору, его реакцию, доказательство необходимости, уговоры, что автору на самом деле нужно поддерживать еще один публичный интерфейс, ожидание когда у него до этого дойдут руки, исправления, выпуску новой версии.


                                  Вполне может быть, что они окажутся меньше, чем поддержка своей версии, а так же поиск ошибки из-за внезапной смены работы с этой внутренней переменной.

                                  С другой стороны, если вы используете чей-то код, то не поддерживать никакого контакта с апстримом я считаю странной практикой.

                                  На случай же, когда всё действительно горит, есть способы обойти private.

                                  в большинстве более менее распространенных языков его точно нет.

                                  Если вам не сложно, пожалуйста, приведите список этих «более-менее распространённых языков» без private.

                                  Куда отлавливаются?

                                  Конкретно в PHP — генерируется ошибка в случае, когда кто-то обращается извне к приватной переменной. В плюсах компилятор тоже ругается.
                        • +1
                          Проблема в том, что с замыканием мы должны знать, как называются приватные свойства (возьмем скомпилированный класс, чтобы не было доступа к исходникам, ведь если есть доступ — то никто не мешает поменять видимость прямо в них), а для этого придется использовать рефлексию.

                          Но вообще мне кажется, что это бага.
                          • 0
                            Посмотрите на пример в документации, для этого этот метод и задумывался
                          • +2
                            А зачем? Если они приватны значит это не просто так. Или я чего то не понимаю.
                            • –2
                              У php в ядре есть возможность создания объекта любого класса в обход вызова конструктора. Вот эта возможность бывает очень нужна. Фактически это даже не полноценные классы, это структуры. Самый простой пример — реализация ОРМ
                              • +1
                                ORM ORM, расскажи где это в ORM необходимо такое? вопрос конкретно про приватные свойства класса.
                                • 0
                                  Например то, чего мы не можем поменять в объекте. То есть сохраняются в базу изменения же, так вот хочется чтобы не было метода setId, но была возможность установить этот самый id
                                  • +3
                                    Зачем?
                                    • –3
                                      что значит зачем? Я хочу связать объект с строкой из таблицы. Я хочу чтобы эта связь была жесткой, чтобы у программиста не было возможность установить id самостоятельно

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.