Избавляемся от дублирования сквозного кода в PHP: рефакторинг кода с АОП

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

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

Эта статья о том, как можно избавиться от дублирования сквозного кода, и как сделать его чуточку лучше с помощью АОП.



Сквозная функциональность или «мокрый» код


С вероятностью около 95% в любом приложении можно найти куски сквозной функциональности, которые прячутся в коде под видом кэширования, логирования, обработки исключений, транзакционного контроля и разграничения прав доступа. Как вы уже догадались из названия, эта функциональность живет на всех слоях приложения (у вас же есть слои?) и вынуждает нас нарушать несколько важных принципов: DRY и KISS. Нарушая принцип DRY, вы автоматически начинаете использовать принцип WET и код становится «мокрым», что отражается в виде увеличения метрик Lines of Code (LOC), Weighted Method Count (WMC), Cyclomatic Complexity (CCN).

Давайте посмотрим, как это происходит в реальной жизни. Приходит техническое задание, по нему проектируется система, проводится декомпозиция на классы и описываются необходимые методы. На этом этапе система совершенна, понятно назначение каждого класса и сервиса, все просто и логично. А дальше сквозная функциональность начинает диктовать свои правила и заставляет программиста вносить правки в код всех классов, так как в ООП нет возможности провести декомпозицию по сквозному функционалу. Этот процесс протекает незаметно, потому что все привыкли к нему, как к обычному явлению, и никто не пытается что-либо исправить. Процесс идет по стандартной схеме, отработанной годами.

Сначала пишется логика самого метода, которая содержит необходимую и достаточную реализацию:

/**
 * Creates a new user
 *
 * @param string $newUsername Name for a new user
 */
public function createNewUser($newUsername)
{
    $user = new User();
    $user->setName($newUsername);

    $this->entityManager->persist($user);
    $this->entityManager->flush();
}


… после этого добавляем еще 3 строчки кода на проверку прав доступа

/** ... */
public function createNewUser($newUsername)
{
    if (!$this->security->isGranted('ROLE_ADMIN')) {
        throw new AccessDeniedException();
    }
    
    $user = new User();
    $user->setName($newUsername);

    $this->entityManager->persist($user);
    $this->entityManager->flush();
}


… потом еще 2 строчки для логирования начала и конца выполнения метода

/** ... */
public function createNewUser($newUsername)
{
    if (!$this->security->isGranted('ROLE_ADMIN')) {
        throw new AccessDeniedException();
    }

    $this->logger->info("Creating a new user {$newUsername}");

    $user = new User();
    $user->setName($newUsername);

    $this->entityManager->persist($user);
    $this->entityManager->flush();

    $this->logger->info("User {$newUsername} was created");
}


Узнаете свой код? Еще нет? Тогда давайте добавим туда еще 5 строчек обработки возможной исключительной ситуации с нескольким разными обработчиками. В методах, возвращающих данные, может добавиться еще 5 строчек для сохранения результата в кэше. Таким образом, из 4 строчек кода, которые реально имеют ценность, может получиться порядка 20 строк кода. Чем это грозит, думаю понятно — метод становится сложнее, его труднее читать, дольше приходится разбираться с тем, что реально он делает, сложнее тестировать, потому что приходится подсовывать моки для логера, кэша и т.д. Так как пример был для одного из методов, то логично предположить, что утверждения относительно размера метода справедливы и для класса, и для всей системы в целом. Чем старше код системы, тем больше он обрастает подобным мусором и становится все тяжелее следить за ним.

Давайте посмотрим на существующие решения проблем сквозной функциональности.

Чистота — залог здоровья! Здоровье прежде всего!


Заголовок я рекомендую читать в контексте разработки приложений так: «Чистота кода — залог здоровья приложения! Здоровье приложения прежде всего!». Было бы неплохо повесить такую табличку перед каждым разработчиком, чтобы всегда об этом помнить :)

Итак, мы решили всеми силами содержать код в чистоте. Какие решения у нас есть и что мы можем использовать?

Декораторы


Декораторы — первое, что приходит на ум. Декоратор — структурный шаблон проектирования, предназначенный для динамического подключения дополнительного поведения к объекту. Шаблон Декоратор предоставляет гибкую альтернативу практике создания подклассов с целью расширения функциональности.

