Pull to refresh

Comments 48

Начали за здравие, закончили за упокой. В конце, такое чувство, что вы торопились, быстрей бы написать статью, и в итоге, последние примеры проглотились.

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

И все будет крайне отлично )
Нет, наоборот, не торопился, у меня были все праздники на написание статьи, поэтому писал потихоньку, но хочется оставить интригу для будущих статей с примерами и рефакторингом. Это только вводная часть. Обязательно учту ваше пожелание на будущее.
Круто- использовал АОП в Spring`e, но даже не подозревал, что есть реализация для php.
По-сути, реализация этой библиотеки и представляет собой портированную версию Spring в PHP. Изучение Spring и AspectJ дало мне много полезной информации и пищи к размышлению. Я сейчас работаю над стандартизацией интерфейсов для АОП в PHP, они будут совместимы с аналогичными в Java: aopalliance/php. Возможно, удастся добиться совместимости между разными фреймворками и библиотеками в PHP как в Java.
UFO just landed and posted this here
Сами аспекты описываются классами, как в Spring-е и используются аннотации для задания поинткатов, однако есть возможность создавать Advisor-ы сразу в коде. Каждый аспект разбивается на серию независимых советников, которые регистрируются отдельно.
static $memoryCache — так и задумывалось, потому что один аспект связывается со всеми классами и более того, аспект — это синглтон, он регистрироваться должен только один раз.
А можно пример с кэшированием, когда в качестве ключа используется не имя метода, а один из параметров? Например, getUser($id) очевидно будет кэшировать запись с ключом = $id, в не «getUser»
Да, конечно, нужно просто при формировании ключа для кэширования добавить еще сами параметры метода, которые доступны через вызов $invocation->getArguments():

// ...
$key  = $class . ':' . $invocation->getMethod()->name . join(':', $invocation->getArguments());
// ...

… не забыв хотя бы отсортировать их содержимое.
Т.к. getUser($id, array $properties) сразу поломает вам всю модель.
И сразу возникает вопрос контроля за ключами подобного кеша, которые иногда необходимо сбрасывать.
Ага, как известно есть две основных проблемы в IT: инвалидация кеша и именование переменных. Тут уже нужно думать в каждом конкретном случае, как делать инвалидацию.
В принципе, это возможно с помощью аспектов и метаинформации. Например, для метода update($id, $value) можно использовать аннотацию @CacheInvalidator(get($id)), по которой совет поймет, что ему нужно сбрасывать кэш для метода get($id) с нужным значением $id.
Возможно, я покажусь тупым, но всё же спрошу.
Всё это мне кажется интересным, но примеры не до конца понятные.
Изначальные утверждения о простоте для меня обернулись тем, что я даже примеры до конца не понял. Вы вызываете методы своей библиотеки, но что они делают мне до конца не понятно.

И так вопрос: почему не сделать предельно простой класс в одним-двумя методами, в которых пустое либо в одну строчку тело, к которому добавляется логирование, потом добавить примитивный класс логирования и показать, как это работает, на пальцах, чтобы не было ни одной строчки, которую можно выбросить?
Можно склонировать библиотеку к себе, поставить зависимости и натравить веб-сервер на папку demos. В ней как раз лежит пример работы простого класса с вплетенными аспектами и кэшированием. Как говорится, лучше один раз увидеть вживую, чем сто раз услышать, а еще можно залезть туда с XDebug-ом.
Note that PHP versions before 5.4.0 will not work completely, if you try to use aspects for code that uses Late Static Binding (LSB) feature.

В чем дело? LSB в PHP поддерживается с версии 5.3
Ожидал этого вопроса. Вопрос на самом деле очень сложный в технической реализации и объяснении.
Но попробую объяснить. Когда перехватываются динамические методы — там все просто, мы всегда знаем объект благодаря $this. Поэтому нам не нужно указывать scope.
Со статическими методами все намного сложнее — нужно переопределить статический метод, после этого вызвать оригинальный метод через Reflection. А в PHP сейчас есть сложность с тем, чтобы указать scope при вызове статического метода через рефлексию. Вот поэтому и ломается LSB. Только в 5.4 можно создать замыкание в дочернем классе, которое форвардит запрос к родительскому методу и сменить ему scope на корректный через метод bindTo()
А в чем отличие вашего детища от, к примеру, AspectPHP?

Вполне успешно выполняет ту же задачу тем же способом (судя по беглому ознакомлению с вашими исходниками), код очень качественный и с документацией, никаких расширений PHP естественно не требует, работает с PHP 5.3
Посмотрел на AspectPHP, странно, что он мне не попался раньше.

Да, идея аналогичная — делать load-time рефлексию кода, но проанализировав ее, я понял, что скорость работы конечного кода (с вплетенными аспектами) тут явно не учитывалась, тогда как Go! и создавался вокруг этой цели. Отсюда я могу сделать предположение, что попытка использовать в боевом режиме AspectPHP на большом количестве классов — будет обречена на провал из-за низкой скорости конечного кода.

Помимо этого, AspectPHP не учитывает необходимость корректной работы с опкод-кэшером, поэтому будут проблемы с этим в боевом режиме. Ну и еще Go! умеет перехватывать обращения к публичным и защищенным свойствам объектов (с ограничениями, правда), а также умеет работать с метаинформацией классов в load-time благодаря интеграции с Doctrine2. Скоро еще будет Introduction для PHP 5.4 с использованием трейтов.
Кажется красивее будет Traits использовать из php 5.4
Именно так будет в скором времени реализована поддержка Introduction, как раз с помощью трейтов. Можно будет указать: добавь пожалуйста указанный интерфейс к следующим классам, а также, добавь еще и реализацию интерфейса с помощью трейта такого-то.
Как я понял, навешивание аспектов происходит через Reflection? Можете сказать, какой оверхед появляется за счет этого?
Само вплетение аспектов проводится ровно один раз через load-time рефлексию, оверхед не измерял, потому что он не имеет особого значения в режиме нормальной работы приложения. Код с вплетенными аспектами кладется в кэш и дальше вызываются сразу нужные методы с нужными обработчиками. Никакой runtime-проверки на различные условия не выполняется. Все советы (Closure) сериализуются в самом коде класса и дальше могут быть закэшированы опкод-кэшером.
Выполнение методов с советами максимально оптимизировано и сопоставимо со скоростью выполнения метода при добавлении аналогичного совета для метода на уровне С, к примеру, с помощью экстеншена PHP-AOP.

Замерил скорость выполнения кода с советами в Go! и экстеншена PHP-AOP. Тест: 10000 итераций, один совет, в котором мы выводим на экран перед вызовом метода класса его имя, имя метода и аргументы. Результат:
Экстеншен: 428-604 мс
Моя либа: 502-710 мс
Без советов: 50-60мс
Уже где-то использовали это решение в продакшене?
В настоящий момент — еще нет, но само решение к продакшену готово.
Поэтому сразу предупреждение для всех — разворачивайте код с аспектами параллельно, а не вместо основного кода.
Фишка АОП в том, что вы не меняете код приложения, поэтому можно написать код и включать аспекты только при наличии волшебного параметра в запросе — это делается запросто. Нет параметра в запросе — не используем аспекты вообще, никаких изменений в логике приложения. Есть наш заветный ключик — переключаемся именно для нашего запроса на код с аспектами. После того, как проверите, что все ок — убираем наш ключик и переходим на аспекты.
медленнее обычного кода в 10 раз, ну это как то круто, чтобы использовать в реальных проектах, не думаю, что кто-то может себе такое позволить использовать.

Где реально это может пригодиться?
Это практически никакой оверхед. Покажите мне код, который за свой цикл Request-Process-Response исполнит 10к советов.
Уточнение: не в 10 раз, потому что если бы не было вынесенного кода в совете, то он бы был в самом методе, точнее, повсюду в методах в разных формах и вариациях. По-факту, при одинаковом функционале в коде совета и в коде метода получается пропорция 4:1-5:1, что весьма неплохо.

Тут огорчу любителей паттерна «Прокси» — у них этот же код работает еще медленней, чем код с аспектом за счет использования __call(), func_get_args() и call_user_func_array(). Но об этом никто не вспоминает при сравнении с АОП, а ведь надо сравнивать с альтернативой аналогичного кода.

Идем дальше, берем реальные цифры. Вызов одного метода без аспекта, но с такой же логикой — 10 мкс, с аспектом — 50 мкс. В случае с хайлоадом это очень и очень важно, а если вы работаете над бизнес-приложениями, как я, то для вас не будут новинкой методы, работающие и несколько минут, но для которых очень важно иметь читаемый и удобный код, который постоянно нужно править.

И последний довод — аспекты используются для тех методов, которые вызываются редко, но они очень важны, оверхед в пару десятков мкс на них даже не заметен. И понятно, что указывать все методы всех классов и логировать все, получая замедление в 5 раз — выстрел в себе в ногу.
Все нужно использовать с умом и правильно, как и любой другой инструмент.
Напоминает попытку реализовать декораторы.
Например как в python
def get(key):
    print 'get'
    return 'get(%s)' % key

print get(42)
print get(42)

Выдает:
get
get(42)
get
get(42)

А с простым «кеширующим» декоратором:
def cache(func):
    _dict = {}
    def _cache(*args, **kawrgs):
        if func not in _dict:
            _dict[func] = func(*args, **kawrgs)
        return _dict[func]
    return _cache

@cache
def get(key):
    print 'get'
    return 'get(%s)' % key

print get(42)
print get(42)


Выдает:
get
get(42)
get(42)


Canonical uses of function decorators are for creating class methods or static methods, adding function attributes, tracing, setting pre- and postconditions, and synchronisation, but can be used for far more besides, including tail recursion elimination, memoization and even improving the writing of decorators.
к сожалению декораторы создают ещё один объект(класс или функцию) а то и не один :(
с другой стороны не такой уж оверхед. один раз делается же.
но в php нет таких удобных декораторов :)
Ну то всего один небольшой кусок кода с замыканием, а то разбор аннотаций в комментариях и генерация чего-то где-то непрозрачно для разработчика.
Ну это Symfony-coupled код, а Go не зависит от платформы
Да, помимо завязки на контейнер симфони и ее архитектуру, есть еще несколько отличий. Если попробовать перехватить статический метод — JMSAopBundle отдыхает, перехватить метод в финальном классе — опять отдыхает. Пишем явно (new Class())->methodName() — опять отдыхает. У меня будет еще Introduction и перехват финальных методов, чем JMSAopBundle еще не скоро обзаведется.
Однако создание декораторов и подмена их в классе весьма неплохое решение и для работы в Symfony2 с АОП я его рекомендую больше, чем Go! Но вот не все он умеет, далеко не все…
Утром на Хабре — вечером в PHP Weekly. Только что пришла рассылка. :)
И поздравляю с топ1 по Most Starred Today в PHP на гитхабе.
Ого, неожиданно )) Спасибо всем за отзывы и stars. Буду и дальше воевать за АОП )
Если попытаться завернуть кэширующий прокси поверх логирующего, а тот, в свою очередь, поверх основного класса, то скорость упадет на порядок.
Странно, потому как на моей практике профилирования кешируюших/логируюших прокси классов, время вызова этих __call & __get в среднем на порядки меньше вызова самих кеширующих/логирующих методов. Ну т.е. издержки от их использования составляют 0.1-0.5%.
Все верно подмечено, что вызов даже магического метода будет быстрее если он отдаст данные из кэша. Это же справедливо и для аспектов, только работает еще быстрее, потому что там нет вообще этих магических методов, только прямые вызовы.
На самом деле гораздо больше неприятностей доставляет тот факт, что использование прокси-классов не дает возможности использовать в методах указание ожидаемых интерфейсов и классов.
использование прокси-классов не дает возможности использовать в методах указание ожидаемых интерфейсов и классов
А разве phpdoc annotations не решают эту проблему?
Я про тайпхинтинг по интерфейсу при прокидывании этого объекта в качестве зависимости в другой класс.
Напоминает триггеры в СУБД
статья интересная, спасибо. но мне интересно, нельзя ли избавиться вот от этого

$obj   = $invocation->getThis();
$class = is_object($obj) ? get_class($obj) : $obj;
$key   = $class . ':' . $invocation->getMethod()->name;


вроде бы это как раз то, против чего и направлена мысль, озвученная в посте?
Да, выглядит некрасиво. Но даже если это не реализовано автором, всегда можно
самостоятельно завернуть
class MyInvocation extends InvocationClass
{
    public function foo()
    {
        $obj   = $this->getThis();
        $class = is_object($obj) ? get_class($obj) : $obj;
        $key   = $class . ':' . $this->getMethod()->name;

        return array($obj, $class, $key);
    }
}
...
/**
 * @var MyInvocation $invocation
 */
list($obj, $class, $key) = $invocation->foo();


Ну или вернуть stdClass/отдельный класс с тремя публичными полями…
Буквально в первом абзаце упоминается термин АОП, а его расшифровка появляется только под конец. Немного нелогично. А так в целом статья на хорошо!
В теории красиво, а есть какой-нибудь крупный проект, выполненный по этой методологии, который можно было бы выложить в открытый доступ?
Нет, в открытом доступе пока ничего нет. Но есть примеры в самой библиотеке, а также у меня на гитхабе есть пару тестовых приложений для ZF2 и SF2, которые работают с АОП. Ссылка на ZF2 есть в списке ссылок. Это как раз сделано для практики и оценки скорости работы.
Да, для несведущего примеров из жизни подсыпать бы. Теория — она теория и есть… А так, статья в моём случае свою задачу выполнила — разбудила интерес к АОП. Спасибо!
Уважаемое хабрасообщество, что еще вам было бы интересно узнать по АОП в PHP? Также интересно получить обратную связь от тех, кто попробовал установить библиотеку и поработал с аспектами.
Я знаю, что есть небольшая парочка умельцев, которые не побоялись засучить рукава и поставить библиотеку на своем локальном компьютере )
АОП это круто. Но создание аспектов выглядит пока что страшновато.
github.com/lisachenko/go-aop-php#5-create-an-aspect

Планируется ли возможность добавления в виде анонимных функций? Все таки это php а не java.
Думаю, что код большого приложения без аспектов выглядит еще более страшновато )

А теперь по-делу: задавать советы в виде анонимных функции — нет проблем. Внутри библиотека так и работает. Каждый аспект разбивается на серию советников (Advisor), которая регистрируется в контейнере. Советник по своей природе — это пара, состоящая из регулярного выражения для точки кода и замыкания (совета), полученного из метода аспекта. Поэтому можно легко регистрировать в ядре свои советники напрямую, API для этого уже есть.
Это фундамент на будущее, чтобы можно было загружать аспекты откуда угодно: из xml, из yml, с помощью CompilerPass-ов и другого. Однако использование аспектов — более предпочтительно, потому что позволяет структурировать код АОП в логические блоки, как классы в ООП. Тогда как советник в АОП сопоставим с простой функцией в ООП.
Sign up to leave a comment.

Articles