Статьи

Ваши собственные пользовательские аннотации — больше, чем просто комментарии!

В этой статье мы рассмотрим, как мы можем создавать и использовать наши собственные пользовательские аннотации в приложении Symfony 3. Вы знаете аннотации правильно? Это метаданные / конфигурация docblock, которые мы видим выше классов, методов и свойств. Скорее всего, вы видели, как они используются для объявления маршрутов контроллера ( @Route() ) и сопоставления Doctrine ORM ( @ORM() ) или даже для управления доступом к различным классам и методам в пакетах, таких как Rauth . Но вы когда-нибудь задумывались, как вы можете использовать их самостоятельно? Как вы можете определить свою собственную аннотацию и затем использовать ее для чтения информации о классе или методе без фактической загрузки?

Фрагмент кода с пользовательскими аннотациями

Прежде чем мы сделаем это, небольшой отказ от ответственности. Аннотации не имеют ничего общего с Symfony. Это концепция, разработанная в рамках проекта Doctrine для решения проблемы отображения информации ORM в методы класса.

В этой статье мы собираемся создать небольшой многоразовый пакет под названием WorkerBundle . Можно использовать многократно, но все же только для демонстрационных целей, поэтому не совсем совместимо с пакетами. Мы собираемся разработать небольшую концепцию, которая позволяет определять различные типы рабочих, которые «работают» на разных скоростях и которые затем могут использоваться любым пользователем в приложении. Фактические рабочие операции выходят за рамки этого поста, поскольку мы сосредоточены на настройке системы для управления ими (и обнаружения их с помощью аннотаций).

Чтобы увидеть, куда мы идем, вы можете проверить этот репозиторий и следовать инструкциям, приведенным в нем, для настройки пакета в локальном приложении Symfony.

Рабочие

Рабочие будут реализовывать интерфейс, который требует один метод: ::work() . Внутри нашего нового WorkerBundle давайте создадим каталог Workers/ чтобы сохранить порядок и добавить туда интерфейс:

 <?php namespace WorkerBundle\Workers; interface WorkerInterface { /** * Does the work * * @return NULL */ public function work(); } 

Аннотация

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

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

Каждый экземпляр WorkerInterface будет иметь следующую аннотацию в своем блоке документации:

 /** * @Worker( * name = "The unique Worker name", * speed = 10 * ) */ 

Мы собираемся сделать вещи простыми и иметь только два свойства: уникальное имя (строка) и рабочая скорость (целое число). Чтобы эта аннотация распознавалась библиотекой аннотаций Doctrine, нам нужно создать соответствующий класс, который, что не случайно, имеет свои собственные аннотации.

Мы поместим этот класс в папку Annotation нашего пространства имен пакета и назовем его просто Worker :

 <?php namespace WorkerBundle\Annotation; use Doctrine\Common\Annotations\Annotation; /** * @Annotation * @Target("CLASS") */ class Worker { /** * @Required * * @var string */ public $name; /** * @Required * * @var int */ public $speed; /** * @return string */ public function getName() { return $this->name; } /** * @return int */ public function getSpeed() { return $this->speed; } } 

Как видите, у нас есть два свойства с несколькими простыми геттерами. Что еще более важно, у нас есть две аннотации вверху: @Annotation (которая сообщает Doctrine, что этот класс представляет аннотацию) и @Target("CLASS") которая говорит, что она должна использоваться над целым классом, а не метод или свойство. И все, классы WorkerInterface теперь могут использовать эту аннотацию, убедившись, что соответствующий класс также импортируется с use оператора use в верхней части файла, например:

 use WorkerBundle\Annotation\Worker; 