Когда разговор заходит об АОП, первый вопрос, который обычно задают ООП-программисты — почему не использовать обычный декоратор? И это правильно! Потому что декоратором можно сделать почти все то, что делается с помощью АОП, но… Контр-пример: что если мы сделаем LoggingDecorator поверх CachingDecorator, а последний, в свою очередь, поверх основного класса? Сколько однотипного кода будет в этих декораторах? Сколько различных классов декораторов будет во всей системе?

Легко прикинуть, что если у нас 100 классов, реализующих 100 интерфейсов, то добавление кэширующих декораторов добавит нам в систему еще 100 классов. Конечно, это не проблема в современном мире (загляните в папку cache любого большого фреймворка), но зачем нам нужны эти 100 однотипных классов? Непонятно, согласитесь?

Тем не менее, умеренное использование декораторов полностью оправданно.

Прокси-классы


Прокси-классы — второе, что приходит на ум. Прокси — шаблон проектирования, предоставляет объект, который контролирует доступ к другому объекту, перехватывая все вызовы (выполняет функцию контейнера).

Не очень хорошее решение с моей точки зрения, но у всех разработчиков на слуху кэширующие прокси, поэтому их так часто можно встретить в приложениях. Основные недостатки: падение скорости работы (часто используется __call, __get, __callStatic, call_user_func_array), а также ломается тайпхинтинг, потому что вместо реального объекта приходит прокси-объект. Если попытаться завернуть кэширующий прокси поверх логирующего, а тот, в свою очередь, поверх основного класса, то скорость упадет на порядок.

Но есть и плюс: в случае 100 классов мы можем написать один кэширующий прокси на все классы. Но! Ценой отказа от тайпхинтинга по 100 интерфейсам, что категорически неприемлемо при разработке современных приложений.

События и паттерн Наблюдатель


Трудно не вспомнить такой замечательный паттерн, как Наблюдатель. Наблюдатель (Observer) — поведенческий шаблон проектирования. Также известен как «подчинённые» (Dependents), «издатель-подписчик» (Publisher-Subscriber).

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

Надо отметить, что это максимально гибкий шаблон, так как на его основе вы можете спроектировать систему, которая будет расширяться очень легко и будет понятной. Если бы не было АОП, это был бы самый лучший способ расширять логику методов, не изменяя при этом исходного кода. Не удивительно, что многие фреймворки используют события для расширения функциональности, например ZF2, Symfony2. На сайте Symfony2 есть отличная статья о том, как можно расширить логику метода, не используя наследование.

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

class Foo
{
    // ...

    public function __call($method, $arguments)
    {
        // create an event named 'foo.method_is_not_found'
        $event = new HandleUndefinedMethodEvent($this, $method, $arguments);
        $this->dispatcher->dispatch('foo.method_is_not_found', $event);

        // no listener was able to process the event? The method does not exist
        if (!$event->isProcessed()) {
            throw new \Exception(sprintf('Call to undefined method %s::%s.', get_class($this), $method));
        }

        // return the listener returned value
        return $event->getReturnValue();
    }
}


Сигналы и слоты


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

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

use lithium\analysis\Logger;
use lithium\data\Connections;

// Set up the logger configuration to use the file adapter.
Logger::config(array(
    'default' => array('adapter' => 'File')
));

// Filter the database adapter returned from the Connections object.
Connections::get('default')->applyFilter('_execute', function($self, $params, $chain) {
    // Hand the SQL in the params headed to _execute() to the logger:
    Logger::debug(date("D M j G:i:s") . " " . $params['sql']);

    // Always make sure to keep the filter chain going.
    return $chain->next($self, $params, $chain);
});


Настоятельно рекомендую ознакомиться с системой фильтров, потому что реализация фильтров в Lithium максимально приближает разработку к аспектно-ориентированному программированию и может стать для вас тем толчком, который позволит окунуться в мир АОП окончательно.

Аспектно-ориентированное программирование


Итак, мы подошли к самому интересному — к использованию аспектно-ориентированного программирования для борьбы с дублированием сквозного кода. На хабре уже были статьи по АОП, в том числе и для PHP, поэтому я не буду повторять этот материал и давать определения тех терминов и те приемы, которые использует АОП. Если вы не знакомы с терминами и понятиями АОП, то перед дальнейшим чтением можно ознакомиться со статьей про АОП на Википедии.

