Pull to refresh

Comments 67

Напишите пожалуйста что-нибудь про примеры применения и отладки АОП. Меня сильно останавливает возможность АОП случайно обвесить не те классы а потом выяснять почему всё сломалось.
Было бы неплохо ещё вкратце описать, как реализуется АОП в PHP — это фича новой версии PHP, расширение, препроцессор или что-то другое?
Реализуется АОП в PHP ручками )
В самом PHP это пока не планируется, хотя для версии 4.0 были попытки, я даже где-то видел грамматику лексера PHP с учетом аспектов, но это уже в прошлом. Из нового — есть PECL-расширение PHP-AOP, которое разрабатывается, но там куча багов, сегфолтов, да и скорость в отдельных случаях меньше, чем у моей PHP-библиотеки. Моя же библиотека написана на чистом PHP и не требует никаких дополнительных экзотических расширений, за счет этого она стабильна и активно развивается.
Идея работы такая — отслеживаем момент загрузки любого PHP-кода (перехватываются incude/require), делаем статический анализ кода, вносим в него необходимые правки, создаем декоратор, подменяем им оригинальный класс и сохраняем в кэше, дальше отдаем этот код парсеру PHP. После таких манипуляций в памяти PHP будет находиться класс, уже имеющий расширенную функциональность, но с прежним именем класса.
Это краткий обзор предыдущих серий Санта-Барбары под названием «как сделать АОП»
(перехватываются incude/require)
Вроде из предыдущего поста я понял, что перехватывается autoload, то на функцию в include аспект не повесить прозрачно.
ок, уточнение: я перехватываю именно require/include и как частный случай, получается, перехватываю autoload, потому что где-то в нем тоже есть include/require. Сама функция include/require для меня недосягаема, но вот путь к подключаемому файлу я изменить могу, что моя библиотека и делает.
Не понял.

Если в index.php первой строчкой у меня require 'superlib.php'; то могу я аспектами функцию из это йлибы перехватить или нет?
нет, не сможете, это под силу только расширению, такому как runkit, либо PHP-AOP. Тот код, который уже загружен в память, невозможно изменить со стороны PHP. Но вот если вы перед этим подключите АОП, а потом уже передадите управление на superlib.php, то методы в классах уже можно будет перехватить.
Это-то понятно, что сначала нужно подключить. Я о том, что можно ли аспектировать именно функции (для которых нет автолоада), а не методы в классах?
Это будет позже, вся теория и практические наброски у меня готовы. Можно будет перехватывать даже простые функции, например file_get_contens, mysqli_open и т.д. Но только если они будут использоваться в неймспейсе, а не в глобальном пространстве.
Каким образом вы изменяете путь к подключаемому файлу, если не секрет? Из ваших (очень запутанных) исходников я так и не смог этого понять…
Регистрируется фильтр потока php://filter/read, который передаёт тело файла цепочке трансформеров, среди которых есть FilterInjectorTransformer, заменяющий пути в аргументах, передаваемых include* и require*.

Насчёт запутанности исходников: там как раз всё хорошо структурировано, а по-настоящему запутанные исходники — они во всяких wordpress.
Ну то есть не перехватывается include/require :), эти директивы изменяются их только в тех файлах, которые уже распарсены. А парсятся файлы путем подмены autoload.

В общем, прикольно, конечно…
Меня формулировка автора тоже заинтриговала — полез в исходники, в надежде увидеть особую уличную магию. Разочарован не был — академическая ценность библиотеки достаточно высокая.
Да, там есть несколько мест, которые приводили меня в полный ступор, потому что не было возможности двинуться дальше из-за всяких ограничений языка. Два года потребовалось на то, чтобы решить этот большой ребус, но я рад, что у меня все получилось и я могу показать ее сообществу.
Надеюсь, она найдет свое место в приложениях, потому что область применения широка: контрактное программирование, логирование, кэширование, новые шаблоны программирования, новые подходы к разработке ПО.
Отличная работа. Жду новые эксперименты.
Эта статья как раз один из примеров управляемого «АОП»: вы в любой момент можете посмотреть на класс и понять, что он использует реализацию FluentInterface-а. А потом можно зайти в сам интерфейс FluentInterface, нажать «иерархия» и получить отчет о всех классах, которые его реализуют.
А можете подробнее рассказать про ваше последнее предложение? Как получить отчет о всех классах, которые используют этот интерфейс? Куда нажать и где найти «иерархия»?
Все очень просто (если у вас phpStorm): зайдите в нужный вам класс и на самом классе нажмите Ctrl+H. Я думал, что этот инструмент всем знаком. )
Ах PHPStorm, вы же об этом не упомянули в комментарии выше, спасибо.
«Fluent Interface» в данном случае, скорее, «связный (цепочечный, контекстносвязанный) синтаксис», по аналогии с «fluent English». хотя в словарях такого перевода нет. К примеру, было
Все, что нам нужно, чтобы использовать теперь текучий интерфейс в конкретном классе — просто добавить интерфейс — implements FluentInterface.
стало
Всё, что нам нужно, чтобы получить связный синтаксис в конкретном классе — просто добавить интерфейс — implements FluentInterface.