Менеджер

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

 <?php namespace WorkerBundle\Workers; class WorkerManager { /** * @var WorkerDiscovery */ private $discovery; public function __construct(WorkerDiscovery $discovery) { $this->discovery = $discovery; } /** * Returns a list of available workers. * * @return array */ public function getWorkers() { return $this->discovery->getWorkers(); } /** * Returns one worker by name * * @param $name * @return array * * @throws \Exception */ public function getWorker($name) { $workers = $this->discovery->getWorkers(); if (isset($workers[$name])) { return $workers[$name]; } throw new \Exception('Worker not found.'); } /** * Creates a worker * * @param $name * @return WorkerInterface * * @throws \Exception */ public function create($name) { $workers = $this->discovery->getWorkers(); if (array_key_exists($name, $workers)) { $class = $workers[$name]['class']; if (!class_exists($class)) { throw new \Exception('Worker class does not exist.'); } return new $class(); } throw new \Exception('Worker does not exist.'); } } 

Класс WorkerManager выполняет две функции: извлекает определения работников ( ::getWorker() и ::getWorkers() ) и создает их экземпляры ( ::create() ). В качестве аргумента конструктора он извлекает объект WorkerDiscovery который мы напишем через минуту. Остальное довольно легко понять. Метод ::create() ожидает, что каждое определение работника является массивом, который имеет ключ class который будет использоваться для создания экземпляра. Здесь все просто, но, конечно, в сценарии реального мира следующим шагом будет представление этого определения с использованием отдельного класса и делегирование фактической реализации фабрике.

Открытие

Важнейшая часть нашей демонстрации аннотаций на самом деле является частью процесса Discovery. Почему? Потому что мы используем аннотацию Worker чтобы определить, должен ли соответствующий класс считаться Worker. При этом мы используем метаданные до фактического создания объекта. Итак, давайте посмотрим наш класс WorkerDiscovery :

 <?php namespace WorkerBundle\Workers; use WorkerBundle\Annotation\Worker; use Doctrine\Common\Annotations\Reader; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\HttpKernel\Config\FileLocator; class WorkerDiscovery { /** * @var string */ private $namespace; /** * @var string */ private $directory; /** * @var Reader */ private $annotationReader; /** * The Kernel root directory * @var string */ private $rootDir; /** * @var array */ private $workers = []; /** * WorkerDiscovery constructor. * * @param $namespace * The namespace of the workers * @param $directory * The directory of the workers * @param $rootDir * @param Reader $annotationReader */ public function __construct($namespace, $directory, $rootDir, Reader $annotationReader) { $this->namespace = $namespace; $this->annotationReader = $annotationReader; $this->directory = $directory; $this->rootDir = $rootDir; } /** * Returns all the workers */ public function getWorkers() { if (!$this->workers) { $this->discoverWorkers(); } return $this->workers; } /** * Discovers workers */ private function discoverWorkers() { $path = $this->rootDir . '/../src/' . $this->directory; $finder = new Finder(); $finder->files()->in($path); /** @var SplFileInfo $file */ foreach ($finder as $file) { $class = $this->namespace . '\\' . $file->getBasename('.php'); $annotation = $this->annotationReader->getClassAnnotation(new \ReflectionClass($class), 'WorkerBundle\Annotation\Worker'); if (!$annotation) { continue; } /** @var Worker $annotation */ $this->workers[$annotation->getName()] = [ 'class' => $class, 'annotation' => $annotation, ]; } } } 

Первые два аргумента конструктора являются частью настройки нашего пакета. Они оба являются строкой, которая сообщает нашему открытию, в какую папку он должен смотреть и какое пространство имен использовать для загрузки найденных классов. Эти два исходят из определения сервисного контейнера, которое мы увидим в конце. Аргумент rootDir — это простой путь к каталогу Kernel, а экземпляр Reader — это класс Doctrine, который мы используем для чтения аннотаций. Вся магия происходит внутри ::discoverWorkers() .

Во-первых, мы устанавливаем путь, где искать работников. Затем мы используем компонент Symfony Finder для поиска всех файлов в этой папке. Итерируя по всем найденным файлам, мы устанавливаем имена классов всех найденных классов на основе имени файла и создаем экземпляры ReflectionClass которые затем передаем читателю аннотаций ::getClassAnnotation() . Второй аргумент этого метода представляет пространство имен используемого класса аннотаций. И для всех найденных нами аннотаций мы создаем массив определений, содержащих имя класса, которое можно использовать для создания экземпляра, и весь объект аннотации, если это кому-то нужно. Это оно! Наша служба обнаружения теперь может искать работников, не создавая ни одного из них.

