Subsistema de eventos como una forma de deshacerse de tareas "terminando"

Ya sabes, da la casualidad de que la tarea no se debe hacer bien, pero sí rápido, porque El dinero, los socios y muchas otras cosas que son muy importantes para los negocios están ligados a él. Como resultado, en algún lugar no pensaron en algo, en algún lugar lo perdieron, codificaron algo, en general, todo por el bien de la velocidad. Y, como, todo está bien, todo funciona, pero ...



Después de un tiempo, resulta que la funcionalidad debe ampliarse, pero es difícil hacerlo, no hay suficiente flexibilidad. Para la configuración, por supuesto, recurren a los desarrolladores. Y, por supuesto, distrae de otras tareas y no deja la sensación de que se pierde el tiempo.



Entonces tuve una situación así. Érase una vez, rápidamente anotaron la integración con el sistema de marketing por correo electrónico, y luego tareas como "si el usuario hizo esto, debe escribirlo aquí". Debido a la falta de visibilidad de los procesos comerciales, se produjo su intersección, los datos se sobrescribieron entre sí, se registró lo incorrecto.



Subsistema de eventos



Quiero contarte cómo salimos de esta situación.



En algún punto del sistema, algo o alguien genera un evento. Por ejemplo, un usuario se ha registrado, ha actualizado los datos de su perfil, ha realizado una compra, etc.



. , , CRM - . .



. , . , 20 , , 60, .



PHP Laravel. , .



Esquema de subsistema de eventos

, , . , , .



<?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\Services\Events;

use App\Services\FieldMapper;
use App\Interfaces\Services\Filter;
use App\Interfaces\Actions\ActionPool;
use App\Interfaces\Events\SystemEvent;
use App\Interfaces\Events\EventProcessor as IEventProcessor;

/**
 * event processor
 * @package App\Services\Events
 */
class EventProcessor implements IEventProcessor
{

    /** @var ActionPool */
    private $actionPool;

    /** @var Filter */
    private $filter;

    /** @var FieldMapper */
    private $fieldMapper;

    public function __construct(ActionPool $actionPool, Filter $filter, FieldMapper $fieldMapper)
    {
        $this->setActionPool($actionPool)->setFilter($filter)->setFieldMapper($fieldMapper);
    }

    /**
     * Process system event
     *
     * @param SystemEvent $event
     * @param array       $settings
     */
    public function process(SystemEvent $event, array $settings = []): void
    {
        collect($settings)->each(function (array $action) use ($event) {
            $eventData = $event->toArray();
            $conditions = $action['conditions'] ?? [];
            foreach ($conditions as $index => $condition) {
                if (isset($condition['not']) && $condition['not'] == 1) {
                    $conditions[$index]['condition'] .= '|!';
                }
            }
            if ($this->getFilter()->check($conditions, $eventData)) {
                foreach ($action['actions'] as $actionData) {
                    if (($actionO = $this->getActionPool()->create($actionData['action'])) !== null) {
                        try {
                            $freeInput = $actionData['free_input'] ?? [];
                            foreach ($freeInput as $key => $data) {
                                unset($freeInput[$key]);
                                $freeInput[$data['id']] = $data;
                            }
                            $data = $this->getFieldMapper()->map(array_merge($actionData['input'] ?? [], $freeInput), $eventData);
                            foreach ($data as $key => $val) {
                                $data[$key] = $this->prepareValue($val);
                            }

                            $data['event_fields'] = $eventData;
                            $actionO->run($data);
                        } catch (\Throwable $ex) {
                            \Log::critical($ex);
                        }
                    } else {
                        \Log::info('System', ['Can\'t create action ' . $actionData['action']]);
                    }
                }
            }
        });
    }

    /**
     * Prepare constants
     *
     * @param $value
     *
     * @return false|string
     */
    protected function prepareValue($value)
    {
        if ($value === 'current_date') {
            return date('Y-m-d H:i:s');
        }

        return $value;
    }

    /**
     * @return ActionPool
     */
    public function getActionPool(): ActionPool
    {
        return $this->actionPool;
    }

    /**
     * @param ActionPool $actionPool
     *
     * @return $this
     */
    public function setActionPool(ActionPool $actionPool): self
    {
        $this->actionPool = $actionPool;

        return $this;
    }

    /**
     * @return Filter
     */
    public function getFilter(): Filter
    {
        return $this->filter;
    }

    /**
     * @param Filter $filter
     *
     * @return $this
     */
    public function setFilter(Filter $filter): self
    {
        $this->filter = $filter;

        return $this;
    }

    /**
     * @return FieldMapper
     */
    public function getFieldMapper(): FieldMapper
    {
        return $this->fieldMapper;
    }

    /**
     * @param FieldMapper $fieldMapper
     *
     * @return $this
     */
    public function setFieldMapper(FieldMapper $fieldMapper): self
    {
        $this->fieldMapper = $fieldMapper;

        return $this;
    }
}


El método de proceso se llamará en 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; 
    } 
} 


Nos registramos con el proveedor:



<?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, 
        ], 
 
    ]; 
}


Como resultado, tuvimos la oportunidad de configurar eventos en el sistema a través de la interfaz. Habilite y deshabilite los controladores sin cambiar el código. Los nuevos módulos del sistema pueden agregar sus propios eventos y / o controladores sin intervención adicional.



Después de una pequeña formación, todo esto se transfirió a los usuarios del panel de administración, lo que liberó tiempo de trabajo adicional.



Y algo más de código.



Verificación de condiciones y mapeo de parámetros:



<?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; 
    } 



All Articles