Sous-système d'événements comme moyen de se débarrasser des tâches en «finissant»

Vous savez, en l'occurrence, la tâche doit être effectuée non pas bien, mais rapidement, car l'argent, les partenaires et bien d'autres choses qui sont très importantes pour les affaires y sont liés. En conséquence, quelque part ils n'ont pas pensé à quelque chose, quelque part ils l'ont manqué, ils ont codé quelque chose en dur, en général, tout cela pour des raisons de vitesse. Et, comme, tout va bien, tout fonctionne, mais ...



Après un certain temps, il s'avère que la fonctionnalité doit être étendue, mais c'est difficile à faire, il n'y a pas assez de flexibilité. Pour les paramètres, bien sûr, ils se tournent vers les développeurs. Et, bien sûr, cela distrait des autres tâches et ne laisse pas le sentiment que le temps est perdu.



J'ai donc eu une telle situation. Il était une fois, ils ont rapidement noté l'intégration avec le système de marketing par e-mail, puis des tâches comme «si l'utilisateur a fait cela, vous devez l'écrire ici». En raison du manque de visibilité des processus métier, leur intersection s'est produite, les données se sont écrasées, la mauvaise chose a été enregistrée.



Sous-système d'événement



Je veux vous dire comment nous sommes sortis de cette situation.



À un moment donné du système, quelque chose ou quelqu'un génère un événement. Par exemple, un utilisateur s'est enregistré, a mis à jour ses données de profil, a effectué un achat, etc.



. , , CRM - . .



. , . , 20 , , 60, .



PHP Laravel. , .



Schéma de sous-système d'événement

, , . , , .



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


La méthode de processus sera appelée dans 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; 
    } 
} 


Nous nous inscrivons auprès du fournisseur:



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


En conséquence, nous avons eu la possibilité de configurer des événements dans le système via l'interface. Activez et désactivez les gestionnaires sans changer le code. Les nouveaux modules du système peuvent ajouter leurs propres événements et / ou gestionnaires sans intervention supplémentaire.



Après un peu de formation, tout cela a été transféré aux utilisateurs du panneau d'administration, ce qui a libéré du temps de travail supplémentaire.



Et encore du code.



Vérification des conditions et mappage des paramètres:



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