Для получения дополнительной информации проверьте класс AnnotationReader наличие возможностей извлечения данных аннотации. Существуют также методы чтения аннотаций методов и свойств, которые вы можете использовать, передавая соответствующие объекты отражения.

Связать это

Теперь, когда у нас есть наши основные компоненты, пришло время соединить все. Во-первых, нам нужны определения наших сервисов, поэтому внутри папки Resource/config нашего пакета мы можем получить файл services.yml :

 services: worker_manager: class: WorkerBundle\Workers\WorkerManager arguments: ["@worker_discovery"] worker_discovery: class: WorkerBundle\Workers\WorkerDiscovery arguments: ["%worker_namespace%", "%worker_directory%", "%kernel.root_dir%", "@annotation_reader"] 

Здесь ничего особенного не происходит. WorkerManager получает WorkerDiscovery в качестве зависимости, в то время как последний получает некоторые параметры и сервис чтения аннотаций Doctrine.

Но для того, чтобы наши сервисные определения централизованно выбирались контейнером, нам нужно написать небольшой класс расширения. Поэтому внутри папки DependencyInjection нашего пакета создайте класс с именем WorkerExtension . И местоположение, и имя важны для Symfony, чтобы подобрать его автоматически.

 <?php namespace WorkerBundle\DependencyInjection; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\ContainerBuilder; class WorkerExtension extends Extension { public function load(array $configs, ContainerBuilder $container) { $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); } } 

Здесь мы просто используем метод ::load() для включения наших сервисов в копию контейнера, которая объединяется с основным контейнером.

Последнее, что нам нужно сделать, это зарегистрировать наш пакет. Внутри нашего AppKernel :

 public function registerBundles() { return array( // ... new WorkerBundle\WorkerBundle(), // ... ); } 

Вот и все.

Давай работать!

Теперь мы готовы к работе. Давайте настроим, где наши сотрудники будут находиться в центральном файле parameters.yml :

 worker_namespace: AppBundle\Workers worker_directory: AppBundle/Workers 

Эти параметры передаются из контейнера в класс WorkerDiscovery как мы видели выше.

Расположение зависит от вас, но пока давайте поместим наших сотрудников в основной пакет AppBundle нашего приложения. Наш первый рабочий может выглядеть так:

 <?php namespace AppBundle\Workers; use WorkerBundle\Annotation\Worker; use WorkerBundle\Workers\WorkerInterface; /** * Class SlowWorker * * @Worker( * name = "Slow Worker", * speed = 5 * ) */ class SlowWorker implements WorkerInterface { /** * {@inheritdoc} */ public function work() { return 'I work really slowly'; } } 

Наша аннотация наверху, оператор use готов, поэтому ничто не мешает бизнес-логике найти его и создать его экземпляр. Давайте предположим, внутри метода Controller:

 $manager = $this->get('worker_manager'); $worker = $manager->create('Slow Worker'); $worker->work(); 

или

 $workers = $manager->getWorkers(); $workers = array_filter($workers, function($definition) { return $definition['annotation']->getSpeed() >= 5; }); foreach($workers as $definition) { /** @var WorkerInterface $worker */ $worker = $manager->create($definition['annotation']->getName()); $worker->work(); } 

Пффф … наш SlowWorker едва SlowWorker сегодня работать!

Вывод

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

Используете ли вы собственные аннотации в ваших собственных проектах? Если да, то как вы реализуете их по пути Раут , или как мы это сделали здесь? Возможно, третий подход? Дайте нам знать!


Хотите узнать больше о Symfony, Doctrine, аннотациях и всевозможных вещах PHP? Присоединяйтесь к нам для проведения трехдневных практических семинаров в WebSummerCamp — единственной конференции, которая посвящена исключительно практическим вопросам, а также позаботится обо всех, кого вы хотели бы взять с собой!