Subsystem of events as a way to get rid of tasks by "finishing"

You know, as it happens, the task must be done not well, but quickly, because money, partners and many other things that are very important for business are tied to it. As a result, somewhere they didn't think about something, somewhere they missed it, they hard-coded something, in general, all for the sake of speed. And, like, everything is fine, everything works, but ...



After some time, it turns out that the functionality needs to be expanded, but it is difficult to do it, there is not enough flexibility. For the settings, of course, they turn to the developers. And, of course, it distracts from other tasks and does not leave the feeling that the time is wasted.



So I had such a situation. Once upon a time, they quickly wrote down the integration with the e-mail marketing system, and then tasks like "if the user did this, you need to write this down here". Due to the lack of visibility of business processes, their intersection occurred, the data overwritten each other, the wrong thing was recorded.



Event subsystem



I want to tell you how we got out of this situation.



At some point in the system, something or someone generates an event. For example, a user has registered, updated profile data, made a purchase, etc.



. , , 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\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;
    }
}


The process method will be called in the 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; 
    } 
} 


We register with the provider:



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


As a result, we got the opportunity to configure events in the system through the interface. Enable and disable handlers without changing the code. New modules of the system can add their own events and / or handlers without additional intervention.



After a little training, all this was transferred to the users of the admin panel, which freed up additional working time.



And some more code.



Condition checking and parameter mapping:



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