Вот, в Википедии написано:
Текучий интерфейс (англ. fluent interface, название придумано Эриком Эвансом и Мартином Фаулером)...
Но они-то англичане, им совершенно понятна аналогия между fluent English и Fluent Interface. А почему мы должны топорно калькировать их идиомы? Да, в некоторых языках метод реализации иногда совпадает с названием Interface, как в PHP и С#. Но для остальных языков никакой подсказки нет: видим использование контекстносвязанного синтаксиса, который называют «текучим интерфейсом» (?).

Но из плюсов — теперь ваша статья может служить источником этого утверждения в Википедии:: )
Такой стиль косвенно полезен повышением наглядности и интуитивности кода [источник не указан 692 дня].
Тоже долго думал над тем, как лучше перевести на русский язык этот шаблон, но так и не нашел корректного перевода, поэтому остановился на «текучем», хоть это название меня и раздражает. Я бы его переименовал в ChainInterface и тогда бы он прекрасно сочетался с MethodChaining и переводился бы просто «цепочечным интерфейсом».
Приходится отдавать дань дядюшке Фаулеру )
эм. вы правда предлагаете сделать обертку вокруг всех методов в и так не сомом быстром скриптовом языке?
Я предлагаю это сделать только для публичных сеттеров, оверхед при их использовании будет настолько мал, что вы даже его не увидите на общем фоне приложения. Вряд ли у вас в коде сеттеры вызываются несколько тысяч раз за запрос, ведь так?
Ну, вполне возможен вариант работы с 30к объектами в пределах одной сессии скрипта + инициализации каждого (объекта) десяткой сеттеров.
С другой стороны, может такие приложения не на PHP нужно писать (1), а также даже этот оверхед будет очень мал по сравнению с логикой работы приложения, которое оперирует таким количеством объектов (2).
1. Вижу большое количество проблем при автокомплите в IDE. На данный момент PHPStorm не всегда корректно справляется с обычными методами, унаследованными от другого класса и возвращающими $this, а в такой обертке про автокомплит можно забыть.
А без автокомплита я слабо представляю себе продуктивную работу.

2. Проблема написания большого количества геттеров и сеттеров решается либо средствами IDE, либо с помощью кодогенерации.
К примеру, у меня есть схема БД, одной консольной командой по ней генерируется базовый класс со всеми геттерами и сеттерами. Потом он наследуется «основным» и в нем уже вся реализация логики приложения.
При изменении схемы базовый класс автоматически перегенерируется, не затрагивая логику.
Спасибо за ваш комментарий, очень правильные замечания.
1. На текущий момент автокомплит для таких методов работать точно не будет, что очень неудобно. Но ваш комментарий подтолкнул меня к идее и я только что нашел способ обойти и эту проблему: надо позволить аспектам работать и с док-блоками. В этом случае в phpDoc-блоках можно будет с помощью совета добавить return self и в этом случае phpStorm радостно включает автокомплит на основе полученного класса в кэше.
2. Если у вас есть инструмент кодогенерации для Doctrine — это хорошо, но вот нет у меня уверенности, что весь vendor-код оборудован такими полезными утилитами. Да и ваш пулл-реквест в ядро большого фреймворка с проставленными сеттерами могут и не взять. А вот АОП это легко сделает, потому что это одна из полезных особенностей АОП — возможность дотягиваться до любого кода, внося в него свои правки на уровне приложения.
Если мне не изменяет память, return self, как и return static в phpStorm нормально так пока еще и не заработал.
Это уже задача разработчиков IDE — научиться понимать такие конструкции нормально. Потому что мы код документируем корректно.
Хотя у меня, вроде, и так все хорошо (phpStorm 127.100): построил сейчас цепочку из 10 методов с конструкцией @return self — все подхватывает правильно.
Хорошо, но не всё. При наследовании «новый» класс не подхватывается.