Итак, фильтры в Lithium позволяют подключать дополнительные обработчики почти куда угодно, что дает возможность вынести код кэширования, логирования, проверки прав доступа в отдельные замыкания. Казалось бы, вот она, серебряная пуля. Но все не так уж гладко. Во-первых, для использования фильтров нам нужно подключить весь фреймворк, так как отдельной библиотеки для этого нет, а жаль. Во-вторых, фильтры-замыкания (в терминах АОП — советы) разбросаны повсюду и за ними очень сложно следить. В-третьих, код должен быть написан определенным образом и реализовывать специальные интерфейсы, чтобы можно было использовать фильтры. Эти три минуса значительно ограничивают возможность использовать фильтры в качестве АОП в других приложениях и фреймворках.

Вот здесь у меня и появилась идея — написать библиотеку, которая дала бы возможность использовать АОП в любом приложении на PHP. Дальше была битва с PHP, изучение техник ускорения кода, борьба с багами опкод-ускоритиелей и много-много интересного. В результате родилась библиотека Go! AOP PHP, которая может внедриться в существующее приложение, перехватить доступные методы во всех классах и вынести из них сквозную функциональность на несколько тысяч строк кода в пару десятков строк советов.

Библиотека Go! AOP PHP


Основные отличия от всех существующих аналогов — это библиотека, не требующая никаких расширений PHP, не призывающая на помощь черную магию runkit-a и php-aop. Она не использует eval-ов, не завязана на DI-контейнер, не нуждается в отдельном компиляторе аспектов в конечный код. Аспекты представляют собой обычные классы, органично использующие все возможности ООП. Формируемый библиотекой код с вплетенными аспектами — очень чистый, его можно легко отлаживать с помощью XDebug-а, причем как сами классы, так и аспекты.

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

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

Рефакторинг сквозного кода с использованием АОП


Чтобы разжечь интерес к АОП побольше, я решил выбрать интересную тему, о которой можно найти мало информации — рефакторинг кода к аспектам. Дальше будет два примера того, как можно сделать свой код чище и понятнее с использованием аспектов.

Выносим логирование из кода

Итак, представим, что у нас есть логирование всех выполняемых публичных методов в 20 классах, находящихся в неймспейсе Acme. Выглядит это как-то так:

namespace Acme;

class Controller
{

    public function updateData($arg1, $arg2)
    {
        $this->logger->info("Executing method " . __METHOD__, func_get_args());
        // ...
    }    
}


Давайте возьмем и отрефакторим этот код с использованием аспектов! Легко заметить, что логирование выполняется перед кодом самого метода, поэтому сразу выбираем тип совета — Before. Дальше нам нужно определить точку внедрения — выполнение всех публичных методов внутри неймспейса Acme. Это правило задается выражением execution(public Acme\*->*()). Итак, пишем LoggingAspect:

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Before;

/**
 * Logging aspect
 */
class LoggingAspect implements Aspect
{
    /** @var null|LoggerInterface */
    protected $logger = null;
    
    /** ... */
    public function __construct($logger) 
    {
        $this->logger = $logger;
    }
    
    /**
     * Method that should be called before real method
     *
     * @param MethodInvocation $invocation Invocation
     * @Before("execution(public Acme\*->*())")
     */
    public function beforeMethodExecution(MethodInvocation $invocation)
    {
        $obj    = $invocation->getThis();
        $class  = is_object($obj) ? get_class($obj) : $obj;
        $type   = $invocation->getMethod()->isStatic() ? '::' : '->';
        $name   = $invocation->getMethod()->getName();
        $method = $class . $type . $name;
        
        $this->logger->info("Executing method " . $method, $invocation->getArguments());
    }
}    


Ничего сложного, обычный класс с обычным на вид методом. Однако это — аспект, определяющий совет beforeMethodExecution, который будет вызван перед вызовом нужных нам методов. Как вы уже заметили, Go! использует аннотации для хранения метаданных, что давно уже стало обычной практикой, так как это наглядно и удобно. Теперь мы можем зарегистрировать наш аспект в ядре Go! и выкинуть из кучи наших классов все логирование! Убрав ненужную зависимость от логера, мы сделали наш код классов чище, он стал больше соблюдать принцип единой ответственности, потому что мы вынесли из него то, чем он не должен заниматься.

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

Прозрачное кэширование

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

    /** ... */
    public function cachedMethod()
    {
        $key = __METHOD__;
        $result = $this->cache->get($key, $success);
        if (!$success) {
            $result = // ...
            $this->cache->set($key, $result);
        }
        return $result;
    }


