Skip to content

Быстрый старт

jeyroik edited this page Jun 1, 2021 · 9 revisions

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

Описание базируется на примере из Какую проблему решает библиотека:

Пример схемы

Давайте с помощью текущей библиотеки опишем данный процесс.

Подготовка

Для этого мы создадим свой репозиторий и в качестве зависимости укажем текущую библиотеку:

~# mkdir documentation && cd documentation
~/documentation# composer init
~/documentation# composer require jeyroik/extas-workflow:5.*

Extas.json

Данная библиотека построена на базе extas, который в качестве основного декларирующего файла использует extas.json (подробнее можно почитать здесь).

Поэтому наш бизнес процесс будем описывать в данном файле. Создадим каркас:

{
  "name": "my/documentation"
}

Схема

Первым делом надо создать схему нашего бизнес процесса. Эта схема будет сердцем всего происходящего.

extas.json

{
  "name": "my/documentation",
  "workflow_schemas": [
    {
      "name": "doc_article",
      "title": "Статья документации",
      "description": "Схема бизнес процесса создания, сохранения и публикации статьи документации"
    }
  ]
}

На первых порах этого будет достаточно. Уже сейчас мы можем установить нашу схему:

~/documentation# vendor/bin/extas i

Примечание: подробнее про процесс установки можно почитать здесь.

Состояния

Далее, чтобы описать наш бизнес процесс, нам потребуются состояния, по которым будет путешествовать наша сущность. Для этого используется сущность extas\interfaces\workflows\states\IState.

Примечание: В рамках данного пакета используется такое понятие как "Sample" (образец/сэмпл/шаблон). Сэмплы позволяют переиспользовать общее в разных схемах бизнес процессов, не создавая связности между ними. Например, если в будущем мы планируем описать ещё, скажем, бизнес процесс создания статей в блоге, то нам, скорее всего, потребуются примерно такие же состояния и переходы.

Учитывая примечание (см. выше), мы создадим и шаблон состояния, и само состояние:

extas.json

{
  "name": "my/documentation",
  "workflow_states_samples": [
    {
      "name": "in_work",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
    }
  ],
  "workflow_states": [
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "in_work",
      "schema_name": "doc_article"
    }
  ],
  "workflow_schemas": [
    {
      "name": "doc_article",
      "title": "Статья документации",
      "description": "Схема бизнес процесса создания, сохранения и публикации статьи документации"
    }
  ]
}

Примечание: Далее мы будем опускать некоторые блоки extas.json, которые не обязательно видеть в рамках текущего абзаца, а в самом конце сформируем полную схему.

Что мы натворили:

  • Определили шаблон состояния, который содержит несколько описательных атрибутов (имя, заголовок, описание), а также параметры.
  • Определили состояние, основанное на созданном шаблоне.

Атрибут parameters позволяет задать набор атрибутов, которые необходимо проверить в сущности при переходе в данное состояние. Т.к. при переходе в состояние "В работе" у нас нет параметров, которые уже должны быть, мы оставили этот атрибут пустым.

Таким образом, при гипотетическом создании UI для Workflow, на этапе создания состояния для схемы, вы можете отображать список сэмплов состояний, а при отображении самой созданной схемы - уже сами состояния. Также, при создании состояния программным путём, вы можете принимать на вход всего лишь сэмпл, по образцу которого создавать новое состояние:

use extas\interfaces\workflows\states\IStateSample;
use extas\interfaces\workflows\states\IState;
//...
public function createState(IStateSample $sample)
{
    $state = new State();
    $state->buildFromSample($sample);
}

К текущему пакету подключен плагин, который позволяет автоматически генерировать uuid строку с префиксом имени сэмпла. Это можно использовать следующим образом:

use extas\interfaces\workflows\states\IStateSample;
use extas\interfaces\workflows\states\IState;
use extas\interfaces\workflows\states\IStateRepository;
//...
public function createState(IStateSample $sample, IStateRepository $repo)
{
    $state = new State();
    $state->buildFromSample($sample, '@sample(uuid6)');
    $state = $repo->create($state);
    echo $state->getName(); // что-то вроде <$sample.name>_24ff14bb-c9b9-4d09-a3fd-8fce2030eec8
    // т.е. если $sample->getName() == 'in_work', то $state->getName() будет "in_work_24ff1...0eec8"
}

