-
Notifications
You must be signed in to change notification settings - Fork 0
Быстрый старт
В данной статье будет описано, что потребуется, чтобы начать использовать библиотеку.
Описание базируется на примере из Какую проблему решает библиотека:
Давайте с помощью текущей библиотеки опишем данный процесс.
Для этого мы создадим свой репозиторий и в качестве зависимости укажем текущую библиотеку:
~# mkdir documentation && cd documentation
~/documentation# composer init
~/documentation# composer require jeyroik/extas-workflow:5.*
Данная библиотека построена на базе 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
в отдельном объекте - контексте
.
Итак, давайте установим всё, что мы написали и попробуем прогнать нашу статью по нашей схеме.
~/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());
}