Initial commit

This commit is contained in:
root
2026-01-19 17:44:46 +01:00
commit 823af8b11d
8721 changed files with 1130846 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,237 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Core\Utils\DateTime;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\ORM\Collection;
use Throwable;
abstract class Activity extends Base
{
/** @var string[] */
protected array $pendingBoundaryTypeList = [
'eventIntermediateConditionalBoundary',
'eventIntermediateTimerBoundary',
'eventIntermediateSignalBoundary',
'eventIntermediateMessageBoundary',
];
/**
* @throws Error
*/
public function beforeProcess(): void
{
$this->prepareBoundary();
$this->refreshFlowNode();
$this->refreshTarget();
}
/**
* @throws Error
*/
public function prepareBoundary(): void
{
$boundaryFlowNodeList = [];
$attachedElementIdList = $this->getProcess()->getAttachedToFlowNodeElementIdList($this->getFlowNode());
foreach ($attachedElementIdList as $id) {
$item = $this->getProcess()->getElementDataById($id);
if (!in_array($item->type, $this->pendingBoundaryTypeList)) {
continue;
}
$boundaryFlowNode = $this->getManager()->prepareFlow(
$this->getTarget(),
$this->getProcess(),
$id,
$this->getFlowNode()->get('id'),
$this->getFlowNode()->getElementType()
);
if ($boundaryFlowNode) {
$boundaryFlowNodeList[] = $boundaryFlowNode;
}
}
foreach ($boundaryFlowNodeList as $boundaryFlowNode) {
$this->getManager()->processPreparedFlowNode($this->getTarget(), $boundaryFlowNode, $this->getProcess());
}
}
public function isProcessable(): bool
{
return $this->getFlowNode()->getStatus() === BpmnFlowNode::STATUS_CREATED;
}
protected function isInNormalFlow(): bool
{
return !$this->getFlowNode()->getElementDataItemValue('isForCompensation');
}
/**
* @throws Error
*/
protected function setFailedWithError(?string $errorCode = null, ?string $errorMessage = null): void
{
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_FAILED);
$flowNode->set([
'processedAt' => date(DateTime::SYSTEM_DATE_TIME_FORMAT),
]);
$this->getEntityManager()->saveEntity($flowNode);
$this->getManager()->endProcessWithError($this->getProcess(), $errorCode, $errorMessage);
}
/**
* @throws Error
*/
protected function setFailed(): void
{
$this->rejectPendingBoundaryFlowNodes();
$errorCode = $this->getFlowNode()->getDataItemValue('errorCode');
$errorMessage = $this->getFlowNode()->getDataItemValue('errorMessage');
$boundaryErrorFlowNode = $this->getManager()
->prepareBoundaryErrorFlowNode($this->getFlowNode(), $this->getProcess(), $errorCode);
if (!$boundaryErrorFlowNode) {
$this->setFailedWithError($errorCode, $errorMessage);
return;
}
$boundaryErrorFlowNode->setDataItemValue('code', $errorCode);
$boundaryErrorFlowNode->setDataItemValue('message', $errorMessage);
$this->getEntityManager()->saveEntity($boundaryErrorFlowNode);
parent::setFailed();
$this->getManager()->processPreparedFlowNode($this->getTarget(), $boundaryErrorFlowNode, $this->getProcess());
}
/**
* @throws Error
*/
protected function setFailedWithException(Throwable $e): void
{
$errorCode = (string) $e->getCode();
$this->rejectPendingBoundaryFlowNodes();
$boundaryErrorFlowNode = $this->getManager()
->prepareBoundaryErrorFlowNode($this->getFlowNode(), $this->getProcess(), $errorCode);
if (!$boundaryErrorFlowNode) {
$this->setFailedWithError($errorCode, $e->getMessage());
return;
}
$boundaryErrorFlowNode->setDataItemValue('code', $errorCode);
$boundaryErrorFlowNode->setDataItemValue('message', $e->getMessage());
$this->getEntityManager()->saveEntity($boundaryErrorFlowNode);
parent::setFailed();
$this->getManager()->processPreparedFlowNode($this->getTarget(), $boundaryErrorFlowNode, $this->getProcess());
}
/**
* @return Collection<BpmnFlowNode>
*/
protected function getPendingBoundaryFlowNodeList(): Collection
{
return $this->getEntityManager()
->getRDBRepositoryByClass(BpmnFlowNode::class)
->where([
'elementType' => $this->pendingBoundaryTypeList,
'processId' => $this->getProcess()->get('id'),
'status' => [
BpmnFlowNode::STATUS_CREATED,
BpmnFlowNode::STATUS_PENDING,
],
'previousFlowNodeId' => $this->getFlowNode()->get('id'),
])
->find();
}
protected function rejectPendingBoundaryFlowNodes(): void
{
$boundaryNodeList = $this->getPendingBoundaryFlowNodeList();
foreach ($boundaryNodeList as $boundaryNode) {
$boundaryNode->set('status', BpmnFlowNode::STATUS_REJECTED);
$this->getEntityManager()->saveEntity($boundaryNode);
}
}
protected function setRejected(): void
{
$this->rejectPendingBoundaryFlowNodes();
parent::setRejected();
}
protected function setProcessed(): void
{
$this->rejectPendingBoundaryFlowNodes();
parent::setProcessed();
}
protected function setInterrupted(): void
{
$this->rejectPendingBoundaryFlowNodes();
parent::setInterrupted();
}
/**
* @return string[]
*/
protected function getReturnVariableList(): array
{
$newVariableList = [];
$variableList = $this->getAttributeValue('returnVariableList') ?? [];
foreach ($variableList as $variable) {
if (!$variable) {
continue;
}
if ($variable[0] === '$') {
$variable = substr($variable, 1);
}
$newVariableList[] = $variable;
}
return $newVariableList;
}
}

View File

@@ -0,0 +1,611 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Formula\Exceptions\Error as FormulaError;
use Espo\Core\Formula\Manager as FormulaManager;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\Core\Utils\Log;
use Espo\Core\WebSocket\Submission;
use Espo\Modules\Advanced\Core\Bpmn\BpmnManager;
use Espo\Modules\Advanced\Entities\BpmnProcess;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\Core\Exceptions\Error;
use Espo\Core\Container;
use Espo\Core\Utils\Metadata;
use Espo\ORM\EntityManager;
use Espo\Core\ORM\Entity;
use RuntimeException;
use stdClass;
abstract class Base
{
/**
* // Do not rename the parameters. Mapped by name.
*/
public function __construct(
protected Container $container,
protected BpmnManager $manager,
protected Entity $target,
protected BpmnFlowNode $flowNode,
protected BpmnProcess $process,
) {}
protected function getContainer(): Container
{
return $this->container;
}
protected function getLog(): Log
{
return $this->container->getByClass(Log::class);
}
protected function getEntityManager(): EntityManager
{
return $this->container->getByClass(EntityManager::class);
}
protected function getMetadata(): Metadata
{
return $this->container->getByClass(Metadata::class);
}
protected function getFormulaManager(): FormulaManager
{
return $this->getContainer()->getByClass(FormulaManager::class);
}
private function getWebSocketSubmission(): Submission
{
// Important: The service was not bound prior v9.0.0.
/** @var Submission */
return $this->getContainer()->get('webSocketSubmission');
}
private function getConfig(): Config
{
return $this->getContainer()->getByClass(Config::class);
}
protected function getProcess(): BpmnProcess
{
return $this->process;
}
protected function getFlowNode(): BpmnFlowNode
{
return $this->flowNode;
}
protected function getTarget(): Entity
{
return $this->target;
}
protected function getManager(): BpmnManager
{
return $this->manager;
}
protected function refresh(): void
{
$this->refreshFlowNode();
$this->refreshProcess();
$this->refreshTarget();
}
protected function refreshFlowNode(): void
{
if (!$this->flowNode->hasId()) {
return;
}
$flowNode = $this->getEntityManager()->getEntityById(BpmnFlowNode::ENTITY_TYPE, $this->flowNode->getId());
if ($flowNode) {
$this->flowNode->set($flowNode->getValueMap());
$this->flowNode->setAsFetched();
}
}
protected function refreshProcess(): void
{
if (!$this->process->hasId()) {
return;
}
$process = $this->getEntityManager()->getEntityById(BpmnProcess::ENTITY_TYPE, $this->process->getId());
if ($process) {
$this->process->set($process->getValueMap());
$this->process->setAsFetched();
}
}
protected function saveProcess(): void
{
$this->getEntityManager()->saveEntity($this->getProcess(), ['silent' => true]);
}
protected function saveFlowNode(): void
{
$this->getEntityManager()->saveEntity($this->getFlowNode());
$this->submitWebSocket();
}
private function submitWebSocket(): void
{
if (!$this->getConfig()->get('useWebSocket')) {
return;
}
if (!$this->getProcess()->hasId()) {
return;
}
$entityType = $this->getProcess()->getEntityType();
$id = $this->getProcess()->getId();
$topic = "recordUpdate.$entityType.$id";
$this->getWebSocketSubmission()->submit($topic);
}
protected function refreshTarget(): void
{
if (!$this->target->hasId()) {
return;
}
$target = $this->getEntityManager()->getEntityById($this->target->getEntityType(), $this->target->getId());
if ($target) {
$this->target->set($target->getValueMap());
$this->target->setAsFetched();
}
}
public function isProcessable(): bool
{
return true;
}
public function beforeProcess(): void
{}
/**
* @throws Error
*/
abstract public function process(): void;
/**
* @throws Error
*/
public function proceedPending(): void
{
$flowNode = $this->getFlowNode();
throw new Error("BPM Flow: Can't proceed element ". $flowNode->getElementType() . " " .
$flowNode->get('elementId') . " in flowchart " . $flowNode->getFlowchartId() . ".");
}
/**
* @throws Error
*/
protected function getElementId(): string
{
$flowNode = $this->getFlowNode();
$elementId = $flowNode->getElementId();
if (!$elementId) {
throw new Error("BPM Flow: No id for element " . $flowNode->getElementType() .
" in flowchart " . $flowNode->getFlowchartId() . ".");
}
return $elementId;
}
protected function isInNormalFlow(): bool
{
return true;
}
protected function hasNextElementId(): bool
{
$flowNode = $this->getFlowNode();
$item = $flowNode->getElementData();
$nextElementIdList = $item->nextElementIdList;
if (!count($nextElementIdList)) {
return false;
}
return true;
}
protected function getNextElementId(): ?string
{
$flowNode = $this->getFlowNode();
if (!$this->hasNextElementId()) {
return null;
}
$item = $flowNode->getElementData();
$nextElementIdList = $item->nextElementIdList;
return $nextElementIdList[0];
}
/**
* @return mixed
*/
public function getAttributeValue(string $name)
{
$item = $this->getFlowNode()->getElementData();
if (!property_exists($item, $name)) {
return null;
}
return $item->$name;
}
protected function getVariables(): stdClass
{
return $this->getProcess()->getVariables() ?? (object) [];
}
/**
* @todo Revise the need.
*/
protected function getClonedVariables(): stdClass
{
return clone $this->getVariables();
}
protected function getVariablesForFormula(): stdClass
{
$variables = $this->getClonedVariables();
$variables->__createdEntitiesData = $this->getCreatedEntitiesData();
$variables->__processEntity = $this->getProcess();
$variables->__targetEntity = $this->getTarget();
return $variables;
}
protected function addCreatedEntityDataToVariables(stdClass $variables): void
{
$variables->__createdEntitiesData = $this->getCreatedEntitiesData();
}
protected function sanitizeVariables(stdClass $variables): void
{
unset($variables->__createdEntitiesData);
unset($variables->__processEntity);
unset($variables->__targetEntity);
}
protected function setProcessed(): void
{
$this->getFlowNode()->set([
'status' => BpmnFlowNode::STATUS_PROCESSED,
'processedAt' => date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)
]);
$this->saveFlowNode();
}
/**
* @throws Error
*/
protected function setInterrupted(): void
{
$this->getFlowNode()->set([
'status' => BpmnFlowNode::STATUS_INTERRUPTED,
]);
$this->saveFlowNode();
$this->endProcessFlow();
}
/**
* @throws Error
*/
protected function setFailed(): void
{
$this->getFlowNode()->set([
'status' => BpmnFlowNode::STATUS_FAILED,
'processedAt' => date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
]);
$this->saveFlowNode();
$this->endProcessFlow();
}
/**
* @throws Error
*/
protected function setRejected(): void
{
$this->getFlowNode()->set([
'status' => BpmnFlowNode::STATUS_REJECTED,
]);
$this->saveFlowNode();
$this->endProcessFlow();
}
/**
* @throws Error
*/
public function fail(): void
{
$this->setFailed();
}
/**
* @throws Error
*/
public function interrupt(): void
{
$this->setInterrupted();
}
public function cleanupInterrupted(): void
{}
/**
* @throws Error
*/
public function complete(): void
{
throw new Error("Can't complete " . $this->getFlowNode()->getElementType() . ".");
}
/**
* @param string|false|null $divergentFlowNodeId
* @throws Error
*/
protected function prepareNextFlowNode(
?string $nextElementId = null,
$divergentFlowNodeId = false
): ?BpmnFlowNode {
$flowNode = $this->getFlowNode();
if (!$nextElementId) {
if (!$this->isInNormalFlow()) {
return null;
}
if (!$this->hasNextElementId()) {
$this->endProcessFlow();
return null;
}
$nextElementId = $this->getNextElementId();
}
if ($divergentFlowNodeId === false) {
$divergentFlowNodeId = $flowNode->getDivergentFlowNodeId();
}
return $this->getManager()->prepareFlow(
$this->getTarget(),
$this->getProcess(),
$nextElementId,
$flowNode->get('id'),
$flowNode->getElementType(),
$divergentFlowNodeId
);
}
/**
* @param string|false|null $divergentFlowNodeId
* @throws Error
*/
protected function processNextElement(
?string $nextElementId = null,
$divergentFlowNodeId = false,
bool $dontSetProcessed = false
): ?BpmnFlowNode {
$nextFlowNode = $this->prepareNextFlowNode($nextElementId, $divergentFlowNodeId);
if (!$dontSetProcessed) {
$this->setProcessed();
}
if ($nextFlowNode) {
$this->getManager()->processPreparedFlowNode(
$this->getTarget(),
$nextFlowNode,
$this->getProcess()
);
}
return $nextFlowNode;
}
/**
* @throws Error
*/
protected function processPreparedNextFlowNode(BpmnFlowNode $flowNode): void
{
$this->getManager()->processPreparedFlowNode($this->getTarget(), $flowNode, $this->getProcess());
}
/**
* @throws Error
*/
protected function endProcessFlow(): void
{
$this->getManager()->endProcessFlow($this->getFlowNode(), $this->getProcess());
}
protected function getCreatedEntitiesData(): stdClass
{
$createdEntitiesData = $this->getProcess()->get('createdEntitiesData');
if (!$createdEntitiesData) {
$createdEntitiesData = (object) [];
}
return $createdEntitiesData;
}
protected function getCreatedEntity(string $target): ?Entity
{
$createdEntitiesData = $this->getCreatedEntitiesData();
if (str_starts_with($target, 'created:')) {
$alias = substr($target, 8);
} else {
$alias = $target;
}
if (!property_exists($createdEntitiesData, $alias)) {
return null;
}
if (empty($createdEntitiesData->$alias->entityId) || empty($createdEntitiesData->$alias->entityType)) {
return null;
}
$entityType = $createdEntitiesData->$alias->entityType;
$entityId = $createdEntitiesData->$alias->entityId;
$entity = $this->getEntityManager()->getEntityById($entityType, $entityId);
if (!$entity) {
return null;
}
if (!$entity instanceof Entity) {
throw new RuntimeException();
}
return $entity;
}
/**
* @throws FormulaError
* @throws Error
*/
protected function getSpecificTarget(?string $target): ?Entity
{
$entity = $this->getTarget();
if (!$target || $target == 'targetEntity') {
return $entity;
}
if (str_starts_with($target, 'created:')) {
return $this->getCreatedEntity($target);
}
if (str_starts_with($target, 'record:')) {
$entityType = substr($target, 7);
$targetIdExpression = $this->getAttributeValue('targetIdExpression');
if (!$targetIdExpression) {
return null;
}
if (str_ends_with($targetIdExpression, ';')) {
$targetIdExpression = substr($targetIdExpression, 0, -1);
}
$id = $this->getFormulaManager()->run(
$targetIdExpression,
$this->getTarget(),
$this->getVariablesForFormula()
);
if (!$id) {
return null;
}
if (!is_string($id)) {
throw new Error("BPM: Target-ID evaluated not to string.");
}
$entity = $this->getEntityManager()->getEntityById($entityType, $id);
if (!$entity) {
return null;
}
if (!$entity instanceof Entity) {
throw new RuntimeException();
}
return $entity;
}
if (str_starts_with($target, 'link:')) {
$link = substr($target, 5);
$linkList = explode('.', $link);
$pointerEntity = $entity;
$notFound = false;
foreach ($linkList as $link) {
$type = $this->getMetadata()
->get(['entityDefs', $pointerEntity->getEntityType(), 'links', $link, 'type']);
if (empty($type)) {
$notFound = true;
break;
}
$pointerEntity = $this->getEntityManager()
->getRDBRepository($pointerEntity->getEntityType())
->getRelation($pointerEntity, $link)
->findOne();
if (!$pointerEntity instanceof Entity) {
$notFound = true;
break;
}
}
if (!$notFound) {
if ($pointerEntity && !$pointerEntity instanceof Entity) {
throw new RuntimeException();
}
return $pointerEntity;
}
}
return null;
}
}