Итого на текущем этапе: если попытаться сейчас нарисовать схему бизнес процесса по тому, что у нас имеется в extas.json, то мы увидим схему с одним состоянием. Добавьте недостающие состояния done и published самостоятельно и сверьтесь с результатом:

extas.json

{
  "name": "my/documentation",
  "workflow_states_samples": [
    {
      "name": "in_work",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
    },
    {
      "name": "done",
      "title": "Статья готова",
      "description": "Статья сохранена и готова к публикации",
      "parameters": [],
    },
    {
      "name": "published",
      "title": "Статья опубликована",
      "description": "Статья опубликована",
      "parameters": [],
    }
  ],
  "workflow_states": [
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "in_work",
      "schema_name": "doc_article"
    },
    {
      "name": "@sample(uuid6)",
      "title": "Статья готова",
      "description": "Статья сохранена и готова к публикации",
      "parameters": [],
      "sample_name": "done",
      "schema_name": "doc_article"
    },
    {
      "name": "@sample(uuid6)",
      "title": "Статья опубликована",
      "description": "Статья опубликована",
      "parameters": [],
      "sample_name": "published",
      "schema_name": "doc_article"
    }
  ]
}

Переходы

Итак, у нас есть схема с состояниями, пора описать переходы между ними. Здесь картина аналогичная состояниям в плане сэмплов.

extas.json

{
  "name": "my/documentation",
  "workflow_transitions_samples": [
    {
      "name": "to_work",
      "title": "Взять в работу",
      "description": "Взять статью в работу, т.е. фактически начать её создавать",
      "parameters": [],
    },
    {
      "name": "save",
      "title": "Сохранить",
      "description": "Сохранить статью",
      "parameters": [],
    },
    {
      "name": "publish",
      "title": "Опубликовать",
      "description": "Опубликовать статью",
      "parameters": [],
    }
  ],
  "workflow_transitions": [
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "to_work",
      "schema_name": "doc_article"
    },
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "save",
      "schema_name": "doc_article"
    },
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "publish",
      "schema_name": "doc_article"
    }
  ]
}

В итоге по имеющимся в нашем extas.json данным уже можно нарисовать настоящую схему с вершинами и связями между ними.

Сущность

Теперь нам нужно описать сущность, которая будет путешествовать по нашей схеме. С сущностями также всё аналогично - есть сэмплы и сущности, привязанные к схемам.

Опишем нашу сущность.

{
  "workflow_entities_samples": [
    {
      "name": "article",
      "title": "Статья",
      "description": "Статья с заголовком, содержанием, автором и тегами",
      "parameters": [
        {
          "name": "title",
          "title": "Заголовок",
          "description": "Заголовок статьи",
        },
        {
          "name": "content",
          "title": "Содержание",
          "description": "Содержание статьи",
        },
        {
          "name": "author",
          "title": "Автор",
          "description": "Автор статьи",
        },
        {
          "name": "tags",
          "title": "Теги",
          "description": "Теги статьи",
        }
      ]
    }
  ],
  "workflow_entities": [
    {
      "name": "@sample(uuid6)",
      "title": "Статья",
      "description": "Статья с заголовком, содержанием, автором и тегами",
      "schema_name": "doc_article",
      "parameters": [
        {
          "name": "title",
          "title": "Заголовок",
          "description": "Заголовок статьи",
        },
        {
          "name": "content",
          "title": "Содержание",
          "description": "Содержание статьи",
        },
        {
          "name": "author",
          "title": "Автор",
          "description": "Автор статьи",
        },
        {
          "name": "tags",
          "title": "Теги",
          "description": "Теги статьи",
        }
      ]
    }
  ]
}

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

Обработчики переходов