Хм, проверил static. Видимо таки сделали корректно работающими оба свойства:

Так тут все правильно же, из b() возвращается Test, в котором f() нет.
Значит для каждого метода надо будет написать аннотацию с @ return, причем явно указать какой класс возвращается.
Соотв. автокомплит для унаследованных классов будет ущербный. Как по мне, лучше и понятнее написать return $this в теле метода, чем в аннотации, и не городить магию.

Инструмент кодогенерации не доктриновский, но аналогичный, да. Не очень понял зачем мне делать pull-request в ядро большого (?) фреймворка, если я работаю на уровне приложения и все геттеры\сеттеры относятся к реализации моей модели.
Согласен, что реализация FluentInterface на АОП — не самая лучшая затея хотя бы с точки зрения логичности: наличие конструкции implements SomeInterface предполагает то, что класс будет содержать код реализующий интерфейс, чего при этом подходе нет.
Это концепция маркеров, вы с ней не сталкивались ранее? Когда, например, класс исключения помечают дополнительно такими маркерами, а потом ловят по классу интерфейса-маркера.

class AppException extends Exception implements ServiceLayerException
{
}

// ...

try {
    throw new AppException();
} catch (ServiceLayerException $e) {
    // .. service layer fault
}
Не обязательно вам писать аннотацию с @return, ее может вставить сама библиотека АОП, а IDE ее подхватит сама. Насчет магии — это ваш выбор, использовать или нет. Статья имеет исключительно ознакомительный характер, но ваши мысли и опасения мне понятны.

Не всегда сеттеры находятся в моделях. И не всегда они ваши и находятся на уровне приложения. Очень часто я встречаю код, где нужно настроить вендорский сервис, а в нем есть лишь тонна сеттеров и нет цепочечного интерфейса.
Очень часто я встречаю код, где нужно настроить вендорский сервис, а в нем есть лишь тонна сеттеров и нет цепочечного интерфейса.

Основной плюс АОП для меня это именно возможность адаптировать сторонний софт без ручного написания однотипных прокси и адаптеров
А вся «магия», связывающая аспект с реализациями интерфейса, находится в комментарии к методу:

@Around("within(FluentInterface+) && execution(public **->set*(*))")

Очень надеялся увидеть в статье побольше информации про этот магический синтаксис, но даже упоминания не нашел.
Автор просто рассчитывает, что читатель знаком с его предыдущими топиками.
Да, как вы уже поняли, это синтаксис, описывающий точки во всем коде, аналог SQL для данных. Синтаксис пришел из аннотаций Spring и AspectJ, поэтому с ними лучше ознакомиться отдельно. Если есть интерес — могу в отдельной статье рассмотреть этот синтаксис и как он применяется относительно к PHP. Хотя сейчас Go! поддерживает довольно небольшое количество срезов из того, что предлагает AspectJ.
при вызове публичных методов-сеттеров, начинающихся на set, и находящихся в классе, реализующем интерфейс FluentInterface — необходимо возвращать в качестве результата вызова метода сам объект, для которого осуществляется вызов, если метод ничего не возвращаетт

fixed

Интересное применение АОП, но вот не уверен, что нужно анализировать возврат сеттера. Сеттер по идее ничего возвращать не должен, процедура, а не функция. А если возвращает, то NULL может быть валидным значением.
Спасибо за уточнение, внес его в статью.
Насчет анализа возвращаемого значения — это делать необходимо, иначе можно случайно сломать существующий метод, который возвращает другое значение (например, экземпляр другого класса). Без проверки результата совет подменит результат и вернет его вместо оригинального без зазрения совести )
Для сеттеров довольно необычное поведение вообще что-то возвращать. А если явно возвращает NULL, то это будет штатное поведение и заменять его на this нельзя. По идее нужно анализировать код метода на предмет наличия в нем return: если есть, то нельзя чэйнить, если есть — нельзя.
Насколько я помню в PHP нет полноценного типа void, как следствие метод (с том числе и без return) всегда будет возращать null.
Слишком затратно для экономии одной строчки в коде метода.