Несомненно, все узнают этот шаблонный код, так как таких мест всегда достаточно. Если у нас большая система, то таких методов может быть очень много, вот бы сделать так, чтобы они сами кэшировались. А что, идея! Давайте помечать аннотацией те методы, которые должны кэшироваться, а в поинткате зададим условие — все методы, помеченные определенной аннотацией. Так как кэширование «оборачивает» код метода, то и нам нужен подходящий тип совета — Around, самый могущественный. Этот тип совета сам принимает решение о необходимости выполнения исходного кода метода. А дальше все просто:

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Around;

class CachingAspect implements Aspect
{

    /**
     * Cache logic
     *
     * @param MethodInvocation $invocation Invocation
     * @Around("@annotation(Annotation\Cacheable)")
     */
    public function aroundCacheable(MethodInvocation $invocation)
    {
        static $memoryCache = array();

        $obj   = $invocation->getThis();
        $class = is_object($obj) ? get_class($obj) : $obj;
        $key   = $class . ':' . $invocation->getMethod()->name;
        if (!isset($memoryCache[$key])) {
            $memoryCache[$key] = $invocation->proceed();
        }
        return $memoryCache[$key];
    }
}


В этом совете самое интересное — вызов оригинального метода, который осуществляется с помощью вызова proceed() у объекта MethodInvocation, содержащего информацию о текущем методе. Легко заметить, что если у нас есть данные в кэше, то мы не производим вызов оригинального метода. При этом, ваш код не изменяется никак!
Имея такой аспект, мы можем перед любым методом поставить аннотацию Annotation\Cacheable и этот метод будет кэшироваться благодаря АОП автоматически. Проходимся по всем методам и вырезаем логику кэширования, заменяя ее на аннотацию. Теперь шаблонный код метода с использованием кэширования выглядит просто и изящно:

    /** 
     * @Cacheable
     */
    public function cachedMethod()
    {
        $result = // ...
        return $result;
    }


Этот пример можно также найти внутри папки demos библиотеки Go! AOP PHP, а также посмотреть на коммит, реализующий вышесказанное в действии.

Заключение


Аспектно-ориентированное программирование — довольно новая парадигма для PHP, но у нее большое будущее. Развитие метапрограммирования, написание Enterprise-фреймворков в PHP — все это идет по следам Java, а АОП в Java живет уже очень давно, так что нужно готовиться к АОП уже сейчас.

Go! AOP PHP — одна из немногих библиотек, которая работает с АОП и в некоторых вопросах она выгодно отличается от аналогов — возможность перехватывать статические методы, методы в финальных классах, обращения к свойствам объектов, возможность отладки исходного кода и кода аспектов. Go! использует массу техник для обеспечения высокого быстродействия: компиляция вместо интерпретации, отсутствие медленных техник, оптимизированный код выполнения, возможность использовать опкод-кэшер — все это дает свой вклад в общее дело. Одним из удивительных открытий было то, что Go! в некоторых аналогичных условиях может работать быстрее C-экстеншена PHP-AOP. Да-да, это правда, которая имеет простое объяснение — экстеншен вмешивается в работу всех методов в PHP в рантайме и делает небольшие проверки на соответствие поинткату, чем больше таких проверок, тем медленнее вызов каждого метода, тогда как Go! делает это один раз при компиляции кода класса, не влияя на скорость работы методов в рантайме.

Если есть вопросы и пожелания по библиотеке — я с радостью обсужу их с вами. Надеюсь, моя первая статья на хабре была вам полезной.

Ссылки

  1. Исходный код https://github.com/lisachenko/go-aop-php
  2. Презентация SymfonyCampUA-2012 http://www.slideshare.net/lisachenko/php-go-aop
  3. Видео SymfonyCampUA-2012 http://www.youtube.com/watch?v=ZXbREKT5GWE
  4. Пример перехвата всех методов в ZF2 (после клонирования устанавливаем зависимости через composer) https://github.com/lisachenko/zf2-aspect
  5. Интересная статья по теме: Аспекты, фильтры и сигналы — о, боже! (en)
Поделиться публикацией

