Русский
Русский
English
Статистика
Реклама

Подсистема событий как способ избавиться от задач по допилу

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


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


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


Event subsystem / Подсистема событий

Хочу рассказать, как вышли из этой ситуации.


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


Это событие нужно поймать и обработать. Например, отправить письмо, передать данные в CRM или какую-то другую систему. Обработчиков может быть много и их количество будет увеличиваться со временем.


Необходимо связать событие и обработчики. Запускать их нужно как безусловно, так и по некоему условию. Например, если пользователю 20 лет, то ему отправляем письмо одного вида, а если 60, то другого.


Разработка ведется на PHP на Laravel. В этом фреймворке уже есть события и обработчики, на их основе и построена подсистема.


Event subsystem scheme / Подсистема событий - диаграмма

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


<?php App\Interfaces\Events  use Illuminate\Contracts\Support\Arrayable;  /** * System event * @package App\Interfaces\Events */ interface SystemEvent extends Arrayable {      /**      * Get event id      *      * @return string      */     public static function getId(): string;      /**      * Event name      *      * @return string      */     public static function getName(): string;      /**      * Available params      *      * @return array      */     public static function getAvailableParams(): array;      /**      * Get param by name      *      * @param string $name      *      * @return mixed      */     public function getParam(string $name); } 

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


<?php namespace App\Interfaces\Events;  /** * Interface for event pool * @package App\Interfaces\Events */ interface EventsPool {     /**      * Register event      *      * @param string $event      *      * @return mixed      */     public function register(string $event): self;      /**      * Get events list      *      * @return array      */     public function getAvailableEvents(): array;      /**      * @param string $alias      *      * @param array  $params      *      * @return mixed      */     public function create(string $alias, array $params = []); } 

Обработчик событий это просто класс, имеющий определённый интерфейс. И он, как и событие, сообщает, какие данные может принимать, что получается на выходе, имеет название и ID.


<?php namespace App\Interfaces\Actions;  /** * Interface for system action * @package App\Interfaces\Actions */ interface Action {     /**      * Get ID      *      * @return string      */     public static function getId(): string;      /**      * Get name      *      * @return string      */     public static function getName(): string;      /**      * Available input params      *      * @return array      */     public static function getAvailableInput(): array;      /**      * Available output params      *      * @return array      */     public static function getAvailableOutput(): array;      /**      * Run action      *      * @param array $params      *      * @return void      */     public function run(array $params): void; } 

Обработчики так же регистрируются в реестре с таким же интерфейсом как у пула событий.


Рассмотрим gui настройки связи событие-обработчик. У меня он реализован с использованием knockout.js, но это не принципиально.



Как вы видите, есть блок в котором настраиваются условия запуска обработчика. Первая колонка параметр из события, затем идёт условие и значение, с которым будет сравнение.


В настройке обработчика так же три основных колонки. Первая параметр из обработчика. В него нужно передать параметр из события(это вторая колонка). Параметр события можно не задавать, значение может быть константой. Например, в случае регистрации по e-mail передаётся 0, а в случае регистрации через соц.сеть передаётся 1, или какие-то человекопонятные значения.


В самом начале говорил, что все началось с интеграции с системой email- маркетинга Sendsay. В момент создания сущности в нашей системе, должна создаваться так называемая анкета на стороне Sendsay. При создании, в неё не передаются пользовательские данные, все статично. Это тот случай, когда нужно задать произвольные значения. Добавляем строку, вбиваем название поля в анкете, а в значение тип поля.


Связь настроили, посмотрим на главный обработчик событий.


<?php namespace App\Interfaces\Events;  /** * Interface for event processor * @package App\Interfaces\Events */ interface EventProcessor {     /**      * Process system event      *      * @param SystemEvent $event      * @param array       $settings      */     public function process(SystemEvent $event, array $settings = []): void; } 

<?php namespace App\Interfaces\Events;  /** * Interface for event processor * @package App\Interfaces\Events */ interface EventProcessor {     /**      * Process system event      *      * @param SystemEvent $event      * @param array       $settings      */     public function process(SystemEvent $event, array $settings = []): void; } 

Метод process будем вызывать в SystemEventListener.


<?php namespace App\Listeners;  use App\Interfaces\Events\SystemEvent; use App\Interfaces\Events\EventProcessor; use App\Models\EventSettings; use Illuminate\Support\Collection;  class SystemEventListener {     /** @var EventProcessor */     private $eventProcessor;      public function __construct(EventProcessor $eventProcessor)     {         $this->setEventProcessor($eventProcessor);     }      public function handle(SystemEvent $event): void     {         EventSettings::query()->where('is_active', true)->where('event_id', $event::getId())->chunk(10, function (Collection $collection) use ($event) {             $collection->each(function (EventSettings $model) use ($event) {                 $this->getEventProcessor()->process($event, $model->settings);             });         });     }      /**      * @return EventProcessor      */     public function getEventProcessor(): EventProcessor     {         return $this->eventProcessor;     }      /**      * @param EventProcessor $eventProcessor      *      * @return $this      */     public function setEventProcessor(EventProcessor $eventProcessor): self     {         $this->eventProcessor = $eventProcessor;          return $this;     } } 

Регистрируем в провайдере:


<?php namespace App\Providers;  use App\Interfaces\Events\SystemEvent; use App\Listeners\SystemEventListener; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;   class EventServiceProvider extends ServiceProvider {     /**      * The event listener mappings for the application.      *      * @var array      */     protected $listen = [          SystemEvent::class            => [             SystemEventListener::class,         ],      ]; }

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


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


