- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Встречайте: Annotator [4].

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

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

Info

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

Before

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

After

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

Around

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

Установка

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

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

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

Класс предоставляет 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 [9].

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

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

Автор: BVadim


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/razrabotka/12697

Ссылки в тексте:

[1] АОП: http://http://ru.wikipedia.org/wiki/%D0%90%D1%81%D0%BF%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5

[2] волшебных методов: http://www.php.net/manual/ru/language.oop5.overloading.php#object.call

[3] runkit: http://ru2.php.net/manual/ru/book.runkit.php

[4] Annotator: https://github.com/baryshev/Annotator

[5] официальном репозитории: https://github.com/zenovich/runkit

[6] другой разработчик: https://github.com/nkresge

[7] https://github.com/nkresge/runkit: https://github.com/nkresge/runkit

[8] Annotator.php: https://github.com/baryshev/Annotator/blob/master/Annotator.php

[9] Dependency injection: http://ru.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8

[10] автоматическую загрузку классов php: http://php.net/manual/ru/language.oop5.autoload.php

[11] phpDaemon: http://phpdaemon.net