В нашей схеме мы видим ограничения на переходы в состояния "Готова" и "Опубликована". Для описания ограничений используются обработчики переходов. С ними история уже знакомая - есть шаблоны и есть конкретные сущности, привязанные к схеме, а в случае обработчиков переходов, ещё и к конкретному переходу.

Существует пакет extas-workflow-dispatchers, который содержит несколько базовых обработчиков, которые мы и будем использовать, чтобы не растягивать старт.

~/documentation# composer require jeyroik/extas-workflow-dispatchers:1.*

Установим шаблоны обработчиков из этого пакета:

~/documentation# vendor/bin/extas i

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

Прежде, чем приступить к описанию ограничений, важно отметить, что в рамках Workflow существует три типа обработчиков:

  • Условия (conditions).
  • Валидаторы (validators).
  • Триггеры (triggers).

Условия

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

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

Валидаторы

Данные обработчики срабатывают непосредственно при попытке перехода сущности из одного состояния в другое, т.е. в момент перехода.

Триггеры

Из названия уже понятно, что данные обработчики вступают в игру уже после перехода сущности в конечное состояние.

Описание ограничений

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

  • Проверка заголовка при сохранении статьи: заголовка нет в момент перехода статьи в работу, а значит это ограничение не может быть условием. Следовательно, это валидатор.
  • Проверка заголовка и содержания при публикации статьи: оба параметры уже должны существовать у статьи в состоянии готова, а значит это ограничение можно оформить условием.
  • Оба уведомления (о создании статьи и о её публикации), очевидно, должны быть триггерами.

Отлично, когда мы разобрались что к чему, можно приступать к описанию.

{
  "workflow_transition_dispatchers": [
    {
      "name": "@sample(uuid6)",
      "title": "Параметры сущности",
      "description": "Проверка наличия в сущности необходимых параметров",
      "type": "validator",
      "transition_name": "@schema.doc_article.save",
      "class": "extas\\components\\workflows\\transitions\\dispatchers\\EntityHasAllParams",
      "parameters": [
        {
          "name": "title",
          "condition": "not_empty"
        }
      ]
    },
    {
      "name": "@sample(uuid6)",
      "title": "Параметры сущности",
      "description": "Проверка наличия в сущности необходимых параметров",
      "type": "condition",
      "transition_name": "@schema.doc_article.publish",
      "class": "extas\\components\\workflows\\transitions\\dispatchers\\EntityHasAllParams",
      "parameters": [
        {
          "name": "title",
          "condition": "not_empty"
        },
        {
          "name": "description",
          "condition": "not_empty"
        }
      ]
    },
    {
      "name": "@sample(uuid6)",
      "title": "Уведомление",
      "description": "Отправить внешний запрос",
      "type": "trigger",
      "transition_name": "@schema.doc_article.save",
      "class": "extas\\components\\workflows\\transitions\\dispatchers\\Notify",
      "parameters": [
        {
          "name": "notifier_class",
          "value": "documentation\\components\\dispatchers\\CurlNotifier"
        },
        {
          "name": "host",
          "value": "localhost"
        },
        {
          "name": "port",
          "value": "80"
        },
        {
          "name": "schema",
          "value": "http"
        },
        {
          "name": "route",
          "value": "test/route/saved"
        }
      ]
    },
    {
      "name": "@sample(uuid6)",
      "title": "Уведомление",
      "description": "Отправить внешний запрос",
      "type": "trigger",
      "transition_name": "@schema.doc_article.publish",
      "class": "extas\\components\\workflows\\transitions\\dispatchers\\Notify",
      "parameters": [
        {
          "name": "notifier_class",
          "value": "documentation\\components\\dispatchers\\CurlNotifier"
        },
        {
          "name": "host",
          "value": "localhost"
        },
        {
          "name": "port",
          "value": "80"
        },
        {
          "name": "schema",
          "value": "http"
        },
        {
          "name": "route",
          "value": "test/route/published"
        }
      ]
    }
  ]
}

В обработчиках видно несуществующий класс CurlNotifier, давайте его создадим.

namespace documentation\components\dispatchers;

use extas\components\samles\parameters\THasSampleParameters;
use extas\components\Item;
use extas\interfaces\workflows\transitions\dispatchers\INotifier;

