В прошлом году я работал над обновлением нашей клиентской панели управления. Панель управления клиентом – это веб-приложение, которое позволяет нашим пользователям получать представление об их различных услугах и кампаниях вместе с нами. Целью проекта было создание совершенно новой панели управления с использованием компонентов Zend Framework 3, а также обновление до последней версии PHP и использование компонентов Material Design для интерфейса.
В нашей предыдущей версии панели управления было очень мало инфраструктуры для ведения журнала. Использование ведения журнала было непоследовательным, и использовались различные библиотеки ведения журнала, в зависимости от того, какой разработчик работал над этим разделом сайта.
После некоторых исследований мы сузили круг до трех потенциальных вариантов:
Используйте класс
Регистраторсо статическими методами для каждого из потенциальных уровней журнала. Мы использовали этот подход в нескольких небольших приложениях, но посчитали, что он не подойдет для такого крупного проекта, как этот. Хотя этот подход прост в использовании и создании, мы не хотели загрязнять нашу кодовую базу сотнями статических вызовов, над которыми нельзя издеваться.Внедрите экземпляр регистратора, реализующего
LoggerInterface, в любой класс, в котором мы хотели регистрировать информацию. В то время казалось, что это добавляет ненужных накладных расходов на строительство каждого объекта.Используйте Zend event-менеджер . Отправляйте события журнала (
Событие журнала) и прикрепляйтеПрослушиватель журналак менеджеру событий. ЗатемПрослушиватель журналарегистрирует информацию о событии, используя экземплярИнтерфейса регистратора.
Третий подход казался простым в реализации и понимании, поэтому мы решили использовать его в нашем приложении. В этом посте будет рассказано о том, как мы это реализовали, а также о рассмотрении этого подхода теперь, когда мы потратили более года на его использование в производстве.
Создание события журнала
Это наше Событие журнала ниже. Событие будет отправлено классом, желающим зарегистрировать информацию, а затем поймано слушателем, подключенным к нашему менеджеру событий. Мы определяем общедоступные константы, которые сопоставляются с уровнями журнала из PSR-3 . Мы могли бы использовать уровни PSR-3 напрямую, однако мы хотели обеспечить уровень абстракции между нашим LogEvent и любой библиотекой ведения журнала, которую мы использовали. Этот шаг необязателен, так как большинство людей будут использовать регистраторы, совместимые с PSR-3.
declare(strict_types=1);
namespace Application\Event;
use Psr\Log\LogLevel;
use Zend\EventManager\Event;
class LogEvent extends Event
{
public const DEBUG = LogLevel::DEBUG;
public const INFO = LogLevel::INFO;
public const NOTICE = LogLevel::NOTICE;
public const WARNING = LogLevel::WARNING;
public const ERROR = LogLevel::ERROR;
public const CRITICAL = LogLevel::CRITICAL;
public const ALERT = LogLevel::ALERT;
public const EMERGENCY = LogLevel::EMERGENCY;
}
Создание и регистрация слушателя
Теперь, когда у нас есть наше мероприятие, нам нужно создать слушателя и зарегистрировать его у менеджера мероприятий. Прослушиватели подключаются к менеджеру событий, который затем отвечает за реагирование на события, генерируемые приложением.
Наш слушатель реализует интерфейс Listeneraggregate , который требует двух методов: attach для подключения одного или нескольких слушателей и отсоединения для удаления этих слушателей. Мы используем агрегатную характеристику слушателя для реализации метода отсоединения .
declare(strict_types=1);
namespace Application\Listener;
use Application\Event\LogEvent;
use Application\Model\LoggerAwareInterface as LoggerAware;
use Psr\Log\LoggerInterface;
use Zend\EventManager\EventInterface;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\EventManager\ListenerAggregateTrait;
class LogListener implements ListenerAggregateInterface
{
use ListenerAggregateTrait;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @param LoggerInterface $logger
*/
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* @inheritdoc
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
$this->listeners[] = $sharedManager->attach(LoggerAware::class, LogEvent::DEBUG, [$this, 'addDebug'], $priority);
$this->listeners[] = $sharedManager->attach(LoggerAware::class, LogEvent::INFO, [$this, 'addInfo'], $priority);
$this->listeners[] = $sharedManager->attach(LoggerAware::class, LogEvent::NOTICE, [$this, 'addNotice'], $priority);
$this->listeners[] = $sharedManager->attach(LoggerAware::class, LogEvent::WARNING, [$this, 'addWarning'], $priority);
$this->listeners[] = $sharedManager->attach(LoggerAware::class, LogEvent::ERROR, [$this, 'addError'], $priority);
$this->listeners[] = $sharedManager->attach(LoggerAware::class, LogEvent::CRITICAL, [$this, 'addCritical'], $priority);
$this->listeners[] = $sharedManager->attach(LoggerAware::class, LogEvent::ALERT, [$this, 'addAlert'], $priority);
$this->listeners[] = $sharedManager->attach(LoggerAware::class, LogEvent::EMERGENCY, [$this, 'addEmergency'], $priority);
}
private function addRecord(string $loggerLevel, EventInterface $event)
{
$logMessage = $event->getTarget();
$logContext = $event->getParams();
$this->logger->log($loggerLevel, $logMessage, $logContext);
}
public function addDebug(EventInterface $event)
{
return $this->addRecord(LogEvent::DEBUG, $event);
}
public function addInfo(EventInterface $event)
{
return $this->addRecord(LogEvent::INFO, $event);
}
// Methods must be added for each log level listed in the LogEvent
}
Мы специально используем SharedEventManager , поскольку его можно использовать, когда мы хотим подключить слушателей, еще не имея экземпляра класса, составляющего EventManager . В нашем случае это будет любой класс, который желает испускать Событие входа в систему . Как вы можете себе представить, прикрепление Прослушивателя журнала к каждому отдельному классу, излучающему события, было бы громоздким.
Мы подключаем прослушиватель и определяем метод для каждого уровня журнала, определенного в нашем Событие входа в систему . Некоторые из методов были опущены в приведенном выше примере для удобства чтения. Это позволяет нам регистрировать события на разных уровнях в зависимости от константы, заданной классом излучения.
Например:
// This event will be logged as INFO due to the constant specified $this->getEventManager()->trigger(LogEvent::INFO, 'Example log message', ['context']); // Whereas this event will be logged as CRITICAL $this->getEventManager()->trigger(LogEvent::CRITICAL, 'All connections down', ['context']);
Интерфейс LoggerAwareИнтерфейс
Поскольку мы используем SharedEventManager , нам необходимо указать идентификатор при подключении прослушивателя. Идентификатор используется для идентификации классов, которые могут испускать Регистрируйте событие и поэтому его следует прослушивать. Мы используем Интерфейс LoggerAwareИнтерфейс псевдоним Программное обеспечение для регистрации как наш идентификатор для слушателя. Он расширяет интерфейс EventManagerAwareИнтерфейс как для запуска тот Регистрируйте событие , класс-эмитент должен иметь доступ к экземпляру диспетчера событий. Интерфейс EventManagerAwareInterface требует реализации двух методов: seteventmanager и geteventmanager .
Для любых классов, полученных с помощью диспетчера служб Zend, которые реализуют интерфейс EventManagerAwareInterface , автоматически будет установлен менеджер событий. Это делает отправку событий очень простой, но в ней есть определенное “волшебное” качество, которое может быть непонятно разработчикам, которые раньше не работали с менеджером событий.
declare(strict_types=1);
namespace Application\Model;
use Zend\EventManager\EventManagerAwareInterface;
interface LoggerAwareInterface extends EventManagerAwareInterface
{
}
Например, класс Foo реализует интерфейс LoggerAwareИнтерфейс и имеет метод bar . Когда вызывается метод bar ,/| Событие журнала генерируется в диспетчере событий и обрабатывается любыми слушателями, подключенными к Событие входа в систему . Мы сами внедрили методы setEventManager и getEventManager . В методе setEventManager мы используем имя класса, а также любые интерфейсы, которые он реализует, в качестве идентификаторов событий. Вы помните, что ранее мы использовали Интерфейс LoggerAwareInterface в качестве нашего идентификатора.
class Foo implements LoggerAwareInterface {
private $events;
public function bar(): void
{
$this->getEventManager()->trigger(LogEvent::INFO, 'Example log message', ['context']);
}
/**
* @inheritDoc
*/
public function setEventManager(EventManagerInterface $events)
{
$className = get_class($this);
$nsPos = strpos($className, '\\') ?: 0;
$events->setIdentifiers(array_merge(
[
__CLASS__,
$className,
substr($className, 0, $nsPos),
],
array_values(class_implements($className))
));
$this->events = $events;
return $this;
}
/**
* @inheritDoc
*/
public function getEventManager()
{
if (!$this->events) {
$this->setEventManager(new EventManager());
}
return $this->events;
}
}
Интерфейс управления событиями
Дублирование setEventManager и getEventManager в каждом классе, из которого мы хотим выйти, это будет кошмаром для обслуживания, а также увеличит “визуальный долг”. Мы разделили эти методы на Признак EventManagerAware , который может использоваться в любом классе, который хочет генерировать события.
declare(strict_types=1);
namespace Application\Model;
use Zend\EventManager\EventManager;
use Zend\EventManager\EventManagerInterface;
trait EventManagerAwareTrait
{
private $events;
/**
* Set the event manager instance used by this context
*
* @param EventManagerInterface $events
* @return $this
*/
public function setEventManager(EventManagerInterface $events)
{
...
}
/**
* Retrieve the event manager
*
* Lazy-loads an EventManager instance if none registered.
*
* @return EventManagerInterface
*/
public function getEventManager()
{
...
}
}
Это упрощает наш пример до этого:
use Application\Model\EventManagerAwareTrait;
class Foo implements LoggerAwareInterface {
use EventManagerAwareTrait;
public function bar(): void
{
$this->getEventManager()->trigger(LogEvent::INFO, 'Example log message', ['context']);
}
}
Следует отметить, что вам не нужно будет использовать Функция EventManagerAware при отправке событий с контроллера Zend, поскольку в нем уже реализованы Seteventmanager и geteventmanager .
Прикрепление прослушивателя к начальной загрузке приложения
Почти на месте. Есть еще одна вещь, которую нужно сделать, прежде чем мы сможем начать регистрацию. Нам необходимо убедиться, что наш Прослушиватель журналов подключен к менеджеру событий на этапе начальной загрузки приложения. Мы можем сделать это в Module.php для модуля Приложения .
declare(strict_types=1);
namespace Application;
use Application\Listener\LogListener;
use Zend\EventManager\EventInterface;
use Zend\ModuleManager\Feature\BootstrapListenerInterface;
use Zend\ModuleManager\Feature\ConfigProviderInterface;
class Module implements ConfigProviderInterface, BootstrapListenerInterface
{
/**
* @inheritdoc
*/
public function getConfig()
{
return include __DIR__ . '/../config/module.config.php';
}
/**
* @inheritdoc
*/
public function onBootstrap(EventInterface $e)
{
$serviceManager = $e->getApplication()->getServiceManager();
$eventManager = $e->getApplication()->getEventManager();
$sharedEventManager = $eventManager->getSharedManager();
// Logging Listener
$logListener = $serviceManager->get(LogListener::class);
$logListener->attach($eventManager);
}
}
Отлично! Теперь мы можем легко запустить Войдите в систему из любого класса, просто реализовав интерфейс LoggerAwareИнтерфейс и используя Интерфейс управления событиями .
Ведение Журнала на основе Событий: 1 Год На
Новая панель управления имела успех и стала огромным шагом вперед по сравнению с предыдущей версией, как для разработчиков, так и для наших пользователей. Ведение журнала работает отлично и позволило нам лучше видеть наше приложение, обеспечивая при этом согласованный опыт для разработчиков.
Однако этот подход в значительной степени связал наше приложение с компонентом Zend event manager. Если бы мы провели рефакторинг нашего приложения для использования другой платформы, это заняло бы гораздо больше времени из-за связи. Эта проблема не была бы столь распространенной в других реализациях, которые я перечислил в начале этого поста.
В прошлом году мы также приняли в команду несколько новых разработчиков. Эти разработчики занимали младшие должности и раньше не имели опыта работы с такими приложениями, как наша панель управления. Система управления событиями обладает большим количеством “волшебного” поведения, которое непонятно новым разработчикам, особенно тем, которые не знакомы с архитектурой, управляемой событиями. Я думаю, что важно, чтобы такая жизненно важная вещь, как ведение журнала, немедленно отлаживалась всеми членами команды, независимо от уровня квалификации.
Если бы я снова начал этот проект, я бы передал экземпляр Интерфейса регистратора в любой класс, который этого требует. Этот подход может быть более “ручным”, но он гораздо более четкий и не привязывает нас к какой-либо конкретной структуре. Если вы уже работаете в системе , управляемой событиями, вы можете решить, что связь с компонентом event manager не является проблемой. Речь идет об использовании правильного инструмента для работы.
Если у вас есть какие-либо вопросы или отзывы по этой статье, вы можете написать мне по адресу ethan@messagecloud.com .
Оригинал: “https://dev.to/ethanbray/logging-in-zend-framework-using-the-event-manager-2a4e”