Использование аннотаций в PHP 5.4 для АОП и не только

в 10:02, , рубрики: aop, php, аннотации, Программирование, разработка, метки: , ,

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

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

Технологии реализации AOP в PHP

Волшебные методы

Самое простое решение — использование «волшебных методов» __call и __callStatic. Эти методы вызываются (если они определены в классе) при обращении к несуществующему методу класса. В качестве аргументов они получают имя несуществующего метода и переданные ему параметры.
В данном случае, приложение строится таким образом, что реальные методы имеют имя отличное от имени указанном в вызывающих их конструкциях. Сквозной функционал реализуется в «волшебных методах», которые, при необходимости, передают управление реальным методам классов.

Плюсы:
  • Легко начать использовать;
  • Реализация не требует дополнительных модулей (нативный PHP).

Минусы:
  • Не удобно использовать при большом количестве сквозного функционала;
  • Т.к. имена методов в определении и в вызовах различаются, создаются трудности при использовании автодополнения кода в IDE.

Предварительный разбор кода

Этот способ подразумевает наличие посредника, позволяющего использовать «синтаксический сахар». Необходимый функционал описывается вспомогательным синтаксисом (xml/json конфигурация, дополнительные php-классы или аннотации в коде), который разбирается посредником. На основе разбора генерируется результирующий код, который содержит вставки сквозного функционала в необходимые места.

Плюсы:
  • Работает быстро, т.к. на выходе это обычный PHP-код, просто сгенерированный за Вас автоматически.

Минусы:
  • Сложно внедрить в большой проект;
  • Требуется разбор кода после каждого изменения, для внесения корректировок в результирующий код.

Замена кода приложения во время выполнения

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

Встречайте: Annotator.

Возможности Annotator

Библиотека реализует 4 типа обработчиков:

Info

Обработчик типа Info получает информацию о методе во время обработки класса. Это позволяет «зарегистрировать» метод для дальнейшего использования в приложении. Например с его помощью можно назначить метод на обработку определённого URL и таким образом реализовать роутинг в приложении.

Before

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

After

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

Around

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

Установка

Для работы Annotator требуется PHP 5.4 и модуль runkit.
Последняя версия в официальном репозитории runkit, ещё не адаптирована для PHP 5.4, но, к счастью, его уже адаптировал другой разработчик.

  1. Качаем расширение отсюа: https://github.com/nkresge/runkit;
  2. Собираем и устанавливаем его:
    phpize && ./configure && make && sudo make install
    
  3. Если всё прошло успешно, подключаем модуль runkit.so в conf.d или php.ini;
  4. Качаем класс Annotator.php и подключаем его к проекту.

Примеры использования

Класс предоставляет 4 заранее зарезервированных аннотации для всех типов обработчиков, при этом есть возможность зарегистрировать свои аннотации любого из 4 типов.

Info

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	public static function infoStatic($point, $options) {
		var_dump($point);
		var_dump($options);
	}
}

class Test {
	/**
	 * @info Advice::infoStatic hello world
	 */
	public static function testInfoStatic() {
		return 'info';
	}
}

Annotator::compile('Test');

Уже во время вызова Annotator::compile('Test'); будет вызван обработчик infoStatic класса Advice, который получит инфрмацию о методе testInfoStatic класса Test и параметры обработчика в качестве массива array('hello', 'world').

Before

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

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	public function before($point, $params, $options) {
		$params['string'] = 'bar';
	}
}

class Test {
	/**
	 * @registered_before
	 */
	public function testBefore($string) {
		return $string;
	}
}

$advice = new Advice();

Annotator::register('registered_before', array($advice, 'before'), Annotator::BEFORE);

Annotator::compile('Test');

$test = new Test();

echo $test->testBefore('foo');

Методом Annotator::register мы создали аннотацию @registered_before, ассоциированную с методом before объекта $advice. При вызове testBefore управление будет передано в обработчик, который заменит параметр $string и вместо ожидаемого «foo» в результате работы скрипта будет выведено «bar».

After

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	public function power($point, $params, $options, $result) {
		return pow($result, $options[0]);
	}
}

class Test {
	/**
	 * @power 4
	 */
	public function testAfter($number) {
		return $number + 1;
	}
}