class CurlNotifier extends Item implements INotifier
{
    use THasSampleParameters;

    /**
     * @param IEntity $entity
     * @param IItem $context
     * @param ITransitResult $result
     */
    public function notify(IEntity $entity, IItem $context, ITransitResult &$result): void
    {
        $url = $this->getParameterValue('schema')
             . $this->getParameterValue('host') . ':'
             . $this->getparameterValue('port') . '/'
             . $this->getparameterValue('route') . '?name=' . $entity->getName()
        exec('curl ' . $url . ' >> /tmp/notifier.log');
    }

    protected function getSubjectForExtension(): string
    {
        return 'documentation.notifier';
    }
}

Наш "уведомлятор" просто стучится curl'ом по урлу и складывает ответ в /tmp/notifier.log. Это очень плохой пример, но простой.

В имени перехода в описании обработчиков видно странную конструкцию, она не рабочая, а просто показывает, откуда надо подставить имя. Т.е. чтобы заранее описать обработчики, надо дать переходам фиксированные имена и подставить их здесь в поле transition_name.

Итого наш extas.json выглядит следующим образом:

{
  "name": "my/documentation",
  "workflow_schemas": [
    {
      "name": "doc_article",
      "title": "Статья документации",
      "description": "Схема бизнес процесса создания, сохранения и публикации статьи документации"
    }
  ],
  "workflow_states_samples": [
    {
      "name": "in_work",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
    },
    {
      "name": "done",
      "title": "Статья готова",
      "description": "Статья сохранена и готова к публикации",
      "parameters": [],
    },
    {
      "name": "published",
      "title": "Статья опубликована",
      "description": "Статья опубликована",
      "parameters": [],
    }
  ],
  "workflow_states": [
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "in_work",
      "schema_name": "doc_article"
    },
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "in_work",
      "schema_name": "doc_article"
    },
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "in_work",
      "schema_name": "doc_article"
    }
  ],
  "workflow_transitions_samples": [
    {
      "name": "to_work",
      "title": "Взять в работу",
      "description": "Взять статью в работу, т.е. фактически начать её создавать",
      "parameters": [],
    },
    {
      "name": "save",
      "title": "Сохранить",
      "description": "Сохранить статью",
      "parameters": [],
    },
    {
      "name": "publish",
      "title": "Опубликовать",
      "description": "Опубликовать статью",
      "parameters": [],
    }
  ],
  "workflow_transitions": [
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "to_work",
      "schema_name": "doc_article"
    },
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "save",
      "schema_name": "doc_article"
    },
    {
      "name": "@sample(uuid6)",
      "title": "Статья в работе",
      "description": "Статья в процессе создания",
      "parameters": [],
      "sample_name": "publish",
      "schema_name": "doc_article"
    }
  ],
  "workflow_entities_samples": [
    {
      "name": "article",
      "title": "Статья",
      "description": "Статья с заголовком, содержанием, автором и тегами",
      "parameters": [
        {
          "name": "title",
          "title": "Заголовок",
          "description": "Заголовок статьи",
        },
        {
          "name": "content",
          "title": "Содержание",
          "description": "Содержание статьи",
        },
        {
          "name": "author",
          "title": "Автор",
          "description": "Автор статьи",
        },
        {
          "name": "tags",
          "title": "Теги",
          "description": "Теги статьи",
        }
      ]
    }
  ],
  "workflow_entities": [
    {
      "name": "@sample(uuid6)",
      "title": "Статья",
      "description": "Статья с заголовком, содержанием, автором и тегами",
      "schema_name": "doc_article",
      "parameters": [
        {
          "name": "title",
          "title": "Заголовок",
          "description": "Заголовок статьи",
        },
        {
          "name": "content",
          "title": "Содержание",
          "description": "Содержание статьи",
        },
        {
          "name": "author",
          "title": "Автор",
          "description": "Автор статьи",
        },
        {
          "name": "tags",
          "title": "Теги",
          "description": "Теги статьи",
        }
      ]
    }
  ],
  "workflow_transition_dispatchers": [
    {
      "name": "@sample(uuid6)",
      "title": "Параметры сущности",
      "description": "Проверка наличия в сущности необходимых параметров",
      "type": "validator",
      "transition_name": "@schema.doc_article.save",
      "class": "extas\\components\\workflows\\transitions\\dispatchers\\EntityHasAllParams",
      "parameters": [
        {
          "name": "title",
          "condition": "not_empty"
        }
      ]
    },
    {
      "name": "@sample(uuid6)",
      "title": "Параметры сущности",
      "description": "Проверка наличия в сущности необходимых параметров",
      "type": "condition",
      "transition_name": "@schema.doc_article.publish",
      "class": "extas\\components\\workflows\\transitions\\dispatchers\\EntityHasAllParams",
      "parameters": [
        {
          "name": "title",
          "condition": "not_empty"
        },
        {
          "name": "description",
          "condition": "not_empty"
        }
      ]
    },
    {
      "name": "@sample(uuid6)",
      "title": "Уведомление",
      "description": "Отправить внешний запрос",
      "type": "trigger",
      "transition_name": "@schema.doc_article.save",
      "class": "extas\\components\\workflows\\transitions\\dispatchers\\Notify",
      "parameters": [
        {
          "name": "notifier_class",
          "value": "documentation\\components\\dispatchers\\CurlNotifier"
        },
        {
          "name": "host",
          "value": "localhost"
        },
        {
          "name": "port",
          "value": "80"
        },
        {
          "name": "schema",
          "value": "http"
        },
        {
          "name": "route",
          "value": "test/route/saved"
        }
      ]
    },
    {
      "name": "@sample(uuid6)",
      "title": "Уведомление",
      "description": "Отправить внешний запрос",
      "type": "trigger",
      "transition_name": "@schema.doc_article.publish",
      "class": "extas\\components\\workflows\\transitions\\dispatchers\\Notify",
      "parameters": [
        {
          "name": "notifier_class",
          "value": "documentation\\components\\dispatchers\\CurlNotifier"
        },
        {
          "name": "host",
          "value": "localhost"
        },
        {
          "name": "port",
          "value": "80"
        },
        {
          "name": "schema",
          "value": "http"
        },
        {
          "name": "route",
          "value": "test/route/published"
        }
      ]
    }
  ]
}