Как демонстрация подхода — да, годно.
Как говорят: «копейка — рубль бережет». Так и здесь, в коде одного метода — да, всего одна строчка, в коде класса их может быть уже пара десятков, а в коде всего приложения — несколько тысяч.
А еще интереснее будет, если у вас уже есть эти несколько тысяч сеттеров по всему проекту и у вас есть необходимость сделать их цепочечными, чтобы было удобно работать.
С несколькими тысячами и производительность просядет уже заметно.

Вот если бы эти все аспектные дела оформить в pecl extension… :)
А где в голосовании вариант «Статья понравилась, но все слишком непрозрачно и непонятно, буду делать ручками по-старинке.»? :)
А этот вариант не подошел: «Статья понравилась, возьму на заметку, но пока не время для АОП, остановлюсь на ООП.»?
Нет, моя мысль была в том, что автору за его труд и описание шаблона определённо респект, т.к. я не исключаю, что есть люди, которые увидев предложенный метод, напишут лучший / более красивый / etc код, чем был до этого. В любом случае, каким бы шаблон ни был, он улучшает читаемость кода. Но моё личное мнение, что именно так, как описано в статье делать не надо.
Выше уже дали комментарии с предложением альтернатив, с некоторыми из которых я согласен.
UFO just landed and posted this here
А как-то так?
aaaaa
    ->method1('some text1')
    ->method2('some text2')
    ->method3('some text3')
;
bbbbbb
    ->method1('some text1')
    ->method2('some text2')
;
ccccc
    ->method1('some text1')
    ->method2('some text2')
;
bbbbb->method_c( c );
aaaaa->method_b( b );
UFO just landed and posted this here
Вроде все приличные IDE позволяют если не полностью красиво отформатировать код всего проекта, то очень близко к нему. И конструкции типа preg_match('/.*sometext.*/', substr(editText3->getText(), 10)) выносить в переменную практически на автомате.
красиво отформатированный код, который редко встречается в моей повседневной жизни

Если единственное, что вас смущает во fluent-интерфейсе это то, что ваши коллеги не умеют им пользоваться, то проблема решается на другом уровне. Научите их пользоваться автоформаттером (в любой нормальной IDE он есть), воспользуйтесь каким-нибудь StyleCop для проверки на сервере, или просто бейте по рукам :)
Отсутствие автокомплита, проблемы с дебагом и всё ради экономии на одной строчке?

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

p.s. У вас пример неудачный, а вот сама библиотека для AOP хороша
Это как в Pascal :) А вообще, да, иногда не хватает такой конструкции, особено в форме with $this
Вам в JavaScript этой конструкции мало :)? По-моему, эта конструкция удобна лишь в небольшом количестве случаев, а в остальных только добавляет путаницы. Например:

with $objectUser do {
    method1(strtolower($var));
    method2(method1('2'));
    method3('3')
}


Что должно произойти на первой строчке? Должна ли выполниться функция strtolower() или нужно искать метод strtolower() в объекте $objectUser?
На второй? А если существует глобальная функция method1()?
Поиск нужной реализации идет по восходящей от текущей области видимости до глобальной. Нормальное, логичное, ожидаемое поведение.
такая банальность, а сколько пафоса.
чем плох интерфейс? отладкой.

Обсуждение топика закончилось через 2 дня после его публикации… Но я всё же задам вопрос автору: Есть где-нибудь данные о возрастании стоимости исполнения кода, я имею ввиду на сколько милисекунд увеличится время исполнения кода. Введение дополнительных обработчиков не может быть бесплатным.
Есть данные с прошлой версии фреймворка на слабенькой машинке: performance test. Этот тест показывает, что 10000 итераций пустого метода выполняются за 3.0мс, тогда как с дополнительным советом в моей библиотеке — за 90мс. Если делать аналогичное с помощью экстеншена PHP-AOP, то получится за 25мс. Однако, скорость экстеншена очень сильно падает с ростом количества советов (обратите внимание, как начинает тормозить экстеншен, в случае если есть много срезов, которые не совпадают с измеряемым методом)
Тем не менее, это синтетический тест, потому что, как правило, сам метод работает в реальных условиях медленнее совета, вызывается всего раз или два и замедление скорости от совета практически незаметно из-за значительной оптимизации. Таким образом, добавление совета к большому методу почти не тормозит его работу и если этот метод вызывается один или несколько раз, то его даже не будет заметно.
Sign up to leave a comment.

Articles

Change theme settings