$advice = new Advice();

Annotator::register('power', array($advice, 'power'), Annotator::AFTER);

Annotator::compile('Test');

$test = new Test();

echo $test->testAfter(1);

В этом примере результат работы метода testAfter будет возведён в степень 4. Скрипт выведет значение 16.

Around

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	private static $cache = array();

	public function cache($point, $params, $options, $proceed) {
		if (isset(self::$cache[$options[0]])) {
			// Если значение ключа содержится в кеше - возвращаем его
			return self::$cache[$options[0]];
		} else {
			// Если значения в кеше нет - выполняем функцию
			$result = $proceed();
			// Перед возвращением значения не забываем положить его в кеш
			self::$cache[$options[0]] = $result;
			return $result;
		}
	}
}

class Test {
	/**
	 * @cache around_cache_key
	 */
	public function testAround($string) {
		return $string;
	}
}

$advice = new Advice();

Annotator::register('cache', array($advice, 'cache'), Annotator::AROUND);

Annotator::compile('Test');

$test = new Test();

echo $test->testAround('foo') . PHP_EOL;
echo $test->testAround('foo') . PHP_EOL;
echo $test->testAround('foo') . PHP_EOL;

Этот пример представляет реализацию простого механизма кеширования. Метод testAround вызывается 3 раза подряд, но будет выполнен только 1 раз. Остальные 2 раза значение будет взято из статической переменной $cache класса Advice, куда оно будет сохранено после первого вызова.

Использование нескольких обработчиков

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

<?php

require_once __DIR__ . '/Annotator.php';

class Advice {
	public static function before1($point, $params, $options) {
		$params['string'] .= 'before1';
	}

	public static function before2($point, $params, $options) {
		$params['string'] .= ' before1';
	}

	public static function after1($point, $params, $options, $result) {
		return $result . ' after1';
	}

	public static function after2($point, $params, $options, $result) {
		return $result .= ' after2';
	}

	public static function around1($point, $params, $options, $proceed) {
		return $proceed() . ' around1';
	}

	public static function around2($point, $params, $options, $proceed) {
		return $proceed() . ' around2';
	}
}

class Test {
	/**
	 * @before Advice::before1
	 * @after Advice::after1
	 * @around Advice::around1
	 * @before Advice::before2
	 * @after Advice::after2
	 * @around Advice::around2
	 */
	public function testMulti($string) {
		return $string;
	}
}

Annotator::compile('Test');

$test = new Test();

echo $test->testMulti('');

В результате работы этого примера будет выведена строка «before1 before1 around1 around2 after1 after2».

Обработчики

Каждый тип обработчика имеет свой набор параметров:

Info
  • $point — метод на который навешан обработчик;
  • $options — массив параметров, указанных в аннотации.
Before
  • $point — метод на который навешан обработчик;
  • $params — массив параметров, переданных в метод при вызове;
  • $options — массив параметров, указанных в аннотации.
After
  • $point — метод на который навешан обработчик;
  • $params — массив параметров, переданных в метод при вызове;
  • $options — массив параметров, указанных в аннотации;
  • $result — переменная содержащая результат метода, на который навешан обработчик.
Around
  • $point — метод на который навешан обработчик;
  • $params — массив параметров, переданных в метод при вызове;
  • $options — массив параметров, указанных в аннотации;
  • $proceed — функция передающая управление назад в метод, на который навешан обработчик.

Послесловие

На основе Annotator можно очень просто и быстро реализовать удобные механизмы, позволяющие сильно сократить код и улучшить структуру приложения, упрощая его поддержку. Например, кроме реализации АОП с его помощью можно довольно легко реализовать паттерн Dependency injection.

Необходимо помнить, что замена методов во время выполения скрипта требует некоторого времени. Не стоит вызывать Annotator::compile для классов, которые не будут использоваться в данном запросе. Проще всего это реализуется чрез автоматическую загрузку классов php.

Для долгоживущих приложений (простые демоны или приложения на основе phpDaemon) вносимый overhead практически не будет влиять на производительность, т.к. классы будут загружены и обработаны всего 1 раз.

Автор: BVadim

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js