Используя данное описание, уже можно достаточно подробно описать наш бизнес процесс.

Но смысл данной библиотеки не только в описании схемы бизнес процесса, но и в "прогоне" сущности по состояниям. Делается это с помощью класса extas\components\workflows\Workflow.

Workflow

Любой процесс проходит в рамках какого-то контекста. Это может быть текущее время, пользователь и т.п. Все эти данные передаются в Workflow в отдельном объекте - контексте.

Итак, давайте установим всё, что мы написали и попробуем прогнать нашу статью по нашей схеме.

~/documentation# vendor/bin/extas i

use extas\components\workflows\Workflow;
use extas\components\workflows\entities\Entity;
use extas\components\workflows\transition\Transition;

$entity = new Entity([
    Entity::FIELD__NAME => 'article',
    Entity::FIELD__STATE_NAME => 'in_work_24ff14bb-c9b9-4d09-a3fd-8fce2030eec8',
    'title' => 'Тестовая статья',
    'content' => 'Содержание статьи довольно короткое, но может быть и огромным',
    'author' => 'jeyroik',
    'tags' => 'workflow, test, article'    
]);
$transition = new Transition([
    Transition::FIELD__NAME => 'save_24ff14bb-c9b9-4d09-a3fd-8fce2030eec8',
    Transition::FIELD__STATE_TO => 'done_2f14f4bb-4d09-a3fd-c9b9-8fce2030eec8'
]);
$workflow = new Wrofklow([
    Workflow::FIELD__CONTEXT => new EntityContext(['current_date' => date('Y-m-d')])
]);

$result = $workflow->transit($entity, $transition);

if ($result->hasErrors()) {
    print_r($result);
} else {
    echo 'Finished.';
    print_r($result->getEntity());
}