View File

@@ -0,0 +1,779 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Core\Formula\Exceptions\Error as FormulaException;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\ObjectUtil;
use Espo\Core\Utils\Util;
use Espo\Modules\Advanced\Entities\BpmnFlowchart;
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\Modules\Advanced\Entities\BpmnProcess;
use Espo\ORM\Entity;
use Throwable;
use stdClass;
use Traversable;
class CallActivity extends Activity
{
protected const MAX_INSTANCE_COUNT = 20;
protected const CALLABLE_TYPE_PROCESS = 'Process';
/**
* @throws FormulaException
* @throws Error
*/
public function process(): void
{
if ($this->isMultiInstance()) {
$this->processMultiInstance();
return;
}
$callableType = $this->getAttributeValue('callableType');
if (!$callableType) {
$this->fail();
return;
}
if ($callableType === self::CALLABLE_TYPE_PROCESS) {
$this->processProcess();
return;
}
$this->fail();
}
/**
* @throws FormulaException
* @throws Error
*/
protected function processProcess(): void
{
$target = $this->getNewTargetEntity();
if (!$target) {
$this->getLog()->notice("BPM Call Activity: Could not get target for sub-process.");
$this->fail();
return;
}
$flowchartId = $this->getAttributeValue('flowchartId');
$flowNode = $this->getFlowNode();
$variables = $this->getPrepareVariables();
/** @var BpmnProcess $subProcess */
$subProcess = $this->getEntityManager()->createEntity(BpmnProcess::ENTITY_TYPE, [
'status' => BpmnProcess::STATUS_CREATED,
'flowchartId' => $flowchartId,
'targetId' => $target->getId(),
'targetType' => $target->getEntityType(),
'parentProcessId' => $this->getProcess()->getId(),
'parentProcessFlowNodeId' => $flowNode->getId(),
'rootProcessId' => $this->getProcess()->getRootProcessId(),
'assignedUserId' => $this->getProcess()->getAssignedUser()?->getId(),
'teamsIds' => $this->getProcess()->getTeams()->getIdList(),
'variables' => $variables,
], [
'skipCreatedBy' => true,
'skipModifiedBy' => true,
'skipStartProcessFlow' => true,
]);
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$flowNode->setDataItemValue('subProcessId', $subProcess->getId());
$this->getEntityManager()->saveEntity($flowNode);
try {
$this->getManager()->startCreatedProcess($subProcess);
} catch (Throwable $e) {
$message = "BPM Call Activity: Starting sub-process failure, {$subProcess->getId()}. {$e->getMessage()}";
$this->getLog()->error($message, ['exception' => $e]);
$this->fail();
return;
}
}
public function complete(): void
{
if (!$this->isInNormalFlow()) {
$this->setProcessed();
return;
}
$subProcessId = $this->getFlowNode()->getDataItemValue('subProcessId');
if (!$subProcessId) {
$this->processNextElement();
return;
}
$subProcess = $this->getEntityManager()->getEntityById(BpmnProcess::ENTITY_TYPE, $subProcessId);
if (!$subProcess) {
$this->processNextElement();
return;
}
$spCreatedEntitiesData = $subProcess->get('createdEntitiesData') ?? (object) [];
$createdEntitiesData = $this->getCreatedEntitiesData();
$spVariables = $subProcess->get('variables') ?? (object) [];
$variables = $this->getVariables();
$isUpdated = false;
foreach (get_object_vars($spCreatedEntitiesData) as $key => $value) {
if (!isset($createdEntitiesData->$key)) {
$createdEntitiesData->$key = $value;
$isUpdated = true;
}
}
$variableList = $this->getReturnVariableList();
if ($this->isMultiInstance()) {
$variableList = [];
$returnCollectionVariable = $this->getReturnCollectionVariable();
if ($returnCollectionVariable !== null) {
$variables->$returnCollectionVariable = $spVariables->outputCollection;
}
}
foreach ($variableList as $variable) {
$variables->$variable = $spVariables->$variable ?? null;
}
if (
$isUpdated ||
count($variableList) ||
$this->isMultiInstance()
) {
$this->refreshProcess();
$this->getProcess()->set('createdEntitiesData', $createdEntitiesData);
$this->getProcess()->set('variables', $variables);
$this->getEntityManager()->saveEntity($this->getProcess());
}
$this->processNextElement();
}
protected function getReturnCollectionVariable(): ?string
{
$variable = $this->getAttributeValue('returnCollectionVariable');
if (!$variable) {
return null;
}
if ($variable[0] === '$') {
$variable = substr($variable, 1);
}
return $variable;
}
/**
* @throws FormulaException
* @throws Error
*/
protected function getNewTargetEntity(): ?Entity
{
$target = $this->getAttributeValue('target');
return $this->getSpecificTarget($target);
}
protected function isMultiInstance(): bool
{
return (bool) $this->getAttributeValue('isMultiInstance');
}
protected function isSequential(): bool
{
return (bool) $this->getAttributeValue('isSequential');
}
protected function getLoopCollectionExpression(): ?string
{
$expression = $this->getAttributeValue('loopCollectionExpression');
if (!$expression) {
return null;
}
$expression = trim($expression, " \t\n\r");
if (str_ends_with($expression, ';')) {
$expression = substr($expression, 0, -1);
}
return $expression;
}
protected function getConfig(): Config
{
return $this->getContainer()->getByClass(Config::class);
}
protected function getMaxInstanceCount(): int
{
return $this->getConfig()->get('bpmnSubProcessInstanceMaxCount', self::MAX_INSTANCE_COUNT);
}
/**
* @throws FormulaException
* @throws Error
*/
protected function processMultiInstance(): void
{
$loopCollectionExpression = $this->getLoopCollectionExpression();
if (!$loopCollectionExpression) {
throw new Error("BPM Sub-Process: No loop-collection-expression.");
}
$loopCollection = $this->getFormulaManager()->run(
$loopCollectionExpression,
$this->getTarget(),
$this->getVariablesForFormula()
);
if (!is_iterable($loopCollection)) {
throw new Error("BPM Sub-Process: Loop-collection-expression evaluated to a non-iterable value.");
}
if ($loopCollection instanceof Traversable) {
$loopCollection = iterator_to_array($loopCollection);
}
$maxCount = $this->getMaxInstanceCount();
$returnVariableList = $this->getReturnVariableList();
$outputCollection = [];
for ($i = 0; $i < count($loopCollection); $i++) {
$outputItem = (object) [];
foreach ($returnVariableList as $variable) {
$outputItem->$variable = null;
}
$outputCollection[] = $outputItem;
}
if ($maxCount < count($loopCollection)) {
$loopCollection = array_slice($loopCollection, 0, $maxCount);
}
$count = count($loopCollection);
$flowchart = $this->createMultiInstanceFlowchart($count);
$flowNode = $this->getFlowNode();
$variables = $this->getClonedVariables();
$this->refreshProcess();
$variables->inputCollection = $loopCollection;
$variables->outputCollection = $outputCollection;
/** @var BpmnProcess $subProcess */
$subProcess = $this->getEntityManager()->createEntity(BpmnProcess::ENTITY_TYPE, [
'status' => BpmnFlowNode::STATUS_CREATED,
'targetId' => $this->getTarget()->getId(),
'targetType' => $this->getTarget()->getEntityType(),
'parentProcessId' => $this->getProcess()->getId(),
'parentProcessFlowNodeId' => $flowNode->getId(),
'rootProcessId' => $this->getProcess()->getRootProcessId(),
'assignedUserId' => $this->getProcess()->getAssignedUser()?->getId(),
'teamsIds' => $this->getProcess()->getTeams()->getIdList(),
'variables' => $variables,
'createdEntitiesData' => clone $this->getCreatedEntitiesData(),
],
[
'skipCreatedBy' => true,
'skipModifiedBy' => true,
'skipStartProcessFlow' => true,
]);
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$flowNode->setDataItemValue('subProcessId', $subProcess->getId());
$this->getEntityManager()->saveEntity($flowNode);
try {
$this->getManager()->startCreatedProcess($subProcess, $flowchart);
} catch (Throwable $e) {
$message = "BPM Sub-Process: Starting sub-process failure, {$subProcess->getId()}. {$e->getMessage()}";
$this->getLog()->error($message, ['exception' => $e]);
$this->fail();
return;
}
}
protected function createMultiInstanceFlowchart(int $count): BpmnFlowchart
{
/** @var BpmnFlowchart $flowchart */
$flowchart = $this->getEntityManager()->getNewEntity(BpmnFlowchart::ENTITY_TYPE);
$dataList = $this->isSequential() ?
$this->generateSequentialMultiInstanceDataList($count) :
$this->generateParallelMultiInstanceDataList($count);
$eData = Helper::getElementsDataFromFlowchartData((object) [
'list' => $dataList,
]);
$name = $this->isSequential() ?
'Sequential Multi-Instance' :
'Parallel Multi-Instance';
$flowchart->set([
'targetType' => $this->getTarget()->getEntityType(),
'data' => (object) [
'createdEntitiesData' => clone $this->getCreatedEntitiesData(),
'list' => $dataList,
],
'elementsDataHash' => $eData['elementsDataHash'],
'teamsIds' => $this->getProcess()->getLinkMultipleIdList('teams'),
'assignedUserId' => $this->getProcess()->get('assignedUserId'),
'name' => $name,
]);
return $flowchart;
}
/**
* @return stdClass[]
*/
protected function generateParallelMultiInstanceDataList(int $count): array
{
$dataList = [];
for ($i = 0; $i < $count; $i++) {
$dataList = array_merge($dataList, $this->generateMultiInstanceIteration($i));
}
return array_merge($dataList, $this->generateMultiInstanceCompensation($count, $dataList));
}
/**
* @return stdClass[]
*/
protected function generateSequentialMultiInstanceDataList(int $count): array
{
$dataList = [];
$groupList = [];
for ($i = 0; $i < $count; $i++) {
$groupList[] = $this->generateMultiInstanceIteration($i);
}
foreach ($groupList as $i => $itemList) {
$dataList = array_merge($dataList, $itemList);
if ($i == 0) {
continue;
}
$previousItemList = $groupList[$i - 1];
$dataList[] = (object) [
'type' => 'flow',
'id' => self::generateElementId(),
'startId' => $previousItemList[2]->id,
'endId' => $itemList[0]->id,
'startDirection' => 'r',
];
}
return array_merge($dataList, $this->generateMultiInstanceCompensation(0, $dataList));
}
/**
* @return stdClass[]
*/
protected function generateMultiInstanceIteration(int $loopCounter): array
{
$dataList = [];
$x = 100;
$y = ($loopCounter + 1) * 130;
if ($this->isSequential()) {
$x = $x + ($loopCounter * 400);
$y = 50;
}
$initElement = (object) [
'type' => 'taskScript',
'id' => self::generateElementId(),
'formula' =>
"\$loopCounter = $loopCounter;\n" .
"\$inputItem = array\\at(\$inputCollection, $loopCounter);\n",
'center' => (object) [
'x' => $x,
'y' => $y,
],
'text' => $loopCounter . ' init',
];
$subProcessElement = $this->generateSubProcessMultiInstance($loopCounter, $x, $y);
$endScript = "\$outputItem = array\\at(\$outputCollection, $loopCounter);\n";
foreach ($this->getReturnVariableList() as $variable) {
$endScript .= "object\set(\$outputItem, '$variable', \$$variable);\n";
}
$endElement = (object) [
'type' => 'taskScript',
'id' => self::generateElementId(),
'formula' => $endScript,
'center' => (object) [
'x' => $x + 250,
'y' => $y,
],
'text' => $loopCounter . ' out',
];
$dataList[] = $initElement;
$dataList[] = $subProcessElement;
$dataList[] = $endElement;
$dataList[] = (object) [
'type' => 'flow',
'id' => self::generateElementId(),
'startId' => $initElement->id,
'endId' => $subProcessElement->id,
'startDirection' => 'r',
];
$dataList[] = (object) [
'type' => 'flow',
'id' => self::generateElementId(),
'startId' => $subProcessElement->id,
'endId' => $endElement->id,
'startDirection' => 'r',
];
foreach ($this->generateBoundaryMultiInstance($subProcessElement) as $item) {
$dataList[] = $item;
}
return $dataList;
}
protected function generateSubProcessMultiInstance(int $loopCounter, int $x, int $y): stdClass
{
return (object) [
'type' => $this->getAttributeValue('type'),
'id' => self::generateElementId(),
'center' => (object) [
'x' => $x + 125,
'y' => $y,
],
'callableType' => $this->getAttributeValue('callableType'),
'flowchartId' => $this->getAttributeValue('flowchartId'),
'flowchartName' => $this->getAttributeValue('flowchartName'),
'returnVariableList' => $this->getAttributeValue('returnVariableList'),
'target' => $this->getAttributeValue('target'),
'targetType' => $this->getAttributeValue('targetType'),
'targetIdExpression' => $this->getAttributeValue('targetIdExpression'),
'isMultiInstance' => false,
'isSequential' => false,
'loopCollectionExpression' => null,
'text' => (string) $loopCounter,
];
}
/**
* @return stdClass[]
*/
protected function generateBoundaryMultiInstance(stdClass $element): array
{
$dataList = [];
$attachedElementIdList = array_filter(
$this->getProcess()->getAttachedToFlowNodeElementIdList($this->getFlowNode()),
function (string $id): bool {
$data = $this->getProcess()->getElementDataById($id);
return in_array(
$data->type,
[
'eventIntermediateErrorBoundary',
'eventIntermediateEscalationBoundary',
'eventIntermediateCompensationBoundary',
]
);
}
);
$compensationId = array_values(array_filter(
$this->getProcess()->getElementIdList(),
function (string $id): bool {
$data = $this->getProcess()->getElementDataById($id);
return ($data->isForCompensation ?? null) === true;
}
))[0] ?? null;
foreach ($attachedElementIdList as $i => $id) {
$boundaryElementId = self::generateElementId();
$throwElementId = self::generateElementId();
$originalData = $this->getProcess()->getElementDataById($id);
$o1 = (object) [
'type' => $originalData->type,
'id' => $boundaryElementId,
'attachedToId' => $element->id,
'cancelActivity' => $originalData->cancelActivity ?? false,
'center' => (object) [
'x' => $element->center->x - 20 + $i * 25,
'y' => $element->center->y - 35,
],
'attachPosition' => $originalData->attachPosition,
];
if ($originalData->type === 'eventIntermediateCompensationBoundary') {
if (!$compensationId) {
continue;
}
$dataList[] = $o1;
continue;
}
$o2 = (object) [
'type' => 'eventEndError',
'id' => $throwElementId,
'errorCode' => $originalData->errorCode ?? null,
'center' => (object) [
'x' => $element->center->x - 20 + $i * 25 + 80,
'y' => $element->center->y - 35 - 25,
],
];
if ($originalData->type === 'eventIntermediateErrorBoundary') {
$o2->type = 'eventEndError';
$o1->errorCode = $originalData->errorCode ?? null;
$o2->errorCode = $originalData->errorCode ?? null;
$o1->cancelActivity = true;
}
else if ($originalData->type === 'eventIntermediateEscalationBoundary') {
$o2->type = 'eventEndEscalation';
$o1->escalationCode = $originalData->escalationCode ?? null;
$o2->escalationCode = $originalData->escalationCode ?? null;
}
$dataList[] = $o1;
$dataList[] = $o2;
$dataList[] = (object) [
'type' => 'flow',
'id' => self::generateElementId(),
'startId' => $boundaryElementId,
'endId' => $throwElementId,
'startDirection' => 'r',
];
}
return $dataList;
}
/**
* @param stdClass[] $currentDataList
* @return stdClass[]
*/
private function generateMultiInstanceCompensation(int $loopCounter, array $currentDataList): array
{
$x = 150;
$y = ($loopCounter + 1) * 100 + 100;
$internalDataList = $this->generateMultiInstanceCompensationSubProcessDataList();
$dataList = [];
$dataList[] = (object) [
'type' => 'eventSubProcess',
'id' => self::generateElementId(),
'center' => (object) [
'x' => $x,
'y' => $y,
],
'height' => 100,
'width' => 205,
'target' => null,
'isExpanded' => true,
'dataList' => $internalDataList,
'eventStartData' => clone $internalDataList[0],
];
$activity = $this->generateMultiInstanceBoundaryCompensationActivity();
if ($activity) {
$activity->center = (object) [
'x' => 350,
'y' => $y,
];
$dataList[] = $activity;
foreach ($currentDataList as $item) {
if ($item->type === 'eventIntermediateCompensationBoundary') {
$dataList[] = (object) [
'type' => 'flow',
'id' => self::generateElementId(),
'startId' => $item->id,
'endId' => $activity->id,
'startDirection' => 'd',
'isAssociation' => true,
];
}
}
}
return $dataList;
}
/**
* @return stdClass[]
*/
private function generateMultiInstanceCompensationSubProcessDataList(): array
{
$x = 50;
$y = 35;
$dataList = [];
$dataList[] = (object) [
'type' => 'eventStartCompensation',
'id' => self::generateElementId(),
'center' => (object) [
'x' => $x,
'y' => $y,
],
];
$dataList[] = (object) [
'type' => 'eventEndCompensation',
'id' => self::generateElementId(),
'center' => (object) [
'x' => $x + 100,
'y' => $y,
],
'activityId' => null,
];
$dataList[] = (object) [
'type' => 'flow',
'id' => self::generateElementId(),
'startId' => $dataList[0]->id,
'endId' => $dataList[1]->id,
'startDirection' => 'r',
];
return $dataList;
}
private function generateMultiInstanceBoundaryCompensationActivity(): ?stdClass
{
/** @var string[] $attachedElementIdList */
$attachedElementIdList = array_values(array_filter(
$this->getProcess()->getAttachedToFlowNodeElementIdList($this->getFlowNode()),
function (string $id): bool {
$data = $this->getProcess()->getElementDataById($id);
return $data->type === 'eventIntermediateCompensationBoundary';
}
));
if ($attachedElementIdList === []) {
return null;
}
$boundaryId = $attachedElementIdList[0];
$compensationId = $this->getProcess()->getElementNextIdList($boundaryId)[0] ?? null;
if (!$compensationId) {
return null;
}
$item = $this->getProcess()->getElementDataById($compensationId);
if (!$item) {
return null;
}
$item = ObjectUtil::clone($item);
$item->id = $this->generateElementId();
if ($item->type === 'subProcess') {
$item->isExpanded = false;
}
return $item;
}
protected static function generateElementId(): string
{
return Util::generateId();
}
protected function getPrepareVariables(): stdClass
{
$variables = $this->getClonedVariables();
unset($variables->__caughtErrorCode);
unset($variables->__caughtErrorMessage);
return $variables;
}
}

