Пользователь
0,0
рейтинг
15 июля 2013 в 21:56

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

PHP*
Несколько недель назад я работал над проблемой в 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 показал себя с хорошей и плохой стороны одновременно. Это ужасный язык, с ужасным синтаксисом, но он позволяет нам обходить любые языковые ограничения радуя нас новой функциональностью с каждым релизом.
Я не буду использовать эту технику сам, но если мне потребуется получить приватное свойство объекта, да еще и по ссылке, я сделаю это именно так.

Дисклеймер: используйте данную возможность с осторожностью!
Перевод: Marco Pivetta
Никита Гусаков @hell0w0rd
карма
25,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (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 самостоятельно

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