И еще немного кода.


Проверка условий и маппинг параметров:


<?php namespace App\Interfaces\Services;  /** * Interface for service to filter data (from HUB) * @package App\Interfaces\Services */ interface Filter {     public const CONDITION_EQUAL = '=';      public const CONDITION_MORE = '>';      public const CONDITION_LESS = '<';      public const CONDITION_NOT = '!';      public const CONDITION_BETWEEN = 'between';      public const CONDITION_IN = 'in';      public const CONDITION_EMPTY = 'empty';      /**      * Filter data      *      * @param array $filter      * @param array $data      *      * @return array      */     public function filter(array $filter, array $data): array;      /**      * Check conditions      *      * @param array $conditions      * @param array $data      *      * @return bool      */     public function check(array $conditions, array $data): bool; } 

<?php namespace App\Services;  use Illuminate\Support\Arr; use App\Interfaces\Services\Filter as IFilter;  /** * Service to filter data by conditions   * @package App\Services */ class Filter implements IFilter {      /**      * Filter data      *      * @param array $filter      * @param array $data      *      * @return array      */     public function filter(array $filter, array $data): array     {         if (!empty($filter)) {             foreach ($filter as $condition) {                 $field = $condition['field'] ?? null;                 if (empty($field)) {                     continue;                 }                 $operation = $condition['operation'] ?? null;                 $value1 = $condition['value1'] ?? null;                 $value2 = $condition['value2'] ?? null;                 $success = $condition['success'] ?? null;                 $filterResult = $condition['result'] ?? null;                  $value = Arr::get($data, $field, '');                 if ($field !== null && $this->checkCondition($value, $operation, $value1, $value2)) {                     return $success !== null ? $this->filter($success, $data) : $filterResult;                 }             }         }          return [];     }      /**      * Check condition      *      * @param $value      * @param $condition      * @param $value1      * @param $value2      *      * @return bool      */     protected function checkCondition($value, $condition, $value1, $value2): bool     {         $result = false;         $value = \is_string($value) ? mb_strtolower($value) : $value;         $value1 = \is_string($value1) ? mb_strtolower($value1) : $value1;         if ($value2 !== null) {             $value2 = \is_string($value2) ? mb_strtolower($value2) : $value2;         }         $conditions = explode('|', $condition);         $invert = \in_array(self::CONDITION_NOT, $conditions);         $conditions = array_filter($conditions, function ($item) {             return $item !== self::CONDITION_NOT;         });         $condition = implode('|', $conditions);         switch ($condition) {             case self::CONDITION_EQUAL:                 $result = ($value == $value1);                 break;             case self::CONDITION_IN:                 $result = \in_array($value, (array)$value1);                 break;             case self::CONDITION_LESS:                 $result = ($value < $value1);                 break;             case self::CONDITION_MORE:                 $result = ($value > $value1);                 break;             case self::CONDITION_MORE . '|' . self::CONDITION_EQUAL:             case self::CONDITION_EQUAL . '|' . self::CONDITION_MORE:                 $result = ($value >= $value1);                 break;             case self::CONDITION_LESS . '|' . self::CONDITION_EQUAL:             case self::CONDITION_EQUAL . '|' . self::CONDITION_LESS:                 $result = ($value <= $value1);                 break;             case self::CONDITION_BETWEEN:                 $result = (($value >= $value1) && ($value <= $value2));                 break;             case self::CONDITION_EMPTY:                 $result = empty($value);                 break;         }          return $invert ? !$result : $result;     }      /**      * Check conditions      *      * @param array $conditions      * @param array $data      *      * @return bool      */     public function check(array $conditions, array $data): bool     {         $result = true;         if (!empty($conditions)) {             foreach ($conditions as $condition) {                 $field = $condition['param'] ?? null;                 if (empty($field)) {                     continue;                 }                 $operation = $condition['condition'] ?? null;                 $value1 = $condition['value'] ?? null;                 $value2 = $condition['value2'] ?? null;                  $value = Arr::get($data, $field, '');                  $result &= $this->checkCondition($value, $operation, $value1, $value2);             }         }          return $result;     } } 

<?php namespace App\Interfaces\Services;  /** * Interface for service to map params * @package App\Interfaces\Services */ interface FieldMapper {     /**      * Map      *      * @param array $map      * @param array $data      *      * @return array      */     public function map(array $map, array $data): array; } 

<?php namespace App\Services;  use Illuminate\Support\Arr; use App\Interfaces\Services\FieldMapper as IFieldMapper;  /** * Params/fields mapper (by HUB) * @package App\Services */ class FieldMapper implements IFieldMapper {      /**      * Map      *      * @param array $map      * @param array $data      *      * @return array      */     public function map(array $map, array $data): array     {         $result = [];         foreach ($map as $from => $to) {             $to = (array)$to;             if (!empty($to['param']) && ($value = Arr::get($data, $to['param'])) !== null) {                 Arr::set($result, $from, $value);             } elseif ($to['value'] !== '') {                 Arr::set($result, $from, Arr::get($data, $to['value'], isset($to['value_as_param']) && $to['value_as_param'] ? '' : $to['value']));             }         }          return $result;     } 
Источник: habr.com
К списку статей
Опубликовано: 27.07.2020 20:17:32
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Laravel

Php

Проектирование и рефакторинг

Архитектура приложений

Категории

Последние комментарии

© 2006-2020, personeltest.ru