View File

@@ -0,0 +1,49 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
abstract class Event extends Base
{
protected function rejectConcurrentPendingFlows(): void
{
$flowNode = $this->getFlowNode();
if ($flowNode->getPreviousFlowNodeElementType() === 'gatewayEventBased') {
/** @var iterable<BpmnFlowNode> $concurrentFlowNodeList */
$concurrentFlowNodeList = $this->getEntityManager()
->getRDBRepository(BpmnFlowNode::ENTITY_TYPE)
->where([
'previousFlowNodeElementType' => 'gatewayEventBased',
'previousFlowNodeId' => $flowNode->getPreviousFlowNodeId(),
'processId' => $flowNode->getProcessId(),
'id!=' => $flowNode->get('id'),
'status' => BpmnFlowNode::STATUS_PENDING,
])
->find();
foreach ($concurrentFlowNodeList as $concurrentFlowNode) {
$concurrentFlowNode->setStatus(BpmnFlowNode::STATUS_REJECTED);
$this->getEntityManager()->saveEntity($concurrentFlowNode);
}
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
/**
* @noinspection PhpUnused
*/
class EventEnd extends Base
{
public function process(): void
{
$this->setProcessed();
$this->endProcessFlow();
}
}

View File

@@ -0,0 +1,111 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\Modules\Advanced\Entities\BpmnProcess;
class EventEndCompensation extends Base
{
public function process(): void
{
/** @var ?string $activityId */
$activityId = $this->getAttributeValue('activityId');
$process = $this->getProcess();
if (
$this->getProcess()->getParentProcessFlowNodeId() &&
$this->getProcess()->getParentProcessId()
) {
/** @var ?BpmnFlowNode $parentNode */
$parentNode = $this->getEntityManager()
->getEntityById(BpmnFlowNode::ENTITY_TYPE, $this->getProcess()->getParentProcessFlowNodeId());
if (!$parentNode) {
throw new Error("No parent node.");
}
if ($parentNode->getElementType() === 'eventSubProcess') {
/** @var ?BpmnProcess $process */
$process = $this->getEntityManager()
->getEntityById(BpmnProcess::ENTITY_TYPE, $this->getProcess()->getParentProcessId());
if (!$process) {
throw new Error("No parent process.");
}
}
}
$compensationFlowNodeIdList = $this->getManager()->compensate($process, $activityId);
$this->getFlowNode()->setDataItemValue('compensationFlowNodeIdList', $compensationFlowNodeIdList);
$this->saveFlowNode();
$this->refreshProcess();
$this->refreshTarget();
if ($compensationFlowNodeIdList === [] || $this->isCompensated()) {
$this->setProcessed();
$this->processAfterProcessed();
return;
}
$this->getFlowNode()->set('status', BpmnFlowNode::STATUS_PENDING);
$this->saveFlowNode();
}
protected function processAfterProcessed(): void
{
$this->endProcessFlow();
}
public function proceedPending(): void
{
if (!$this->isCompensated()) {
return;
}
$this->setProcessed();
$this->processAfterProcessed();
}
private function isCompensated(): bool
{
/** @var string[] $compensationFlowNodeIdList */
$compensationFlowNodeIdList = $this->getFlowNode()->getDataItemValue('compensationFlowNodeIdList') ?? [];
$flowNodes = $this->getEntityManager()
->getRDBRepositoryByClass(BpmnFlowNode::class)
->where(['id' => $compensationFlowNodeIdList])
->find();
foreach ($flowNodes as $flowNode) {
if ($flowNode->getStatus() === BpmnFlowNode::STATUS_PROCESSED) {
continue;
}
return false;
}
return true;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventEndError extends Base
{
public function process(): void
{
$this->setProcessed();
$this->getManager()->endProcessWithError($this->getProcess(), $this->getAttributeValue('errorCode'));
}
}

View File

@@ -0,0 +1,34 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventEndEscalation extends Base
{
public function process(): void
{
$this->setProcessed();
$this->getManager()->escalate($this->getProcess(), $this->getAttributeValue('escalationCode'));
$this->refreshProcess();
$this->refreshTarget();
$this->endProcessFlow();
}
}

View File

@@ -0,0 +1,50 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventEndSignal extends EventSignal
{
public function process(): void
{
$this->setProcessed();
$signal = $this->getSignal();
if ($signal) {
if (mb_substr($signal, 0, 1) !== '@') {
$this->getManager()->broadcastSignal($signal);
}
} else {
$this->getLog()->warning("BPM: eventEndSignal, no signal");
}
$this->refreshProcess();
$this->refreshTarget();
$this->endProcessFlow();
if ($signal) {
if (mb_substr($signal, 0, 1) !== '@') {
$this->getSignalManager()->trigger($signal);
} else {
$this->getSignalManager()->trigger($signal, $this->getTarget());
}
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventEndTerminate extends Base
{
public function process(): void
{
$this->setProcessed();
$this->getManager()->endProcess($this->getProcess(), true);
}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventIntermediateBoundary extends Event
{
public function process(): void
{
$this->processNextElement();
}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventIntermediateCompensationThrow extends EventEndCompensation
{
protected function processAfterProcessed(): void
{
$this->processNextElement();
}
}

View File

@@ -0,0 +1,120 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
/**
* @noinspection PhpUnused
*/
class EventIntermediateConditionalBoundary extends EventIntermediateConditionalCatch
{
public function process(): void
{
$result = $this->getConditionManager()->check(
$this->getTarget(),
$this->getAttributeValue('conditionsAll'),
$this->getAttributeValue('conditionsAny'),
$this->getAttributeValue('conditionsFormula'),
$this->getVariablesForFormula()
);
if ($result) {
$cancel = $this->getAttributeValue('cancelActivity');
if (!$cancel) {
$this->createOppositeNode();
}
$this->processNextElement();
if ($cancel) {
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
}
return;
}
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
$this->getEntityManager()->saveEntity($flowNode);
}
public function proceedPending(): void
{
$result = $this->getConditionManager()->check(
$this->getTarget(),
$this->getAttributeValue('conditionsAll'),
$this->getAttributeValue('conditionsAny'),
$this->getAttributeValue('conditionsFormula'),
$this->getVariablesForFormula()
);
if ($this->getFlowNode()->getDataItemValue('isOpposite')) {
if (!$result) {
$this->setProcessed();
$this->createOppositeNode(true);
}
return;
}
if ($result) {
$cancel = $this->getAttributeValue('cancelActivity');
if (!$cancel) {
$this->createOppositeNode();
}
$this->processNextElement();
if ($cancel) {
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
}
}
}
protected function createOppositeNode(bool $isNegative = false): void
{
/** @var BpmnFlowNode $flowNode */
$flowNode = $this->getEntityManager()->getNewEntity(BpmnFlowNode::ENTITY_TYPE);
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
$flowNode->set([
'elementId' => $this->getFlowNode()->getElementId(),
'elementType' => $this->getFlowNode()->getElementType(),
'elementData' => $this->getFlowNode()->getElementData(),
'data' => [
'isOpposite' => !$isNegative,
],
'flowchartId' => $this->getProcess()->getFlowchartId(),
'processId' => $this->getProcess()->getId(),
'previousFlowNodeElementType' => $this->getFlowNode()->getPreviousFlowNodeElementType(),
'previousFlowNodeId' => $this->getFlowNode()->getPreviousFlowNodeId(),
'divergentFlowNodeId' => $this->getFlowNode()->getDivergentFlowNodeId(),
'targetType' => $this->getFlowNode()->getTargetType(),
'targetId' => $this->getFlowNode()->getTargetId(),
]);
$this->getEntityManager()->saveEntity($flowNode);
}
}

View File

@@ -0,0 +1,113 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\InjectableFactory;
use Espo\Modules\Advanced\Core\Bpmn\Utils\ConditionManager;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\ORM\Entity;
class EventIntermediateConditionalCatch extends Event
{
/** @var string */
protected $pendingStatus = BpmnFlowNode::STATUS_PENDING;
/**
* @throws Error
* @throws \Espo\Core\Exceptions\Error
*/
public function process(): void
{
$target = $this->getConditionsTarget();
if (!$target) {
$this->fail();
return;
}
$result = $this->getConditionManager()->check(
$target,
$this->getAttributeValue('conditionsAll'),
$this->getAttributeValue('conditionsAny'),
$this->getAttributeValue('conditionsFormula'),
$this->getVariablesForFormula()
);
if ($result) {
$this->rejectConcurrentPendingFlows();
$this->processNextElement();
return;
}
$flowNode = $this->getFlowNode();
$flowNode->set([
'status' => $this->pendingStatus,
]);
$this->getEntityManager()->saveEntity($flowNode);
}
/**
* @throws Error
* @throws \Espo\Core\Exceptions\Error
*/
public function proceedPending(): void
{
$target = $this->getConditionsTarget();
if (!$target) {
$this->fail();
return;
}
$result = $this->getConditionManager()->check(
$target,
$this->getAttributeValue('conditionsAll'),
$this->getAttributeValue('conditionsAny'),
$this->getAttributeValue('conditionsFormula'),
$this->getVariablesForFormula()
);
if ($result) {
$this->rejectConcurrentPendingFlows();
$this->processNextElement();
}
}
protected function getConditionsTarget(): ?Entity
{
return $this->getTarget();
}
protected function getConditionManager(): ConditionManager
{
$conditionManager = $this->getContainer()
->getByClass(InjectableFactory::class)
->create(ConditionManager::class);
$conditionManager->setCreatedEntitiesData($this->getCreatedEntitiesData());
return $conditionManager;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventIntermediateErrorBoundary extends EventIntermediateBoundary
{
public function process(): void
{
$this->storeErrorVariables();
$this->processNextElement();
}
private function storeErrorVariables(): void
{
$variables = $this->getVariables();
$variables->__caughtErrorCode = $this->getFlowNode()->getDataItemValue('code');
$variables->__caughtErrorMessage = $this->getFlowNode()->getDataItemValue('message');
$this->getProcess()->setVariables($variables);
$this->saveProcess();
}
}

View File

@@ -0,0 +1,36 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
/**
* @noinspection PhpUnused
*/
class EventIntermediateEscalationBoundary extends Event
{
public function process(): void
{
$this->setProcessed();
$this->processNextElement();
if ($this->getAttributeValue('cancelActivity')) {
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventIntermediateEscalationThrow extends Event
{
public function process(): void
{
$this->getManager()->escalate($this->getProcess(), $this->getAttributeValue('escalationCode'));
$this->refreshProcess();
$this->refreshTarget();
$this->processNextElement();
}
}

View File

@@ -0,0 +1,65 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
/**
* @noinspection PhpUnused
*/
class EventIntermediateMessageBoundary extends EventIntermediateMessageCatch
{
protected function proceedPendingFinal(): void
{
$cancel = $this->getAttributeValue('cancelActivity');
if (!$cancel) {
$this->createCopy();
}
$this->processNextElement();
if ($cancel) {
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
}
}
protected function createCopy(): void
{
/** @var BpmnFlowNode $flowNode */
$flowNode = $this->getEntityManager()->getNewEntity(BpmnFlowNode::ENTITY_TYPE);
$flowNode->set([
'status' => BpmnFlowNode::STATUS_PENDING,
'elementId' => $this->getFlowNode()->getElementId(),
'elementType' => $this->getFlowNode()->getElementType(),
'elementData' => $this->getFlowNode()->getElementData(),
'data' => (object) [],
'flowchartId' => $this->getProcess()->getFlowchartId(),
'processId' => $this->getProcess()->get('id'),
'previousFlowNodeElementType' => $this->getFlowNode()->getPreviousFlowNodeElementType(),
'previousFlowNodeId' => $this->getFlowNode()->getPreviousFlowNodeId(),
'divergentFlowNodeId' => $this->getFlowNode()->getDivergentFlowNodeId(),
'targetType' => $this->getFlowNode()->getTargetType(),
'targetId' => $this->getFlowNode()->getTargetId(),
]);
$this->getEntityManager()->saveEntity($flowNode);
}
}

View File

@@ -0,0 +1,208 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Core\Formula\Exceptions\Error as FormulaError;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\Entities\Email;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
class EventIntermediateMessageCatch extends Event
{
public function process(): void
{
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
$this->getEntityManager()->saveEntity($flowNode);
}
/**
* @throws FormulaError
* @throws Error
*/
public function proceedPending(): void
{
$repliedToAliasId = $this->getAttributeValue('repliedTo');
$messageType = $this->getAttributeValue('messageType') ?? 'Email';
$relatedTo = $this->getAttributeValue('relatedTo');
$conditionsFormula = $this->getAttributeValue('conditionsFormula');
$conditionsFormula = trim($conditionsFormula, " \t\n\r");
if (strlen($conditionsFormula) && str_ends_with($conditionsFormula, ';')) {
$conditionsFormula = substr($conditionsFormula, 0, -1);
}
$target = $this->getTarget();
$createdEntitiesData = $this->getCreatedEntitiesData();
$repliedToId = null;
if ($repliedToAliasId) {
if (!isset($createdEntitiesData->$repliedToAliasId)) {
return;
}
$repliedToId = $createdEntitiesData->$repliedToAliasId->entityId ?? null;
$repliedToType = $createdEntitiesData->$repliedToAliasId->entityType ?? null;
if (!$repliedToId || $messageType !== $repliedToType) {
$this->fail();
return;
}
}
$flowNode = $this->getFlowNode();
if ($messageType === 'Email') {
$from = $flowNode->getDataItemValue('checkedAt') ?? $flowNode->get('createdAt');
$whereClause = [
'createdAt>=' => $from,
'status' => Email::STATUS_ARCHIVED,
'dateSent>=' => $flowNode->get('createdAt'),
[
'OR' => [
'sentById' => null,
'sentBy.type' => 'portal', // @todo Change to const.
]
],
];
if ($repliedToId) {
$whereClause['repliedId'] = $repliedToId;
} else if ($relatedTo) {
$relatedTarget = $this->getSpecificTarget($relatedTo);
if (!$relatedTarget) {
$this->updateCheckedAt();
return;
}
if ($relatedTarget->getEntityType() === 'Account') {
$whereClause['accountId'] = $relatedTarget->getId();
} else {
$whereClause['parentId'] = $relatedTarget->getId();
$whereClause['parentType'] = $relatedTarget->getEntityType();
}
}
if (!$repliedToId && !$relatedTo) {
if ($target->getEntityType() === 'Contact' && $target->get('accountId')) {
$whereClause[] = [
'OR' => [
[
'parentType' => 'Contact',
'parentId' => $target->getId(),
],
[
'parentType' => 'Account',
'parentId' => $target->get('accountId'),
],
]
];
}
else if ($target->getEntityType() === 'Account') {
$whereClause['accountId'] = $target->getId();
}
else {
$whereClause['parentId'] = $target->getId();
$whereClause['parentType'] = $target->getEntityType();
}
}
/** @var Config $config */
$config = $this->getContainer()->get('config');
$limit = $config->get('bpmnMessageCatchLimit', 50);
$emailList = $this->getEntityManager()
->getRDBRepository(Email::ENTITY_TYPE)
->leftJoin('sentBy')
->where($whereClause)
->limit(0, $limit)
->find();
if (!count($emailList)) {
$this->updateCheckedAt();
return;
}
if ($conditionsFormula) {
$isFound = false;
foreach ($emailList as $email) {
$formulaResult = $this->getFormulaManager()
->run($conditionsFormula, $email, $this->getVariablesForFormula());
if ($formulaResult) {
$isFound = true;
break;
}
}
if (!$isFound) {
$this->updateCheckedAt();
return;
}
}
}
else {
$this->fail();
return;
}
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$this->getEntityManager()->saveEntity($flowNode);
$this->proceedPendingFinal();
}
/**
* @throws Error
*/
protected function proceedPendingFinal(): void
{
$this->rejectConcurrentPendingFlows();
$this->processNextElement();
}
protected function updateCheckedAt(): void
{
$flowNode = $this->getFlowNode();
$flowNode->setDataItemValue('checkedAt', date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT));
$this->getEntityManager()->saveEntity($flowNode);
}
}

View File

@@ -0,0 +1,95 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
/**
* @noinspection PhpUnused
*/
class EventIntermediateSignalBoundary extends EventSignal
{
public function process(): void
{
$signal = $this->getSignal();
if (!$signal) {
$this->fail();
$this->getLog()->warning("BPM: No signal for sub-process EventIntermediateSignalBoundary");
return;
}
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
$this->getEntityManager()->saveEntity($flowNode);
$this->getSignalManager()->subscribe($signal, $flowNode->getId());
}
public function proceedPending(): void
{
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$this->getEntityManager()->saveEntity($flowNode);
$cancel = $this->getAttributeValue('cancelActivity');
if (!$cancel) {
$this->createCopy();
}
$this->processNextElement();
if ($cancel) {
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
}
}
protected function createCopy(): void
{
$data = $this->getFlowNode()->getData();
$data = clone $data;
/** @var BpmnFlowNode $flowNode */
$flowNode = $this->getEntityManager()->getNewEntity(BpmnFlowNode::ENTITY_TYPE);
$flowNode->set([
'status' => BpmnFlowNode::STATUS_PENDING,
'elementId' => $this->getFlowNode()->getElementId(),
'elementType' => $this->getFlowNode()->getElementType(),
'elementData' => $this->getFlowNode()->getElementData(),
'data' => $data,
'flowchartId' => $this->getProcess()->getFlowchartId(),
'processId' => $this->getProcess()->getId(),
'previousFlowNodeElementType' => $this->getFlowNode()->getPreviousFlowNodeElementType(),
'previousFlowNodeId' => $this->getFlowNode()->getPreviousFlowNodeId(),
'divergentFlowNodeId' => $this->getFlowNode()->getDivergentFlowNodeId(),
'targetType' => $this->getFlowNode()->getTargetType(),
'targetId' => $this->getFlowNode()->getTargetId(),
]);
$this->getEntityManager()->saveEntity($flowNode);
$this->getSignalManager()->subscribe($this->getSignal(), $flowNode->getId());
}
}

View File

@@ -0,0 +1,57 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
class EventIntermediateSignalCatch extends EventSignal
{
public function process(): void
{
$signal = $this->getSignal();
if (!$signal) {
$this->fail();
$this->getLog()->warning("BPM: No signal for EventIntermediateSignalCatch");
return;
}
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
$this->getEntityManager()->saveEntity($flowNode);
$this->getSignalManager()->subscribe($signal, $flowNode->getId());
}
public function proceedPending(): void
{
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$this->getEntityManager()->saveEntity($flowNode);
$this->rejectConcurrentPendingFlows();
$this->processNextElement();
}
}

View File

@@ -0,0 +1,54 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventIntermediateSignalThrow extends EventSignal
{
public function process(): void
{
$nextFlowNode = $this->prepareNextFlowNode();
$this->setProcessed();
$signal = $this->getSignal();
if ($signal) {
if (mb_substr($signal, 0, 1) !== '@') {
$this->getManager()->broadcastSignal($signal);
}
} else {
$this->getLog()->warning("BPM: eventIntermediateSignalThrow, no signal");
}
$this->refreshProcess();
$this->refreshTarget();
if ($nextFlowNode) {
$this->processPreparedNextFlowNode($nextFlowNode);
}
if ($signal) {
if (mb_substr($signal, 0, 1) !== '@') {
$this->getSignalManager()->trigger($signal);
} else {
$this->getSignalManager()->trigger($signal, $this->getTarget());
}
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
/**
* @noinspection PhpUnused
*/
class EventIntermediateTimerBoundary extends EventIntermediateTimerCatch
{
public function proceedPending(): void
{
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$this->getEntityManager()->saveEntity($flowNode);
$this->processNextElement();
if ($this->getAttributeValue('cancelActivity')) {
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
}
}
}

View File

@@ -0,0 +1,168 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Exception;
use DateTime;
class EventIntermediateTimerCatch extends Event
{
/** @var string */
protected $pendingStatus = BpmnFlowNode::STATUS_PENDING;
public function process(): void
{
$timerBase = $this->getAttributeValue('timerBase');
if (!$timerBase || $timerBase === 'moment') {
$dt = new DateTime();
$this->shiftDateTime($dt);
} else if ($timerBase === 'formula') {
$timerFormula = $this->getAttributeValue('timerFormula');
$formulaManager = $this->getFormulaManager();
if (!$timerFormula) {
$this->setFailed();
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
}
$value = $formulaManager->run($timerFormula, $this->getTarget(), $this->getVariablesForFormula());
if (!$value || !is_string($value)) {
$this->setFailed();
throw new Error();
}
try {
$dt = new DateTime($value);
} catch (Exception) {
$this->setFailed();
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
}
}
else if (str_starts_with($timerBase, 'field:')) {
$field = substr($timerBase, 6);
$entity = $this->getTarget();
if (strpos($field, '.') > 0) {
[$link, $field] = explode('.', $field);
$target = $this->getTarget();
$entity = $this->getEntityManager()
->getRDBRepository($target->getEntityType())
->getRelation($target, $link)
->findOne();
if (!$entity) {
$this->setFailed();
throw new Error("Bpmn Flow: EventIntermediateTimer. Related entity doesn't exist.");
}
}
$value = $entity->get($field);
if (!$value || !is_string($value)) {
$this->setFailed();
throw new Error('Bpmn Flow: EventIntermediateTimer.');
}
try {
$dt = new DateTime($value);
}
catch (Exception) {
$this->setFailed();
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
}
$this->shiftDateTime($dt);
}
else {
$this->setFailed();
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
}
$flowNode = $this->getFlowNode();
$flowNode->set([
'status' => $this->pendingStatus,
'proceedAt' => $dt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)
]);
$this->getEntityManager()->saveEntity($flowNode);
}
/**
* @throws Error
*/
protected function shiftDateTime(DateTime $dt): void
{
$timerShiftOperator = $this->getAttributeValue('timerShiftOperator');
$timerShift = $this->getAttributeValue('timerShift');
$timerShiftUnits = $this->getAttributeValue('timerShiftUnits');
if (!in_array($timerShiftUnits, ['minutes', 'hours', 'days', 'months', 'seconds'])) {
$flowNode = $this->getFlowNode();
$this->setFailed();
throw new Error("Bpmn Flow: Bad shift in ". $flowNode->get('elementType') . " " .
$flowNode->get('elementId') . " in flowchart " . $flowNode->get('flowchartId') . ".");
}
if ($timerShift) {
$modifyString = $timerShift . ' ' . $timerShiftUnits;
if ($timerShiftOperator === 'minus') {
$modifyString = '-' . $modifyString;
}
try {
$dt->modify($modifyString);
/** @phpstan-ignore-next-line */
} catch (Exception) {
$this->setFailed();
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
}
}
}
public function proceedPending(): void
{
$this->getFlowNode()->set('status', BpmnFlowNode::STATUS_IN_PROCESS);
$this->saveFlowNode();
$this->rejectConcurrentPendingFlows();
$this->processNextElement();
}
}

View File

@@ -0,0 +1,41 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
use Espo\Modules\Advanced\Core\SignalManager;
abstract class EventSignal extends Event
{
protected function getSignalManager(): SignalManager
{
return $this->getContainer()->getByClass(SignalManager::class);
}
protected function getSignal(): ?string
{
$name = $this->getAttributeValue('signal');
if (!$name || !is_string($name)) {
return null;
}
return Helper::applyPlaceholders($name, $this->getTarget(), $this->getVariables());
}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventStart extends Base
{
public function process(): void
{
$this->processNextElement();
}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventStartCompensation extends Base
{
public function process(): void
{
$this->processNextElement();
}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventStartConditional extends Base
{
public function process(): void
{
$this->processNextElement();
}
}

View File

@@ -0,0 +1,144 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\ORM\Entity;
class EventStartConditionalEventSubProcess extends EventIntermediateConditionalCatch
{
/** @var string */
protected $pendingStatus = BpmnFlowNode::STATUS_STANDBY;
/**
* @throws Error
* @throws \Espo\Core\Exceptions\Error
*/
protected function getConditionsTarget(): ?Entity
{
return $this->getSpecificTarget($this->getFlowNode()->getDataItemValue('subProcessTarget'));
}
/**
* @param string|bool|null $divergentFlowNodeId
*/
protected function processNextElement(
?string $nextElementId = null,
$divergentFlowNodeId = false,
bool $dontSetProcessed = false
): ?BpmnFlowNode {
return parent::processNextElement($this->getFlowNode()->getDataItemValue('subProcessElementId'));
}
public function process(): void
{
$target = $this->getConditionsTarget();
if (!$target) {
$this->fail();
return;
}
$result = $this->getConditionManager()->check(
$target,
$this->getAttributeValue('conditionsAll'),
$this->getAttributeValue('conditionsAny'),
$this->getAttributeValue('conditionsFormula'),
$this->getVariablesForFormula()
);
if ($result) {
$subProcessIsInterrupting = $this->getFlowNode()->getDataItemValue('subProcessIsInterrupting');
if (!$subProcessIsInterrupting) {
$this->createOppositeNode();
}
if ($subProcessIsInterrupting) {
$this->getManager()->interruptProcessByEventSubProcess($this->getProcess(), $this->getFlowNode());
}
$this->processNextElement();
return;
}
$this->getFlowNode()->set(['status' => $this->pendingStatus]);
$this->saveFlowNode();
}
public function proceedPending(): void
{
$result = $this->getConditionManager()->check(
$this->getTarget(),
$this->getAttributeValue('conditionsAll'),
$this->getAttributeValue('conditionsAny'),
$this->getAttributeValue('conditionsFormula'),
$this->getVariablesForFormula()
);
if ($this->getFlowNode()->getDataItemValue('isOpposite')) {
if (!$result) {
$this->setProcessed();
$this->createOppositeNode(true);
}
return;
}
if ($result) {
$subProcessIsInterrupting = $this->getFlowNode()->getDataItemValue('subProcessIsInterrupting');
if (!$subProcessIsInterrupting) {
$this->createOppositeNode();
}
if ($subProcessIsInterrupting) {
$this->getManager()->interruptProcessByEventSubProcess($this->getProcess(), $this->getFlowNode());
}
$this->processNextElement();
}
}
protected function createOppositeNode(bool $isNegative = false): void
{
$data = $this->getFlowNode()->get('data') ?? (object) [];
$data = clone $data;
$data->isOpposite = !$isNegative;
$flowNode = $this->getEntityManager()->getEntity(BpmnFlowNode::ENTITY_TYPE);
$flowNode->set([
'status' => BpmnFlowNode::STATUS_STANDBY,
'elementType' => $this->getFlowNode()->get('elementType'),
'elementData' => $this->getFlowNode()->get('elementData'),
'data' => $data,
'flowchartId' => $this->getProcess()->get('flowchartId'),
'processId' => $this->getProcess()->get('id'),
'targetType' => $this->getFlowNode()->get('targetType'),
'targetId' => $this->getFlowNode()->get('targetId'),
]);
$this->getEntityManager()->saveEntity($flowNode);
}
}

View File

@@ -0,0 +1,68 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
class EventStartError extends Base
{
public function process(): void
{
$this->writeErrorData();
$this->storeErrorVariables();
$this->processNextElement();
}
private function writeErrorData(): void
{
$flowNode = $this->getFlowNode();
$parentFlowNodeId = $this->getProcess()->getParentProcessFlowNodeId();
if (!$parentFlowNodeId) {
return;
}
/** @var ?BpmnFlowNode $parentFlowNode */
$parentFlowNode = $this->getEntityManager()->getEntityById(BpmnFlowNode::ENTITY_TYPE, $parentFlowNodeId);
if (!$parentFlowNode) {
return;
}
$code = $parentFlowNode->getDataItemValue('caughtErrorCode');
$message = $parentFlowNode->getDataItemValue('caughtErrorMessage');
$flowNode->setDataItemValue('code', $code);
$flowNode->setDataItemValue('message', $message);
$this->getEntityManager()->saveEntity($flowNode);
}
private function storeErrorVariables(): void
{
$variables = $this->getVariables();
$variables->__caughtErrorCode = $this->getFlowNode()->getDataItemValue('code');
$variables->__caughtErrorMessage = $this->getFlowNode()->getDataItemValue('message');
$this->getProcess()->setVariables($variables);
$this->saveProcess();
}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventStartEscalation extends Base
{
public function process(): void
{
$this->processNextElement();
}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventStartSignal extends Base
{
public function process(): void
{
$this->processNextElement();
}
}

View File

@@ -0,0 +1,118 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
use Espo\Modules\Advanced\Core\SignalManager;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
/**
* @noinspection PhpUnused
*/
class EventStartSignalEventSubProcess extends Event
{
/**
* @param string|bool|null $divergentFlowNodeId
*/
protected function processNextElement(
?string $nextElementId = null,
$divergentFlowNodeId = false,
bool $dontSetProcessed = false
): ?BpmnFlowNode {
return parent::processNextElement($this->getFlowNode()->getDataItemValue('subProcessElementId'));
}
public function process(): void
{
$signal = $this->getSignal();
if (!$signal) {
$this->fail();
$this->getLog()->warning("BPM: No signal for sub-process start event.");
return;
}
$flowNode = $this->getFlowNode();
$flowNode->set([
'status' => BpmnFlowNode::STATUS_STANDBY,
]);
$this->getEntityManager()->saveEntity($flowNode);
$this->getSignalManager()->subscribe($signal, $flowNode->get('id'));
}
public function proceedPending(): void
{
$subProcessIsInterrupting = $this->getFlowNode()->getDataItemValue('subProcessIsInterrupting');
if (!$subProcessIsInterrupting) {
$this->createCopy();
}
if ($subProcessIsInterrupting) {
$this->getManager()->interruptProcessByEventSubProcess($this->getProcess(), $this->getFlowNode());
}
$this->processNextElement();
}
protected function createCopy(): void
{
$data = $this->getFlowNode()->get('data') ?? (object) [];
$data = clone $data;
$flowNode = $this->getEntityManager()->getEntity(BpmnFlowNode::ENTITY_TYPE);
$flowNode->set([
'status' => BpmnFlowNode::STATUS_STANDBY,
'elementType' => $this->getFlowNode()->getElementType(),
'elementData' => $this->getFlowNode()->get('elementData'),
'data' => $data,
'flowchartId' => $this->getProcess()->getFlowchartId(),
'processId' => $this->getProcess()->get('id'),
'targetType' => $this->getFlowNode()->getTargetType(),
'targetId' => $this->getFlowNode()->getTargetId(),
]);
$this->getEntityManager()->saveEntity($flowNode);
$this->getSignalManager()->subscribe($this->getSignal(), $flowNode->get('id'));
}
protected function getSignal(): ?string
{
$subProcessStartData = $this->getFlowNode()->getDataItemValue('subProcessStartData') ?? (object) [];
$name = $subProcessStartData->signal ?? null;
if (!$name || !is_string($name)) {
return null;
}
return Helper::applyPlaceholders($name, $this->getTarget(), $this->getVariables());
}
protected function getSignalManager(): SignalManager
{
return $this->getContainer()->getByClass(SignalManager::class);
}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
class EventStartTimer extends Base
{
public function process(): void
{
$this->processNextElement();
}
}

View File

@@ -0,0 +1,88 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Core\Formula\Exceptions\Error as FormulaError;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\ORM\Entity;
/**
* @noinspection PhpUnused
*/
class EventStartTimerEventSubProcess extends EventIntermediateTimerCatch
{
/** @var string */
protected $pendingStatus = BpmnFlowNode::STATUS_STANDBY;
/**
* @throws FormulaError
* @throws Error
*/
protected function getConditionsTarget(): ?Entity
{
return $this->getSpecificTarget($this->getFlowNode()->getDataItemValue('subProcessTarget'));
}
/**
* @param string|bool|null $divergentFlowNodeId
* @throws Error
*/
protected function processNextElement(
?string $nextElementId = null,
$divergentFlowNodeId = false,
bool $dontSetProcessed = false
): ?BpmnFlowNode {
return parent::processNextElement($this->getFlowNode()->getDataItemValue('subProcessElementId'));
}
public function proceedPending(): void
{
$flowNode = $this->getFlowNode();
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$this->getEntityManager()->saveEntity($flowNode);
$subProcessIsInterrupting = $this->getFlowNode()->getDataItemValue('subProcessIsInterrupting');
if (!$subProcessIsInterrupting) {
$standbyFlowNode = $this->getManager()->prepareStandbyFlow(
$this->getTarget(),
$this->getProcess(),
$this->getFlowNode()->getDataItemValue('subProcessElementId')
);
if ($standbyFlowNode) {
$this->getManager()->processPreparedFlowNode(
$this->getTarget(),
$standbyFlowNode,
$this->getProcess()
);
}
}
if ($subProcessIsInterrupting) {
$this->getManager()->interruptProcessByEventSubProcess($this->getProcess(), $this->getFlowNode());
}
$this->processNextElement();
}
}

View File

@@ -0,0 +1,45 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Entities\BpmnProcess;
class EventSubProcess extends SubProcess
{
protected function getSubProcessStartElementId(): ?string
{
$eventStartData = $this->getAttributeValue('eventStartData') ?? (object) [];
return $eventStartData->id ?? null;
}
public function complete(): void
{
$this->setProcessed();
if ($this->getProcess()->getStatus() === BpmnProcess::STATUS_STARTED) {
$this->endProcessFlow();
}
}
protected function isMultiInstance(): bool
{
return false;
}
}

View File

@@ -0,0 +1,126 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
abstract class Gateway extends Base
{
public function process(): void
{
if ($this->isDivergent()) {
$this->processDivergent();
return;
}
if ($this->isConvergent()) {
$this->processConvergent();
return;
}
$this->setFailed();
}
abstract protected function processDivergent(): void;
abstract protected function processConvergent(): void;
protected function isDivergent(): bool
{
$nextElementIdList = $this->getAttributeValue('nextElementIdList') ?? [];
return !$this->isConvergent() && count($nextElementIdList);
}
protected function isConvergent(): bool
{
$previousElementIdList = $this->getAttributeValue('previousElementIdList') ?? [];
return is_array($previousElementIdList) && count($previousElementIdList) > 1;
}
/**
* @param string $divergentElementId
* @param string $forkElementId
* @param string $currentElementId
* @param string[] $metElementIdList
*/
private function checkElementsBelongSingleFlowRecursive(
$divergentElementId,
$forkElementId,
$currentElementId,
bool &$result,
&$metElementIdList = null
): void {
if ($divergentElementId === $currentElementId) {
return;
}
if ($forkElementId === $currentElementId) {
$result = true;
return;
}
if (!$metElementIdList) {
$metElementIdList = [];
}
$flowchartElementsDataHash = $this->getProcess()->get('flowchartElementsDataHash');
$elementData = $flowchartElementsDataHash->$currentElementId;
if (!isset($elementData->previousElementIdList)) {
return;
}
foreach ($elementData->previousElementIdList as $elementId) {
if (in_array($elementId, $metElementIdList)) {
continue;
}
$this->checkElementsBelongSingleFlowRecursive(
$divergentElementId,
$forkElementId,
$elementId,
$result,
$metElementIdList
);
}
}
/**
* @param string $divergentElementId
* @param string $forkElementId
* @param string $elementId
*/
protected function checkElementsBelongSingleFlow(
$divergentElementId,
$forkElementId,
$elementId
): bool {
$result = false;
$this->checkElementsBelongSingleFlowRecursive($divergentElementId, $forkElementId, $elementId, $result);
return $result;
}
}

View File

@@ -0,0 +1,63 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
/**
* @noinspection PhpUnused
*/
class GatewayEventBased extends Gateway
{
/**
* @throws Error
*/
protected function processDivergent(): void
{
$flowNode = $this->getFlowNode();
$item = $flowNode->getElementData();
$nextElementIdList = $item->nextElementIdList ?? [];
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$this->getEntityManager()->saveEntity($flowNode);
foreach ($nextElementIdList as $nextElementId) {
$nextFlowNode = $this->processNextElement($nextElementId, false, true);
if ($nextFlowNode->getStatus() === BpmnFlowNode::STATUS_PROCESSED) {
break;
}
}
$this->setProcessed();
$this->getManager()->tryToEndProcess($this->getProcess());
}
/**
* @throws Error
*/
protected function processConvergent(): void
{
$this->processNextElement();
}
}

View File

@@ -0,0 +1,99 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error as FormulaError;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\InjectableFactory;
use Espo\Modules\Advanced\Core\Bpmn\Utils\ConditionManager;
/**
* @noinspection PhpUnused
*/
class GatewayExclusive extends Gateway
{
/**
* @throws Error
* @throws FormulaError
*/
protected function processDivergent(): void
{
$conditionManager = $this->getConditionManager();
$flowList = $this->getAttributeValue('flowList');
if (!is_array($flowList)) {
$flowList = [];
}
$defaultNextElementId = $this->getAttributeValue('defaultNextElementId');
$nextElementId = null;
foreach ($flowList as $flowData) {
$conditionsAll = $flowData->conditionsAll ?? null;
$conditionsAny = $flowData->conditionsAny ?? null;
$conditionsFormula = $flowData->conditionsFormula ?? null;
$result = $conditionManager->check(
$this->getTarget(),
$conditionsAll,
$conditionsAny,
$conditionsFormula,
$this->getVariablesForFormula()
);
if ($result) {
$nextElementId = $flowData->elementId;
break;
}
}
if (!$nextElementId && $defaultNextElementId) {
$nextElementId = $defaultNextElementId;
}
if ($nextElementId) {
$this->processNextElement($nextElementId);
return;
}
$this->endProcessFlow();
}
/**
* @throws FormulaError
*/
protected function processConvergent(): void
{
$this->processNextElement();
}
protected function getConditionManager(): ConditionManager
{
$conditionManager = $this->getContainer()
->getByClass(InjectableFactory::class)
->create(ConditionManager::class);
$conditionManager->setCreatedEntitiesData($this->getCreatedEntitiesData());
return $conditionManager;
}
}

View File

@@ -0,0 +1,234 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error as FormulaError;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\InjectableFactory;
use Espo\Modules\Advanced\Core\Bpmn\Utils\ConditionManager;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\Modules\Advanced\Entities\BpmnProcess;
/**
* @noinspection PhpUnused
*/
class GatewayInclusive extends Gateway
{
/**
* @throws Error
* @throws FormulaError
*/
protected function processDivergent(): void
{
$conditionManager = $this->getConditionManager();
$flowList = $this->getAttributeValue('flowList');
if (!is_array($flowList)) {
$flowList = [];
}
$defaultNextElementId = $this->getAttributeValue('defaultNextElementId');
$nextElementIdList = [];
foreach ($flowList as $flowData) {
$conditionsAll = $flowData->conditionsAll ?? null;
$conditionsAny = $flowData->conditionsAny ?? null;
$conditionsFormula = $flowData->conditionsFormula ?? null;
$result = $conditionManager->check(
$this->getTarget(),
$conditionsAll,
$conditionsAny,
$conditionsFormula,
$this->getVariablesForFormula()
);
if ($result) {
$nextElementIdList[] = $flowData->elementId;
}
}
//$isDefaultFlow = false;
if (!count($nextElementIdList) && $defaultNextElementId) {
//$isDefaultFlow = true;
$nextElementIdList[] = $defaultNextElementId;
}
$flowNode = $this->getFlowNode();
$nextDivergentFlowNodeId = $flowNode->getId();
if (count($nextElementIdList)) {
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$this->getEntityManager()->saveEntity($flowNode);
$nextFlowNodeList = [];
foreach ($nextElementIdList as $nextElementId) {
$nextFlowNode = $this->prepareNextFlowNode($nextElementId, $nextDivergentFlowNodeId);
if ($nextFlowNode) {
$nextFlowNodeList[] = $nextFlowNode;
}
}
$this->setProcessed();
foreach ($nextFlowNodeList as $nextFlowNode) {
if ($this->getProcess()->getStatus() !== BpmnProcess::STATUS_STARTED) {
break;
}
$this->getManager()->processPreparedFlowNode($this->getTarget(), $nextFlowNode, $this->getProcess());
}
$this->getManager()->tryToEndProcess($this->getProcess());
return;
}
$this->endProcessFlow();
}
/**
* @throws FormulaError
*/
protected function processConvergent(): void
{
$flowNode = $this->getFlowNode();
$item = $flowNode->getElementData();
$previousElementIdList = $item->previousElementIdList;
$nextDivergentFlowNodeId = null;
$divergentFlowNode = null;
$convergingFlowCount = 1;
if ($flowNode->getDivergentFlowNodeId()) {
/** @var ?BpmnFlowNode $divergentFlowNode */
$divergentFlowNode = $this->getEntityManager()
->getEntityById(BpmnFlowNode::ENTITY_TYPE, $flowNode->getDivergentFlowNodeId());
if ($divergentFlowNode) {
$nextDivergentFlowNodeId = $divergentFlowNode->getDivergentFlowNodeId();
/** @var iterable<BpmnFlowNode> $forkFlowNodeList */
$forkFlowNodeList = $this->getEntityManager()
->getRDBRepository(BpmnFlowNode::ENTITY_TYPE)
->where([
'processId' => $flowNode->getProcessId(),
'previousFlowNodeId' => $divergentFlowNode->getId(),
])
->find();
$convergingFlowCount = 0;
foreach ($previousElementIdList as $previousElementId) {
$isActual = false;
foreach ($forkFlowNodeList as $forkFlowNode) {
if (
$this->checkElementsBelongSingleFlow(
$divergentFlowNode->getElementId(),
$forkFlowNode->getElementId(),
$previousElementId
)
) {
$isActual = true;
break;
}
}
if ($isActual) {
$convergingFlowCount++;
}
}
}
}
$concurrentFlowNodeList = $this->getEntityManager()
->getRDBRepository(BpmnFlowNode::ENTITY_TYPE)
->where([
'elementId' => $flowNode->getElementId(),
'processId' => $flowNode->getProcessId(),
'divergentFlowNodeId' => $flowNode->getDivergentFlowNodeId(),
])
->find();
$concurrentCount = count(iterator_to_array($concurrentFlowNodeList));
if ($concurrentCount < $convergingFlowCount) {
$this->setRejected();
return;
}
$isBalancingDivergent = true;
if ($divergentFlowNode) {
$divergentElementData = $divergentFlowNode->getElementData();
if (isset($divergentElementData->nextElementIdList)) {
foreach ($divergentElementData->nextElementIdList as $forkId) {
if (
!$this->checkElementsBelongSingleFlow(
$divergentFlowNode->getElementId(),
$forkId,
$flowNode->getElementId()
)
) {
$isBalancingDivergent = false;
break;
}
}
}
}
if ($isBalancingDivergent) {
if ($divergentFlowNode) {
$nextDivergentFlowNodeId = $divergentFlowNode->getDivergentFlowNodeId();
}
$this->processNextElement(null, $nextDivergentFlowNodeId);
return;
}
/** @noinspection PhpRedundantOptionalArgumentInspection */
$this->processNextElement(null, false);
}
protected function getConditionManager(): ConditionManager
{
$conditionManager = $this->getContainer()
->getByClass(InjectableFactory::class)
->create(ConditionManager::class);
$conditionManager->setCreatedEntitiesData($this->getCreatedEntitiesData());
return $conditionManager;
}
}

View File

@@ -0,0 +1,152 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\Modules\Advanced\Entities\BpmnProcess;
/**
* @noinspection PhpUnused
*/
class GatewayParallel extends Gateway
{
/**
* @throws Error
*/
protected function processDivergent(): void
{
$flowNode = $this->getFlowNode();
$item = $flowNode->getElementData();
$nextElementIdList = $item->nextElementIdList ?? [];
if (count($nextElementIdList)) {
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$this->getEntityManager()->saveEntity($flowNode);
$nextFlowNodeList = [];
foreach ($nextElementIdList as $nextElementId) {
$nextFlowNode = $this->prepareNextFlowNode($nextElementId, $flowNode->getId());
if ($nextFlowNode) {
$nextFlowNodeList[] = $nextFlowNode;
}
}
$this->setProcessed();
foreach ($nextFlowNodeList as $nextFlowNode) {
if ($this->getProcess()->getStatus() !== BpmnProcess::STATUS_STARTED) {
break;
}
$this->getManager()->processPreparedFlowNode($this->getTarget(), $nextFlowNode, $this->getProcess());
}
$this->getManager()->tryToEndProcess($this->getProcess());
return;
}
$this->endProcessFlow();
}
/**
* @throws Error
*/
protected function processConvergent(): void
{
$flowNode = $this->getFlowNode();
$item = $flowNode->getElementData();
$previousElementIdList = $item->previousElementIdList;
$convergingFlowCount = count($previousElementIdList);
//$nextDivergentFlowNodeId = null;
$divergentFlowNode = null;
//$divergedFlowCount = 1;
if ($flowNode->getDivergentFlowNodeId()) {
/** @var ?BpmnFlowNode $divergentFlowNode */
$divergentFlowNode = $this->getEntityManager()
->getEntityById(BpmnFlowNode::ENTITY_TYPE, $flowNode->getDivergentFlowNodeId());
/*if ($divergentFlowNode) {
$divergentElementData = $divergentFlowNode->getElementData();
$divergedFlowCount = count($divergentElementData->nextElementIdList ?? []);
}*/
}
$concurrentFlowNodeList = $this->getEntityManager()
->getRDBRepository(BpmnFlowNode::ENTITY_TYPE)
->where([
'elementId' => $flowNode->getElementId(),
'processId' => $flowNode->getProcessId(),
'divergentFlowNodeId' => $flowNode->getDivergentFlowNodeId(),
])
->find();
$concurrentCount = count(iterator_to_array($concurrentFlowNodeList));
if ($concurrentCount < $convergingFlowCount) {
$this->setRejected();
return;
}
$isBalancingDivergent = true;
if ($divergentFlowNode) {
$divergentElementData = $divergentFlowNode->getElementData();
if (isset($divergentElementData->nextElementIdList)) {
foreach ($divergentElementData->nextElementIdList as $forkId) {
if (
!$this->checkElementsBelongSingleFlow(
$divergentFlowNode->getElementId(),
$forkId,
$flowNode->getElementId()
)
) {
$isBalancingDivergent = false;
break;
}
}
}
}
if ($isBalancingDivergent) {
$nextDivergentFlowNodeId = $divergentFlowNode?->getDivergentFlowNodeId();
$this->processNextElement(null, $nextDivergentFlowNodeId);
return;
}
/** @noinspection PhpRedundantOptionalArgumentInspection */
$this->processNextElement(null, false);
}
}

View File

@@ -0,0 +1,144 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
use Espo\Modules\Advanced\Entities\BpmnFlowchart;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\Modules\Advanced\Entities\BpmnProcess;
use Throwable;
use stdClass;
class SubProcess extends CallActivity
{
public function process(): void
{
if ($this->isMultiInstance()) {
$this->processMultiInstance();
return;
}
$target = $this->getNewTargetEntity();
if (!$target) {
$this->getLog()->info("BPM Sub-Process: Could not get target for sub-process.");
$this->fail();
return;
}
$flowNode = $this->getFlowNode();
$variables = $this->getPrepareVariables();
$this->refreshProcess();
$parentFlowchartData = $this->getProcess()->get('flowchartData') ?? (object) [];
$createdEntitiesData = clone $this->getCreatedEntitiesData();
$eData = Helper::getElementsDataFromFlowchartData((object) [
'list' => $this->getAttributeValue('dataList') ?? [],
]);
/** @var BpmnFlowchart $flowchart */
$flowchart = $this->getEntityManager()->getNewEntity(BpmnFlowchart::ENTITY_TYPE);
$flowchart->set([
'targetType' => $target->getEntityType(),
'data' => (object) [
'createdEntitiesData' => $parentFlowchartData->createdEntitiesData ?? (object) [],
'list' => $this->getAttributeValue('dataList') ?? [],
],
'elementsDataHash' => $eData['elementsDataHash'],
'hasNoneStartEvent' => count($eData['eventStartIdList']) > 0,
'eventStartIdList'=> $eData['eventStartIdList'],
'teamsIds' => $this->getProcess()->getTeams()->getIdList(),
'assignedUserId' => $this->getProcess()->getAssignedUser()?->getId(),
'name' => $this->getAttributeValue('title') ?? 'Sub-Process',
]);
/** @var BpmnProcess $subProcess */
$subProcess = $this->getEntityManager()->createEntity(BpmnProcess::ENTITY_TYPE, [
'status' => BpmnFlowNode::STATUS_CREATED,
'targetId' => $target->getId(),
'targetType' => $target->getEntityType(),
'parentProcessId' => $this->getProcess()->getId(),
'parentProcessFlowNodeId' => $flowNode->getId(),
'rootProcessId' => $this->getProcess()->getRootProcessId(),
'assignedUserId' => $this->getProcess()->getAssignedUser()?->getId(),
'teamsIds' => $this->getProcess()->getTeams()->getIdList(),
'variables' => $variables,
'createdEntitiesData' => $createdEntitiesData,
'startElementId' => $this->getSubProcessStartElementId(),
], [
'skipCreatedBy' => true,
'skipModifiedBy' => true,
'skipStartProcessFlow' => true,
]);
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$flowNode->setDataItemValue('subProcessId', $subProcess->getId());
$this->getEntityManager()->saveEntity($flowNode);
try {
$this->getManager()->startCreatedProcess($subProcess, $flowchart);
} catch (Throwable $e) {
$message = "BPM Sub-Process: Starting sub-process failure, {$subProcess->getId()}. {$e->getMessage()}";
$this->getLog()->error($message, ['exception' => $e]);
$this->fail();
return;
}
}
protected function getSubProcessStartElementId(): ?string
{
return null;
}
protected function generateSubProcessMultiInstance(int $loopCounter, int $x, int $y): stdClass
{
return (object) [
'type' => $this->getAttributeValue('type'),
'id' => self::generateElementId(),
'center' => (object) [
'x' => $x + 125,
'y' => $y,
],
'dataList' => $this->getAttributeValue('dataList'),
'returnVariableList' => $this->getAttributeValue('returnVariableList'),
'isExpanded' => false,
'target' => $this->getAttributeValue('target'),
'targetType' => $this->getAttributeValue('targetType'),
'targetIdExpression' => $this->getAttributeValue('targetIdExpression'),
'isMultiInstance' => false,
'triggeredByEvent' => false,
'isSequential' => false,
'loopCollectionExpression' => null,
'text' => (string) $loopCounter,
];
}
}

View File

@@ -0,0 +1,157 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Core\InjectableFactory;
use Espo\Modules\Advanced\Core\Workflow\Actions\Base as BaseAction;
use Throwable;
use stdClass;
class Task extends Activity
{
/** @var string[] */
private array $localVariableList = [
'_lastHttpResponseBody',
'__lastCreatedEntityId',
];
public function process(): void
{
$actionList = $this->getAttributeValue('actionList');
if (!is_array($actionList)) {
$actionList = [];
}
$originalVariables = $this->getVariablesForFormula();
$variables = clone $originalVariables;
try {
foreach ($actionList as $item) {
if (empty($item->type)) {
continue;
}
$this->addCreatedEntityDataToVariables($variables);
$actionImpl = $this->getActionImplementation($item->type);
/** @var stdClass $item */
$item = clone $item;
$item->elementId = $this->getFlowNode()->getElementId();
$actionData = $item;
$actionImpl->process(
entity: $this->getTarget(),
actionData: $actionData,
createdEntitiesData: $this->getCreatedEntitiesData(),
variables: $variables,
bpmnProcess: $this->getProcess(),
);
if ($actionImpl->isCreatedEntitiesDataChanged()) {
$this->getProcess()->setCreatedEntitiesData($actionImpl->getCreatedEntitiesData());
$this->getEntityManager()->saveEntity($this->getProcess(), ['silent' => true]);
}
}
} catch (Throwable $e) {
$message = "Process {$this->getProcess()->getId()}, element {$this->getFlowNode()->getId()}: " .
"{$e->getMessage()}";
$this->getLog()->error($message, ['exception' => $e]);
$this->setFailedWithException($e);
return;
}
$this->processStoreVariables($variables, $originalVariables);
$this->processNextElement();
}
/**
* @throws Error
* @todo Use factory.
*/
private function getActionImplementation(string $name): BaseAction
{
$name = ucfirst($name);
$name = str_replace("\\", "", $name);
$className = 'Espo\\Modules\\Advanced\\Core\\Workflow\\Actions\\' . $name;
if (!class_exists($className)) {
$className .= 'Type';
if (!class_exists($className)) {
throw new Error('Action class ' . $className . ' does not exist.');
}
}
/** @var class-string<BaseAction> $className */
$impl = $this->getContainer()
->getByClass(InjectableFactory::class)
->create($className);
$workflowId = $this->getProcess()->get('workflowId');
if ($workflowId) {
$impl->setWorkflowId($workflowId);
}
return $impl;
}
private function processStoreVariables(stdClass $variables, stdClass $originalVariables): void
{
foreach ($this->localVariableList as $name) {
unset($variables->$name);
}
// The same in TaskScript.
if ($this->getAttributeValue('isolateVariables')) {
$variableList = array_keys(get_object_vars($variables));
$returnVariableList = $this->getReturnVariableList();
foreach (array_diff($variableList, $returnVariableList) as $name) {
unset($variables->$name);
if (property_exists($originalVariables, $name)) {
$variables->$name = $originalVariables->$name;
}
}
}
$this->sanitizeVariables($originalVariables);
$this->sanitizeVariables($variables);
if (serialize($variables) !== serialize($originalVariables)) {
$this->getProcess()->setVariables($variables);
$this->getEntityManager()->saveEntity($this->getProcess(), ['silent' => true]);
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use stdClass;
use Throwable;
/**
* @noinspection PhpUnused
*/
class TaskScript extends Activity
{
public function process(): void
{
$formula = $this->getAttributeValue('formula');
if (!$formula) {
$this->processNextElement();
return;
}
if (!is_string($formula)) {
$message = "Process {$this->getProcess()->getId()}, formula should be string.";
$this->getLog()->error($message);
$this->setFailed();
return;
}
$originalVariables = $this->getVariablesForFormula();
$variables = clone $originalVariables;
try {
$this->getFormulaManager()->run($formula, $this->getTarget(), $variables);
$this->getEntityManager()->saveEntity($this->getTarget(), [
'skipWorkflow' => true,
'skipModifiedBy' => true,
]);
} catch (Throwable $e) {
$message = "Process {$this->getProcess()->getId()} formula error: {$e->getMessage()}";
$this->getLog()->error($message, ['exception' => $e]);
$this->setFailedWithException($e);
return;
}
$this->processStoreVariables($variables, $originalVariables);
$this->processNextElement();
}
private function processStoreVariables(stdClass $variables, stdClass $originalVariables): void
{
// The same in Task.
if ($this->getAttributeValue('isolateVariables')) {
$variableList = array_keys(get_object_vars($variables));
$returnVariableList = $this->getReturnVariableList();
foreach (array_diff($variableList, $returnVariableList) as $name) {
unset($variables->$name);
if (property_exists($originalVariables, $name)) {
$variables->$name = $originalVariables->$name;
}
}
}
$this->sanitizeVariables($variables);
$this->getProcess()->setVariables($variables);
$this->getEntityManager()->saveEntity($this->getProcess(), ['silent' => true]);
}
}

View File

@@ -0,0 +1,96 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Core\InjectableFactory;
use Espo\Modules\Advanced\Core\Bpmn\Utils\MessageSenders\EmailType;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Throwable;
/**
* @noinspection PhpUnused
*/
class TaskSendMessage extends Activity
{
public function process(): void
{
$this->getFlowNode()->setStatus(BpmnFlowNode::STATUS_PENDING);
$this->saveFlowNode();
}
public function proceedPending(): void
{
$createdEntitiesData = $this->getCreatedEntitiesData();
try {
$this->getImplementation()->process(
$this->getTarget(),
$this->getFlowNode(),
$this->getProcess(),
$createdEntitiesData,
$this->getVariables()
);
} catch (Throwable $e) {
$message = "Process {$this->getProcess()->getId()}, element {$this->getFlowNode()->getElementId()}, " .
"send message error: {$e->getMessage()}";
$this->getLog()->error($message, ['exception' => $e]);
$this->setFailedWithException($e);
return;
}
$this->getProcess()->set('createdEntitiesData', $createdEntitiesData);
$this->getEntityManager()->saveEntity($this->getProcess());
$this->processNextElement();
}
/**
* @return EmailType
* @throws Error
* @todo Use factory.
*/
private function getImplementation(): EmailType
{
$messageType = $this->getAttributeValue('messageType');
if (!$messageType) {
throw new Error('Process ' . $this->getProcess()->getId() . ', no message type.');
}
$messageType = str_replace('\\', '', $messageType);
/** @var class-string<EmailType> $className */
$className = "Espo\\Modules\\Advanced\\Core\\Bpmn\\Utils\\MessageSenders\\{$messageType}Type";
if (!class_exists($className)) {
throw new Error(
'Process ' . $this->getProcess()->getId() . ' element ' .
$this->getFlowNode()->get('elementId'). ' send message not found implementation class.');
}
return $this->getContainer()
->getByClass(InjectableFactory::class)
->create($className);
}
}

View File

@@ -0,0 +1,311 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Field\LinkMultiple;
use Espo\Core\Field\LinkParent;
use Espo\Core\Formula\Exceptions\Error as FormulaError;
use Espo\Core\InjectableFactory;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\Utils\Language;
use Espo\Modules\Advanced\Business\Workflow\AssignmentRules\LeastBusy;
use Espo\Modules\Advanced\Business\Workflow\AssignmentRules\RoundRobin;
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use Espo\Modules\Advanced\Entities\BpmnUserTask;
use Espo\ORM\Entity;
use stdClass;
/**
* @noinspection PhpUnused
*/
class TaskUser extends Activity
{
private const ASSIGNMENT_SPECIFIED_USER = 'specifiedUser';
private const ASSIGNMENT_PROCESS_ASSIGNED_USER = 'processAssignedUser';
/**
* @throws FormulaError
* @throws Error
*/
public function process(): void
{
$this->getFlowNode()->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
$this->saveFlowNode();
$target = $this->getSpecificTarget($this->getAttributeValue('target'));
if (!$target) {
$this->getLog()->info("BPM TaskUser: Could not get target.");
$this->fail();
return;
}
$userTask = $this->createUserTask($target);
$this->getFlowNode()->setDataItemValue('userTaskId', $userTask->getId());
$this->saveFlowNode();
$createdEntitiesData = $this->getPreparedCreatedEntitiesData($userTask);
$this->getProcess()->setCreatedEntitiesData($createdEntitiesData);
$this->saveProcess();
}
/**
* @return LeastBusy|RoundRobin
* @throws Error
*/
private function getAssignmentRuleImplementation(string $assignmentRule)
{
/** @var class-string<LeastBusy|RoundRobin> $className */
$className = 'Espo\\Modules\\Advanced\\Business\\Workflow\\AssignmentRules\\' .
str_replace('-', '', $assignmentRule);
if (!class_exists($className)) {
throw new Error('Process TaskUser, Class ' . $className . ' not found.');
}
$injectableFactory = $this->getContainer()->getByClass(InjectableFactory::class);
return $injectableFactory->createWith($className, [
'entityType' => BpmnUserTask::ENTITY_TYPE,
'actionId' => $this->getElementId(),
'flowchartId' => $this->getFlowNode()->getFlowchartId(),
]);
}
public function complete(): void
{
if (!$this->isInNormalFlow()) {
$this->setProcessed();
return;
}
$this->processNextElement();
}
protected function getLanguage(): Language
{
/** @var Language */
return $this->getContainer()->get('defaultLanguage');
}
protected function setInterrupted(): void
{
$this->cancelUserTask();
parent::setInterrupted();
}
public function cleanupInterrupted(): void
{
parent::cleanupInterrupted();
$this->cancelUserTask();
}
private function cancelUserTask(): void
{
$userTaskId = $this->getFlowNode()->getDataItemValue('userTaskId');
if ($userTaskId) {
/** @var ?BpmnUserTask $userTask */
$userTask = $this->getEntityManager()->getEntityById(BpmnUserTask::ENTITY_TYPE, $userTaskId);
if ($userTask && !$userTask->get('isResolved')) {
$userTask->set(['isCanceled' => true]);
$this->getEntityManager()->saveEntity($userTask);
}
}
}
private function getTaskName(): ?string
{
$name = $this->getAttributeValue('name');
if (!is_string($name)) {
return null;
}
if (!$name) {
return null;
}
return Helper::applyPlaceholders($name, $this->getTarget(), $this->getVariables());
}
private function getInstructionsText(): ?string
{
$text = $this->getAttributeValue('instructions');
if (!is_string($text)) {
return null;
}
if (!$text) {
return null;
}
return Helper::applyPlaceholders($text, $this->getTarget(), $this->getVariables());
}
/**
* @return array<string, mixed>
* @throws Error
* @throws Forbidden
*/
private function getAssignmentAttributes(BpmnUserTask $userTask): array
{
$assignmentType = $this->getAttributeValue('assignmentType');
$targetTeamId = $this->getAttributeValue('targetTeamId');
$targetUserPosition = $this->getAttributeValue('targetUserPosition') ?: null;
$assignmentAttributes = [];
if (str_starts_with($assignmentType, 'rule:')) {
$assignmentRule = substr($assignmentType, 5);
$ruleImpl = $this->getAssignmentRuleImplementation($assignmentRule);
$whereClause = null;
if ($assignmentRule === 'Least-Busy') {
$whereClause = ['isResolved' => false];
}
$assignmentAttributes = $ruleImpl->getAssignmentAttributes(
$userTask,
$targetTeamId,
$targetUserPosition,
null,
$whereClause
);
} else if (str_starts_with($assignmentType, 'link:')) {
$link = substr($assignmentType, 5);
$e = $this->getTarget();
if (str_contains($link, '.')) {
[$firstLink, $link] = explode('.', $link);
$target = $this->getTarget();
$e = $this->getEntityManager()
->getRDBRepository($target->getEntityType())
->getRelation($target, $firstLink)
->findOne();
}
if ($e instanceof Entity) {
$field = $link . 'Id';
$userId = $e->get($field);
if ($userId) {
$assignmentAttributes['assignedUserId'] = $userId;
}
}
} else if ($assignmentType === self::ASSIGNMENT_PROCESS_ASSIGNED_USER) {
$userId = $this->getProcess()->getAssignedUser()?->getId();
if ($userId) {
$assignmentAttributes['assignedUserId'] = $userId;
}
} else if ($assignmentType === self::ASSIGNMENT_SPECIFIED_USER) {
$userId = $this->getAttributeValue('targetUserId');
if ($userId) {
$assignmentAttributes['assignedUserId'] = $userId;
}
}
return $assignmentAttributes;
}
private function getTaskNameFinal(): string
{
$name = $this->getTaskName();
$actionType = $this->getAttributeValue('actionType');
if (!$name) {
$name = $this->getLanguage()->translateOption($actionType, 'actionType', BpmnUserTask::ENTITY_TYPE);
}
return $name;
}
private function getPreparedCreatedEntitiesData(BpmnUserTask $userTask): stdClass
{
$createdEntitiesData = $this->getCreatedEntitiesData();
$alias = $this->getFlowNode()->getElementId();
if ($alias) {
$createdEntitiesData->$alias = (object)[
'entityId' => $userTask->getId(),
'entityType' => $userTask->getEntityType(),
];
}
return $createdEntitiesData;
}
private function createUserTask(CoreEntity $target): BpmnUserTask
{
$userTask = $this->getEntityManager()->getRDBRepositoryByClass(BpmnUserTask::class)->getNew();
$userTask
->setProcessId($this->getProcess()->getId())
->setActionType($this->getAttributeValue('actionType'))
->setFlowNodeId($this->getFlowNode()->getId())
->setTarget(LinkParent::create($target->getEntityType(), $target->getId()))
->setDescription($this->getAttributeValue('description'))
->setInstructions($this->getInstructionsText());
$userTask->set($this->getAssignmentAttributes($userTask));
$userTask
->setName($this->getTaskNameFinal())
->setTeams(LinkMultiple::create()->withAddedIdList($this->getTeamIdList()));
$this->getEntityManager()->saveEntity($userTask, ['createdById' => 'system']);
return $userTask;
}
/**
* @return string[]
*/
private function getTeamIdList(): array
{
$teamIdList = $this->getProcess()->getTeams()->getIdList();
$targetTeamId = $this->getAttributeValue('targetTeamId');
if ($targetTeamId && !in_array($targetTeamId, $teamIdList)) {
$teamIdList[] = $targetTeamId;
}
return $teamIdList;
}
}

View File

@@ -0,0 +1,242 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Utils;
use Espo\Core\Exceptions\Error;
use Espo\Core\Container;
use Espo\Core\Formula\Exceptions\Error as FormulaError;
use Espo\Core\Formula\Manager as FormulaManager;
use Espo\Core\InjectableFactory;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Modules\Advanced\Core\Workflow\Conditions\Base;
use Espo\ORM\Entity;
use RuntimeException;
use stdClass;
class ConditionManager
{
private ?stdClass $createdEntitiesData = null;
private const TYPE_AND = 'and';
private const TYPE_OR = 'or';
/** @var string[] */
private array $requiredOptionList = [
'comparison',
'fieldToCompare',
];
public function __construct(
private Container $container,
private InjectableFactory $injectableFactory,
) {}
/**
* @param ?stdClass[] $conditionsAll
* @param ?stdClass[] $conditionsAny
* @throws Error
* @throws FormulaError
*/
public function check(
Entity $entity,
?array $conditionsAll = null,
?array $conditionsAny = null,
?string $conditionsFormula = null,
?stdClass $variables = null,
): bool {
if (!$entity instanceof CoreEntity) {
throw new RuntimeException();
}
$result = true;
if (!is_null($conditionsAll)) {
$result &= $this->checkConditionsAll($entity, $conditionsAll);
}
if (!is_null($conditionsAny)) {
$result &= $this->checkConditionsAny($entity, $conditionsAny);
}
if (!empty($conditionsFormula)) {
$result &= $this->checkConditionsFormula($entity, $conditionsFormula, $variables);
}
return (bool) $result;
}
/**
* @param stdClass[] $items
* @throws Error
*/
public function checkConditionsAll(CoreEntity $entity, array $items): bool
{
foreach ($items as $item) {
if (!$this->processCheck($entity, $item)) {
return false;
}
}
return true;
}
/**
* @param stdClass[] $items
* @throws Error
*/
public function checkConditionsAny(CoreEntity $entity, array $items): bool
{
if ($items === []) {
return true;
}
foreach ($items as $item) {
if ($this->processCheck($entity, $item)) {
return true;
}
}
return false;
}
/**
* @throws FormulaError
*/
public function checkConditionsFormula(Entity $entity, ?string $formula, ?stdClass $variables = null): bool
{
if (empty($formula)) {
return true;
}
$formula = trim($formula, " \t\n\r");
if (str_ends_with($formula, ';')) {
$formula = substr($formula, 0, -1);
}
if (empty($formula)) {
return true;
}
$o = (object) [];
$o->__targetEntity = $entity;
if ($variables) {
foreach (get_object_vars($variables) as $name => $value) {
$o->$name = $value;
}
}
if ($this->createdEntitiesData) {
$o->__createdEntitiesData = $this->createdEntitiesData;
}
return $this->getFormulaManager()->run($formula, $entity, $o);
}
/**
* @throws Error
*/
private function processCheck(CoreEntity $entity, stdClass $item): bool
{
if (!$this->validate($item)) {
return false;
}
$type = $item->type ?? null;
if ($type === self::TYPE_AND || $type === self::TYPE_OR) {
/** @var stdClass[] $value */
$value = $item->value ?? [];
if ($type === self::TYPE_OR) {
return $this->checkConditionsAny($entity, $value);
}
return $this->checkConditionsAll($entity, $value);
}
$impl = $this->getConditionImplementation($item->comparison);
return $impl->process($entity, $item, $this->createdEntitiesData);
}
/**
* @throws Error
*/
private function getConditionImplementation(string $name): Base
{
$name = ucfirst($name);
$name = str_replace("\\", "", $name);
$className = 'Espo\\Modules\\Advanced\\Core\\Workflow\\Conditions\\' . $name;
if (!class_exists($className)) {
$className .= 'Type';
if (!class_exists($className)) {
throw new Error('ConditionManager: Class ' . $className . ' does not exist.');
}
}
/** @var class-string<Base> $className */
return $this->injectableFactory->create($className);
}
public function setCreatedEntitiesData(stdClass $createdEntitiesData): void
{
$this->createdEntitiesData = $createdEntitiesData;
}
private function validate(stdClass $item): bool
{
if (
isset($item->type) &&
in_array($item->type, [self::TYPE_OR, self::TYPE_AND])
) {
if (!isset($item->value) || !is_array($item->value)) {
return false;
}
return true;
}
foreach ($this->requiredOptionList as $optionName) {
if (!property_exists($item, $optionName)) {
return false;
}
}
return true;
}
private function getContainer(): Container
{
return $this->container;
}
private function getFormulaManager(): FormulaManager
{
return $this->getContainer()->getByClass(FormulaManager::class);
}
}

View File

@@ -0,0 +1,195 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Utils;
use Espo\ORM\Entity;
use stdClass;
class Helper
{
/**
* @return array{
* elementsDataHash: stdClass,
* eventStartIdList: string[],
* eventStartAllIdList: string[]
* }
*/
public static function getElementsDataFromFlowchartData(stdClass $data): array
{
$elementsDataHash = (object) [];
$eventStartIdList = [];
$eventStartAllIdList = [];
if (isset($data->list) && is_array($data->list)) {
foreach ($data->list as $item) {
if (!is_object($item)) {
continue;
}
$itType = $item->type ?? null;
$itId = $item->id ?? null;
if ($itType === 'flow') {
continue;
}
$nextElementIdList = [];
$previousElementIdList = [];
foreach ($data->list as $itemAnother) {
if ($itemAnother->type !== 'flow') {
continue;
}
if (!isset($itemAnother->startId) || !isset($itemAnother->endId)) {
continue;
}
if ($itemAnother->startId === $itId) {
$nextElementIdList[] = $itemAnother->endId;
} else if ($itemAnother->endId === $itId) {
$previousElementIdList[] = $itemAnother->startId;
}
}
usort($nextElementIdList, function ($id1, $id2) use ($data) {
$item1 = self::getItemById($data, $id1);
$item2 = self::getItemById($data, $id2);
if (isset($item1->center) && isset($item2->center)) {
if ($item1->center->y > $item2->center->y) {
return 1;
}
if ($item1->center->y == $item2->center->y) {
if ($item1->center->x > $item2->center->x) {
return 1;
}
}
}
return -1;
});
$id = $item->id ?? null;
/** @var stdClass $o */
$o = clone $item;
$o->nextElementIdList = $nextElementIdList;
$o->previousElementIdList = $previousElementIdList;
if (isset($item->flowList)) {
$o->flowList = [];
foreach ($item->flowList as $nextFlowData) {
$nextFlowDataCloned = clone $nextFlowData;
foreach ($data->list as $itemAnother) {
if ($itemAnother->id !== $nextFlowData->id) {
continue;
}
$nextFlowDataCloned->elementId = $itemAnother->endId;
break;
}
$o->flowList[] = $nextFlowDataCloned;
}
}
if (!empty($item->defaultFlowId)) {
foreach ($data->list as $itemAnother) {
if ($itemAnother->id !== $item->defaultFlowId) {
continue;
}
$o->defaultNextElementId = $itemAnother->endId;
break;
}
}
if ($itType === 'eventStart') {
$eventStartIdList[] = $id;
}
if (is_string($itType) && str_starts_with($itType, 'eventStart')) {
$eventStartAllIdList[] = $id;
}
$elementsDataHash->$id = $o;
}
}
return [
'elementsDataHash' => $elementsDataHash,
'eventStartIdList' => $eventStartIdList,
'eventStartAllIdList' => $eventStartAllIdList,
];
}
private static function getItemById(stdClass $data, string $id): ?stdClass
{
foreach ($data->list as $item) {
if ($item->id === $id) {
return $item;
}
}
return null;
}
public static function applyPlaceholders(string $text, Entity $target, stdClass $variables = null): string
{
foreach ($target->getAttributeList() as $attribute) {
$value = $target->get($attribute);
if ($value === null) {
continue;
}
if (is_numeric($value)) {
$value = (string) $value;
}
if (!is_string($value)) {
continue;
}
$text = str_replace('{$' . $attribute . '}', $value, $text);
}
$variables = $variables ?? (object) [];
foreach (get_object_vars($variables) as $key => $value) {
if (is_numeric($value)) {
$value = (string) $value;
}
if (!is_string($value)) {
continue;
}
$text = str_replace('{$$' . $key . '}', $value, $text);
}
return $text;
}
}

View File

@@ -0,0 +1,142 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Core\Bpmn\Utils\MessageSenders;
use Espo\Core\Exceptions\Error;
use Espo\Core\InjectableFactory;
use Espo\Modules\Advanced\Core\Workflow\Actions\SendEmail;
use Espo\ORM\Entity;
use Espo\Modules\Advanced\Entities\BpmnProcess;
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
use stdClass;
class EmailType
{
public function __construct(
private InjectableFactory $injectableFactory,
) {}
/**
* @throws Error
*/
public function process(
Entity $target,
BpmnFlowNode $flowNode,
BpmnProcess $process,
stdClass $createdEntitiesData,
stdClass $variables
): void {
$elementData = $flowNode->getElementData();
if (empty($elementData->from)) {
throw new Error("No 'from'.");
}
$from = $elementData->from;
if (empty($elementData->to)) {
throw new Error("No 'to'.");
}
$to = $elementData->to;
$replyTo = null;
if (!empty($elementData->replyTo)) {
$replyTo = $elementData->replyTo;
}
$cc = null;
if (!empty($elementData->cc)) {
$cc = $elementData->cc;
}
if (empty($elementData->emailTemplateId)) {
throw new Error("No 'emailTemplateId'.");
}
$emailTemplateId = $elementData->emailTemplateId;
$doNotStore = false;
if (isset($elementData->doNotStore)) {
$doNotStore = $elementData->doNotStore;
}
$actionData = (object) [
'type' => 'SendEmail',
'from' => $from,
'to' => $to,
'cc' => $cc,
'replyTo' => $replyTo,
'emailTemplateId' => $emailTemplateId,
'doNotStore' => $doNotStore,
'processImmediately' => true,
'elementId' => $flowNode->get('elementId'),
'optOutLink' => $elementData->optOutLink ?? false,
'attachmentsVariable' => $elementData->attachmentsVariable ?? null,
];
if (property_exists($elementData, 'toEmailAddress')) {
$actionData->toEmail = $elementData->toEmailAddress;
}
if (property_exists($elementData, 'fromEmailAddress')) {
$actionData->fromEmail = $elementData->fromEmailAddress;
}
if (property_exists($elementData, 'replyToEmailAddress')) {
$actionData->replyToEmail = $elementData->replyToEmailAddress;
}
if (property_exists($elementData, 'ccEmailAddress')) {
$actionData->ccEmail = $elementData->ccEmailAddress;
}
if (in_array($to, ['specifiedContacts', 'specifiedUsers', 'specifiedTeams'])) {
$actionData->toSpecifiedEntityIds = $elementData->{'to' . ucfirst($to) . 'Ids'};
}
// Not used. Not available on UI.
if (in_array($replyTo, ['specifiedContacts', 'specifiedUsers', 'specifiedTeams'])) {
$actionData->replyToSpecifiedEntityIds = $elementData->{'replyTo' . ucfirst($replyTo) . 'Ids'};
}
// Not used. Not available on UI.
if (in_array($cc, ['specifiedContacts', 'specifiedUsers', 'specifiedTeams'])) {
$actionData->ccSpecifiedEntityIds = $elementData->{'cc' . ucfirst($cc) . 'Ids'};
}
$this->getActionImplementation()->process(
entity: $target,
actionData: $actionData,
createdEntitiesData: $createdEntitiesData,
variables: $variables,
bpmnProcess: $process,
);
}
private function getActionImplementation(): SendEmail
{
return $this->injectableFactory->create(SendEmail::class);
}
}