Комментарии 48

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

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

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

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

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

            И так вопрос: почему не сделать предельно простой класс в одним-двумя методами, в которых пустое либо в одну строчку тело, к которому добавляется логирование, потом добавить примитивный класс логирования и показать, как это работает, на пальцах, чтобы не было ни одной строчки, которую можно выбросить?
              0
              Можно склонировать библиотеку к себе, поставить зависимости и натравить веб-сервер на папку demos. В ней как раз лежит пример работы простого класса с вплетенными аспектами и кэшированием. Как говорится, лучше один раз увидеть вживую, чем сто раз услышать, а еще можно залезть туда с XDebug-ом.
              0
              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
                +3
                Ожидал этого вопроса. Вопрос на самом деле очень сложный в технической реализации и объяснении.
                Но попробую объяснить. Когда перехватываются динамические методы — там все просто, мы всегда знаем объект благодаря $this. Поэтому нам не нужно указывать scope.
                Со статическими методами все намного сложнее — нужно переопределить статический метод, после этого вызвать оригинальный метод через Reflection. А в PHP сейчас есть сложность с тем, чтобы указать scope при вызове статического метода через рефлексию. Вот поэтому и ломается LSB. Только в 5.4 можно создать замыкание в дочернем классе, которое форвардит запрос к родительскому методу и сменить ему scope на корректный через метод bindTo()
                +1
                А в чем отличие вашего детища от, к примеру, AspectPHP?

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

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

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

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

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

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

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

                              И последний довод — аспекты используются для тех методов, которые вызываются редко, но они очень важны, оверхед в пару десятков мкс на них даже не заметен. И понятно, что указывать все методы всех классов и логировать все, получая замедление в 5 раз — выстрел в себе в ногу.
                              Все нужно использовать с умом и правильно, как и любой другой инструмент.
                                0
                                nikita2206 меня опередил с изящным простым ответом )
                          +2
                          Напоминает попытку реализовать декораторы.
                          Например как в 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.
                            0
                            к сожалению декораторы создают ещё один объект(класс или функцию) а то и не один :(
                            с другой стороны не такой уж оверхед. один раз делается же.
                            но в php нет таких удобных декораторов :)
                              0
                              Ну то всего один небольшой кусок кода с замыканием, а то разбор аннотаций в комментариях и генерация чего-то где-то непрозрачно для разработчика.
                            0
                            А вы смотрели ли на JMSAopBundle?
                              0
                              Ну это Symfony-coupled код, а Go не зависит от платформы
                                0
                                Да, помимо завязки на контейнер симфони и ее архитектуру, есть еще несколько отличий. Если попробовать перехватить статический метод — JMSAopBundle отдыхает, перехватить метод в финальном классе — опять отдыхает. Пишем явно (new Class())->methodName() — опять отдыхает. У меня будет еще Introduction и перехват финальных методов, чем JMSAopBundle еще не скоро обзаведется.
                                Однако создание декораторов и подмена их в классе весьма неплохое решение и для работы в Symfony2 с АОП я его рекомендую больше, чем Go! Но вот не все он умеет, далеко не все…
                                0
                                Утром на Хабре — вечером в PHP Weekly. Только что пришла рассылка. :)
                                И поздравляю с топ1 по Most Starred Today в PHP на гитхабе.
                                  0
                                  Ого, неожиданно )) Спасибо всем за отзывы и stars. Буду и дальше воевать за АОП )
                                  0
                                  Если попытаться завернуть кэширующий прокси поверх логирующего, а тот, в свою очередь, поверх основного класса, то скорость упадет на порядок.
                                  Странно, потому как на моей практике профилирования кешируюших/логируюших прокси классов, время вызова этих __call & __get в среднем на порядки меньше вызова самих кеширующих/логирующих методов. Ну т.е. издержки от их использования составляют 0.1-0.5%.
                                    0
                                    Все верно подмечено, что вызов даже магического метода будет быстрее если он отдаст данные из кэша. Это же справедливо и для аспектов, только работает еще быстрее, потому что там нет вообще этих магических методов, только прямые вызовы.
                                    На самом деле гораздо больше неприятностей доставляет тот факт, что использование прокси-классов не дает возможности использовать в методах указание ожидаемых интерфейсов и классов.
                                      0
                                      использование прокси-классов не дает возможности использовать в методах указание ожидаемых интерфейсов и классов
                                      А разве phpdoc annotations не решают эту проблему?
                                        0
                                        Я про тайпхинтинг по интерфейсу при прокидывании этого объекта в качестве зависимости в другой класс.
                                    0
                                    Напоминает триггеры в СУБД
                                      0
                                      статья интересная, спасибо. но мне интересно, нельзя ли избавиться вот от этого

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


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

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

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

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

                                              Самое читаемое