diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index db3c45a3..3f6d74ce 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -44,8 +44,8 @@ jobs: - name: Run PHPUnit tests run: ./vendor/bin/phpunit -d memory_limit=-1 - - name: List coverage files - run: ls -l coverage + - name: Check coverage + run: ./check_coverage.php - uses: sonarsource/sonarqube-scan-action@master env: diff --git a/src/ProcessMaker/Nayra/Bpmn/Assignment.php b/src/ProcessMaker/Nayra/Bpmn/Assignment.php new file mode 100644 index 00000000..f4c99d2a --- /dev/null +++ b/src/ProcessMaker/Nayra/Bpmn/Assignment.php @@ -0,0 +1,34 @@ +getProperty(self::BPMN_PROPERTY_FROM); + } + + /** + * Get the 'to' formal expression. + * + * @return FormalExpressionInterface|callable + */ + public function getTo() + { + return $this->getProperty(self::BPMN_PROPERTY_TO); + } +} diff --git a/src/ProcessMaker/Nayra/Bpmn/CatchEventTrait.php b/src/ProcessMaker/Nayra/Bpmn/CatchEventTrait.php index e86c6fe1..ff0e95aa 100644 --- a/src/ProcessMaker/Nayra/Bpmn/CatchEventTrait.php +++ b/src/ProcessMaker/Nayra/Bpmn/CatchEventTrait.php @@ -30,6 +30,7 @@ protected function initCatchEventTrait() { $this->setProperty(CatchEventInterface::BPMN_PROPERTY_EVENT_DEFINITIONS, new Collection); $this->setProperty(CatchEventInterface::BPMN_PROPERTY_PARALLEL_MULTIPLE, false); + $this->setProperty(CatchEventInterface::BPMN_PROPERTY_DATA_OUTPUT_ASSOCIATION, new Collection); } /** @@ -42,6 +43,11 @@ public function getEventDefinitions() return $this->getProperty(CatchEventInterface::BPMN_PROPERTY_EVENT_DEFINITIONS); } + public function getDataOutputAssociations() + { + return $this->getProperty(CatchEventInterface::BPMN_PROPERTY_DATA_OUTPUT_ASSOCIATION); + } + /** * Register catch events. * diff --git a/src/ProcessMaker/Nayra/Bpmn/DataInputAssociation.php b/src/ProcessMaker/Nayra/Bpmn/DataInputAssociation.php new file mode 100644 index 00000000..a95b8c54 --- /dev/null +++ b/src/ProcessMaker/Nayra/Bpmn/DataInputAssociation.php @@ -0,0 +1,38 @@ +properties[static::BPMN_PROPERTY_ASSIGNMENT] = new Collection; + } + + public function getSource() + { + return $this->getProperty(static::BPMN_PROPERTY_SOURCES_REF); + } + + public function getTarget() + { + return $this->getProperty(static::BPMN_PROPERTY_TARGET_REF); + } + + public function getTransformation() + { + return $this->getProperty(static::BPMN_PROPERTY_TRANSFORMATION); + } + + public function getAssignments() + { + return $this->getProperty(static::BPMN_PROPERTY_ASSIGNMENT); + } +} diff --git a/src/ProcessMaker/Nayra/Bpmn/DataOutputAssociation.php b/src/ProcessMaker/Nayra/Bpmn/DataOutputAssociation.php new file mode 100644 index 00000000..949e7979 --- /dev/null +++ b/src/ProcessMaker/Nayra/Bpmn/DataOutputAssociation.php @@ -0,0 +1,38 @@ +properties[static::BPMN_PROPERTY_ASSIGNMENT] = new Collection; + } + + public function getSource() + { + return $this->getProperty(static::BPMN_PROPERTY_SOURCES_REF); + } + + public function getTarget() + { + return $this->getProperty(static::BPMN_PROPERTY_TARGET_REF); + } + + public function getTransformation() + { + return $this->getProperty(static::BPMN_PROPERTY_TRANSFORMATION); + } + + public function getAssignments() + { + return $this->getProperty(static::BPMN_PROPERTY_ASSIGNMENT); + } +} diff --git a/src/ProcessMaker/Nayra/Bpmn/DataStoreTrait.php b/src/ProcessMaker/Nayra/Bpmn/DataStoreTrait.php index 47eb66dd..6720854c 100755 --- a/src/ProcessMaker/Nayra/Bpmn/DataStoreTrait.php +++ b/src/ProcessMaker/Nayra/Bpmn/DataStoreTrait.php @@ -100,4 +100,76 @@ public function getItemSubject() { return $this->itemSubject; } + + /** + * Get data using dot notation. + * + * @param string $path Dot notation path (e.g., 'user.profile.name') + * @param mixed $default Default value if path doesn't exist + * + * @return mixed + */ + public function getDotData($path, $default = null) + { + $keys = explode('.', $path); + $current = $this->data; + + // Navigate through the path + foreach ($keys as $key) { + // Handle numeric keys for arrays + if (is_numeric($key)) { + $key = (int) $key; + } + + if (!isset($current[$key])) { + return $default; + } + + $current = $current[$key]; + } + + return $current; + } + + /** + * Set data using dot notation. + * + * @param string $path Dot notation path (e.g., 'user.profile.name') + * @param mixed $value Value to set + * + * @return $this + */ + public function setDotData($path, $value) + { + $keys = explode('.', $path); + $firstKey = $keys[0]; + $current = &$this->data; + + // Navigate to the parent of the target key + for ($i = 0; $i < count($keys) - 1; $i++) { + $key = $keys[$i]; + + // Handle numeric keys for arrays + if (is_numeric($key)) { + $key = (int) $key; + } + + if (!isset($current[$key]) || !is_array($current[$key])) { + $current[$key] = []; + } + $current = &$current[$key]; + } + + // Set the final value + $finalKey = $keys[count($keys) - 1]; + if (is_numeric($finalKey)) { + $finalKey = (int) $finalKey; + } + + $current[$finalKey] = $value; + // Keep compatibility with putData method (required by PM Core) + $this->putData($firstKey, $this->data[$firstKey]); + + return $this; + } } diff --git a/src/ProcessMaker/Nayra/Bpmn/Models/EndEvent.php b/src/ProcessMaker/Nayra/Bpmn/Models/EndEvent.php index 53e5a0ea..04c605ac 100644 --- a/src/ProcessMaker/Nayra/Bpmn/Models/EndEvent.php +++ b/src/ProcessMaker/Nayra/Bpmn/Models/EndEvent.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Nayra\Bpmn\Models; +use ProcessMaker\Nayra\Bpmn\Collection; use ProcessMaker\Nayra\Bpmn\EndEventTrait; use ProcessMaker\Nayra\Contracts\Bpmn\EndEventInterface; use ProcessMaker\Nayra\Model\DataInputAssociationInterface; @@ -19,12 +20,18 @@ class EndEvent implements EndEventInterface { use EndEventTrait; - private $dataInputs; - - private $dataInputAssociations; - private $inputSet; + /** + * Initialize intermediate throw event. + */ + protected function initEndEvent() + { + $this->properties[static::BPMN_PROPERTY_DATA_INPUT_ASSOCIATION] = new Collection(); + $this->properties[static::BPMN_PROPERTY_DATA_INPUT] = new Collection; + $this->setProperty(static::BPMN_PROPERTY_EVENT_DEFINITIONS, new Collection); + } + /** * Array map of custom event classes for the bpmn element. * @@ -52,7 +59,7 @@ public function getTargetInstances(EventDefinitionInterface $message, TokenInter */ public function getDataInputs() { - return $this->dataInputs; + return $this->getProperty(static::BPMN_PROPERTY_DATA_INPUT); } /** @@ -62,7 +69,7 @@ public function getDataInputs() */ public function getDataInputAssociations() { - return $this->getDataInputAssociations(); + return $this->getProperty(static::BPMN_PROPERTY_DATA_INPUT_ASSOCIATION); } /** diff --git a/src/ProcessMaker/Nayra/Bpmn/Models/IntermediateThrowEvent.php b/src/ProcessMaker/Nayra/Bpmn/Models/IntermediateThrowEvent.php index a9977a2e..4edfec67 100644 --- a/src/ProcessMaker/Nayra/Bpmn/Models/IntermediateThrowEvent.php +++ b/src/ProcessMaker/Nayra/Bpmn/Models/IntermediateThrowEvent.php @@ -13,16 +13,6 @@ class IntermediateThrowEvent implements IntermediateThrowEventInterface { use IntermediateThrowEventTrait; - /** - * @var \ProcessMaker\Nayra\Contracts\Bpmn\DataInputAssociationInterface[] - */ - private $dataInputAssociations; - - /** - * @var \ProcessMaker\Nayra\Contracts\Bpmn\DataInputInterface[] - */ - private $dataInputs; - /** * @var \ProcessMaker\Nayra\Contracts\Bpmn\InputSetInterface */ @@ -38,8 +28,8 @@ class IntermediateThrowEvent implements IntermediateThrowEventInterface */ protected function initIntermediateThrowEvent() { - $this->dataInputAssociations = new Collection; - $this->dataInputs = new Collection; + $this->properties[static::BPMN_PROPERTY_DATA_INPUT_ASSOCIATION] = new Collection; + $this->properties[static::BPMN_PROPERTY_DATA_INPUT] = new Collection; $this->setProperty(static::BPMN_PROPERTY_EVENT_DEFINITIONS, new Collection); } @@ -60,7 +50,7 @@ protected function getBpmnEventClasses() */ public function getDataInputAssociations() { - return $this->dataInputAssociations; + return $this->getProperty(static::BPMN_PROPERTY_DATA_INPUT_ASSOCIATION); } /** @@ -70,7 +60,7 @@ public function getDataInputAssociations() */ public function getDataInputs() { - return $this->dataInputs; + return $this->getProperty(static::BPMN_PROPERTY_DATA_INPUT); } /** diff --git a/src/ProcessMaker/Nayra/Bpmn/Models/MessageEventDefinition.php b/src/ProcessMaker/Nayra/Bpmn/Models/MessageEventDefinition.php index 77bae714..b85cff94 100644 --- a/src/ProcessMaker/Nayra/Bpmn/Models/MessageEventDefinition.php +++ b/src/ProcessMaker/Nayra/Bpmn/Models/MessageEventDefinition.php @@ -3,11 +3,15 @@ namespace ProcessMaker\Nayra\Bpmn\Models; use ProcessMaker\Nayra\Bpmn\EventDefinitionTrait; +use ProcessMaker\Nayra\Contracts\Bpmn\CatchEventInterface; +use ProcessMaker\Nayra\Contracts\Bpmn\CollectionInterface; +use ProcessMaker\Nayra\Contracts\Bpmn\DataStoreInterface; use ProcessMaker\Nayra\Contracts\Bpmn\EventDefinitionInterface; use ProcessMaker\Nayra\Contracts\Bpmn\FlowNodeInterface; use ProcessMaker\Nayra\Contracts\Bpmn\MessageEventDefinitionInterface; use ProcessMaker\Nayra\Contracts\Bpmn\MessageInterface; use ProcessMaker\Nayra\Contracts\Bpmn\OperationInterface; +use ProcessMaker\Nayra\Contracts\Bpmn\ThrowEventInterface; use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface; use ProcessMaker\Nayra\Contracts\Engine\ExecutionInstanceInterface; @@ -88,9 +92,123 @@ public function assertsRule(EventDefinitionInterface $event, FlowNodeInterface $ */ public function execute(EventDefinitionInterface $event, FlowNodeInterface $target, ExecutionInstanceInterface $instance = null, TokenInterface $token = null) { + $throwEvent = $token->getOwnerElement(); + $this->executeMessageMapping($throwEvent, $target, $instance, $token); return $this; } + /** + * Map a message payload from a ThrowEvent through an optional CatchEvent mapping + * into the instance data store. + * + * @param ThrowEventInterface $throwEvent + * @param CatchEventInterface $catchEvent + * @param ExecutionInstanceInterface $instance + * @param TokenInterface $token + * + * @return void + */ + private function executeMessageMapping(ThrowEventInterface $throwEvent, CatchEventInterface $catchEvent, ExecutionInstanceInterface $instance, TokenInterface $token): void + { + $sourceMaps = $throwEvent->getDataInputAssociations(); + $targetMaps = $catchEvent->getDataOutputAssociations(); + $targetStore = $instance->getDataStore(); + + // Source of data is the token's instance store if present; otherwise a fresh store. + $sourceStore = $token->getInstance()?->getDataStore() ?? new DataStore(); + + // If target mappings exist we stage into a buffer; otherwise write straight to the instance store. + $bufferStore = !count($targetMaps) ? $targetStore : new DataStore(); + + // 1) Source mappings: source → buffer/instance + $this->evaluateMessagePayload($sourceMaps, $sourceStore, $bufferStore); + + // 2) Optional target mappings: buffer → instance + if (count($targetMaps)) { + $this->evaluateMessagePayload($targetMaps, $bufferStore, $targetStore); + } + } + + /** + * Evaluate the message payload + * + * @param CollectionInterface $associations + * @param DataStoreInterface $sourceStore + * @param DataStoreInterface $targetStore + * + * @return void + */ + private function evaluateMessagePayload(CollectionInterface $associations, DataStoreInterface $sourceStore, DataStoreInterface $targetStore): void + { + $assignments = []; + + foreach ($associations as $association) { + $source = $association->getSource(); + $target = $association->getTarget(); + + $hasSource = $source && $source->getName(); + $hasTarget = $target && $target->getName(); + + // Base data always starts from full source store + $data = $sourceStore->getData(); + + // Optionally add a direct reference to the source value + if ($hasSource) { + $data['sourceRef'] = $sourceStore->getDotData($source->getName()); + } + + // Transformation and assignments build up the assignments list + $this->applyTransformation($association, $data, $assignments, $hasTarget, $hasSource); + $this->evaluateAssignments($association, $data, $assignments); + } + + // Flush all assignments into target store + foreach ($assignments as $assignment) { + $targetStore->setDotData($assignment['key'], $assignment['value']); + } + } + + /** + * Apply transformation to the data and add to payload + * + * @param mixed $association + * @param array $data + * @param array &$payload + * @param bool $hasTarget + * @param bool $hasSource + */ + private function applyTransformation($association, array $data, array &$payload, bool $hasTarget, bool $hasSource) + { + $transformation = $association->getTransformation(); + $target = $association->getTarget(); + + if ($hasTarget && $transformation && is_callable($transformation)) { + $value = $transformation($data); + $payload[] = ['key' => $target->getName(), 'value' => $value]; + } elseif ($hasTarget && $hasSource) { + $payload[] = ['key' => $target->getName(), 'value' => $data['sourceRef']]; + } + } + + /** + * Evaluate assignments and add to payload + * + * @param mixed $association + * @param array $data + * @param array &$payload + */ + private function evaluateAssignments($association, array $data, array &$payload) + { + $assignments = $association->getAssignments(); + foreach ($assignments as $assignment) { + $from = $assignment->getFrom(); + $to = trim($assignment->getTo()?->getBody()); + if (is_callable($from) && !empty($to)) { + $payload[] = ['key' => $to, 'value' => $from($data)]; + } + } + } + /** * Check if the $eventDefinition should be catch * diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/AssignmentInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/AssignmentInterface.php index 20069e60..9a25a1f2 100644 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/AssignmentInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/AssignmentInterface.php @@ -7,6 +7,9 @@ */ interface AssignmentInterface extends EntityInterface { + const BPMN_PROPERTY_FROM = 'from'; + const BPMN_PROPERTY_TO = 'to'; + /** * @return FormalExpressionInterface */ diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/CatchEventInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/CatchEventInterface.php index 06893e58..54cb676a 100644 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/CatchEventInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/CatchEventInterface.php @@ -16,6 +16,12 @@ interface CatchEventInterface extends EventInterface const EVENT_CATCH_TOKEN_ARRIVES = 'CatchEventTokenArrives'; + const BPMN_PROPERTY_DATA_OUTPUT = 'dataOutput'; + + const BPMN_PROPERTY_DATA_OUTPUT_SET = 'outputSet'; + + const BPMN_PROPERTY_DATA_OUTPUT_ASSOCIATION = 'dataOutputAssociation'; + /** * Get EventDefinitions that are triggers expected for a catch Event. * @@ -57,4 +63,11 @@ public function execute(EventDefinitionInterface $event, ExecutionInstanceInterf * @return StateInterface */ public function getActiveState(); + + /** + * Get the data output associations. + * + * @return DataOutputAssociationInterface[] + */ + public function getDataOutputAssociations(); } diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/CollectionInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/CollectionInterface.php index 68c11532..7742c805 100755 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/CollectionInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/CollectionInterface.php @@ -3,11 +3,12 @@ namespace ProcessMaker\Nayra\Contracts\Bpmn; use SeekableIterator; +use Countable; /** * CollectionInterface */ -interface CollectionInterface extends SeekableIterator +interface CollectionInterface extends SeekableIterator, Countable { /** * Count the elements of the collection. diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/DataAssociationInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/DataAssociationInterface.php index b12f5c69..d1de4ee3 100644 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/DataAssociationInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/DataAssociationInterface.php @@ -7,12 +7,17 @@ */ interface DataAssociationInterface extends EntityInterface { + const BPMN_PROPERTY_ASSIGNMENT = 'assignment'; + const BPMN_PROPERTY_SOURCES_REF = 'sourceRef'; + const BPMN_PROPERTY_TARGET_REF = 'targetRef'; + const BPMN_PROPERTY_TRANSFORMATION = 'transformation'; + /** * Get the source of the data association. * - * @return ItemAwareElementInterface[] + * @return ItemAwareElementInterface */ - public function getSources(); + public function getSource(); /** * Get the target of the data association. @@ -31,5 +36,5 @@ public function getTransformation(); /** * @return AssignmentInterface[] */ - public function getAssignmentInterfaces(); + public function getAssignments(); } diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/DataInputAssociationInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/DataInputAssociationInterface.php index 27c817fa..97cca901 100644 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/DataInputAssociationInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/DataInputAssociationInterface.php @@ -7,4 +7,8 @@ */ interface DataInputAssociationInterface extends DataAssociationInterface { + const BPMN_PROPERTY_ASSIGNMENT = 'assignment'; + const BPMN_PROPERTY_SOURCES_REF = 'sourceRef'; + const BPMN_PROPERTY_TARGET_REF = 'targetRef'; + const BPMN_PROPERTY_TRANSFORMATION = 'transformation'; } diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/DataOutputAssociationInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/DataOutputAssociationInterface.php index e0c9ca23..d0edb29c 100644 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/DataOutputAssociationInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/DataOutputAssociationInterface.php @@ -7,4 +7,5 @@ */ interface DataOutputAssociationInterface extends DataAssociationInterface { + } diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/DataStoreInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/DataStoreInterface.php index 3fef523f..0f491500 100755 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/DataStoreInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/DataStoreInterface.php @@ -76,4 +76,24 @@ public function setData($data); * @return $this */ public function putData($name, $data); + + /** + * Set data using dot notation. + * + * @param string $path Dot notation path (e.g., 'user.profile.name') + * @param mixed $value Value to set + * + * @return $this + */ + public function setDotData($path, $value); + + /** + * Get data using dot notation. + * + * @param string $path Dot notation path (e.g., 'user.profile.name') + * @param mixed $default Default value if path doesn't exist + * + * @return mixed + */ + public function getDotData($path, $default = null); } diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/EventInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/EventInterface.php index ef4845f2..6ea16164 100755 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/EventInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/EventInterface.php @@ -10,6 +10,9 @@ interface EventInterface extends FlowNodeInterface { const BPMN_PROPERTY_EVENT_DEFINITIONS = 'eventDefinitions'; + const BPMN_PROPERTY_DATA_INPUT = 'dataInput'; + const BPMN_PROPERTY_DATA_INPUT_SET = 'inputSet'; + const BPMN_PROPERTY_DATA_INPUT_ASSOCIATION = 'dataInputAssociation'; const TYPE_START = 'START'; diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/FormalExpressionInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/FormalExpressionInterface.php index 0b6d2147..66a61449 100644 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/FormalExpressionInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/FormalExpressionInterface.php @@ -39,4 +39,13 @@ public function getBody(); * @param string $body */ public function setBody($body); + + /** + * Invoke the format expression. + * + * @param mixed $data + * + * @return string + */ + public function __invoke($data); } diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/ThrowEventInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/ThrowEventInterface.php index cb59e171..3f4aa06a 100644 --- a/src/ProcessMaker/Nayra/Contracts/Bpmn/ThrowEventInterface.php +++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/ThrowEventInterface.php @@ -18,6 +18,12 @@ interface ThrowEventInterface extends EventInterface const EVENT_THROW_TOKEN_CONSUMED = 'ThrowEventTokenConsumed'; + const BPMN_PROPERTY_DATA_INPUT = 'dataInput'; + + const BPMN_PROPERTY_DATA_INPUT_SET = 'inputSet'; + + const BPMN_PROPERTY_DATA_INPUT_ASSOCIATION = 'dataInputAssociation'; + /** * Get Data Inputs for the throw Event. * diff --git a/src/ProcessMaker/Nayra/RepositoryTrait.php b/src/ProcessMaker/Nayra/RepositoryTrait.php index e56bac84..47c98c70 100644 --- a/src/ProcessMaker/Nayra/RepositoryTrait.php +++ b/src/ProcessMaker/Nayra/RepositoryTrait.php @@ -3,6 +3,9 @@ namespace ProcessMaker\Nayra; use InvalidArgumentException; +use ProcessMaker\Nayra\Bpmn\Assignment; +use ProcessMaker\Nayra\Bpmn\DataInputAssociation; +use ProcessMaker\Nayra\Bpmn\DataOutputAssociation; use ProcessMaker\Nayra\Bpmn\Lane; use ProcessMaker\Nayra\Bpmn\LaneSet; use ProcessMaker\Nayra\Bpmn\Models\Activity; @@ -481,4 +484,19 @@ public function createStandardLoopCharacteristics() { return new StandardLoopCharacteristics(); } + + public function createDataInputAssociation() + { + return new DataInputAssociation(); + } + + public function createAssignment() + { + return new Assignment(); + } + + public function createDataOutputAssociation() + { + return new DataOutputAssociation(); + } } diff --git a/src/ProcessMaker/Nayra/Storage/BpmnDocument.php b/src/ProcessMaker/Nayra/Storage/BpmnDocument.php index 70c7d29e..0425d35a 100644 --- a/src/ProcessMaker/Nayra/Storage/BpmnDocument.php +++ b/src/ProcessMaker/Nayra/Storage/BpmnDocument.php @@ -6,12 +6,15 @@ use DOMElement; use DOMXPath; use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface; +use ProcessMaker\Nayra\Contracts\Bpmn\AssignmentInterface; use ProcessMaker\Nayra\Contracts\Bpmn\BoundaryEventInterface; use ProcessMaker\Nayra\Contracts\Bpmn\CallActivityInterface; use ProcessMaker\Nayra\Contracts\Bpmn\CatchEventInterface; use ProcessMaker\Nayra\Contracts\Bpmn\CollaborationInterface; use ProcessMaker\Nayra\Contracts\Bpmn\ConditionalEventDefinitionInterface; +use ProcessMaker\Nayra\Contracts\Bpmn\DataInputAssociationInterface; use ProcessMaker\Nayra\Contracts\Bpmn\DataInputInterface; +use ProcessMaker\Nayra\Contracts\Bpmn\DataOutputAssociationInterface; use ProcessMaker\Nayra\Contracts\Bpmn\DataOutputInterface; use ProcessMaker\Nayra\Contracts\Bpmn\DataStoreInterface; use ProcessMaker\Nayra\Contracts\Bpmn\EndEventInterface; @@ -52,6 +55,7 @@ use ProcessMaker\Nayra\Contracts\Bpmn\StandardLoopCharacteristicsInterface; use ProcessMaker\Nayra\Contracts\Bpmn\StartEventInterface; use ProcessMaker\Nayra\Contracts\Bpmn\TerminateEventDefinitionInterface; +use ProcessMaker\Nayra\Contracts\Bpmn\ThrowEventInterface; use ProcessMaker\Nayra\Contracts\Bpmn\TimerEventDefinitionInterface; use ProcessMaker\Nayra\Contracts\Engine\EngineInterface; use ProcessMaker\Nayra\Contracts\RepositoryInterface; @@ -117,6 +121,9 @@ class BpmnDocument extends DOMDocument implements BpmnDocumentInterface FlowNodeInterface::BPMN_PROPERTY_INCOMING => ['n', [self::BPMN_MODEL, FlowNodeInterface::BPMN_PROPERTY_INCOMING]], FlowNodeInterface::BPMN_PROPERTY_OUTGOING => ['n', [self::BPMN_MODEL, FlowNodeInterface::BPMN_PROPERTY_OUTGOING]], EndEventInterface::BPMN_PROPERTY_EVENT_DEFINITIONS => ['n', EventDefinitionInterface::class], + IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT => ['n', [self::BPMN_MODEL, IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT]], + IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT_SET => ['1', [self::BPMN_MODEL, IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT_SET]], + IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT_ASSOCIATION => ['n', [self::BPMN_MODEL, IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT_ASSOCIATION]], ], ], 'task' => [ @@ -371,6 +378,54 @@ class BpmnDocument extends DOMDocument implements BpmnDocumentInterface FlowNodeInterface::BPMN_PROPERTY_INCOMING => ['n', [self::BPMN_MODEL, FlowNodeInterface::BPMN_PROPERTY_INCOMING]], FlowNodeInterface::BPMN_PROPERTY_OUTGOING => ['n', [self::BPMN_MODEL, FlowNodeInterface::BPMN_PROPERTY_OUTGOING]], IntermediateThrowEventInterface::BPMN_PROPERTY_EVENT_DEFINITIONS => ['n', EventDefinitionInterface::class], + IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT => ['n', [self::BPMN_MODEL, IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT]], + IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT_SET => ['1', [self::BPMN_MODEL, IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT_SET]], + IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT_ASSOCIATION => ['n', [self::BPMN_MODEL, IntermediateThrowEventInterface::BPMN_PROPERTY_DATA_INPUT_ASSOCIATION]], + ], + ], + ThrowEventInterface::BPMN_PROPERTY_DATA_INPUT_ASSOCIATION => [ + DataInputAssociationInterface::class, + [ + DataInputAssociationInterface::BPMN_PROPERTY_TARGET_REF => ['1', [self::BPMN_MODEL, DataInputAssociationInterface::BPMN_PROPERTY_TARGET_REF]], + DataInputAssociationInterface::BPMN_PROPERTY_SOURCES_REF => ['1', [self::BPMN_MODEL, DataInputAssociationInterface::BPMN_PROPERTY_SOURCES_REF]], + DataInputAssociationInterface::BPMN_PROPERTY_ASSIGNMENT => ['n', [self::BPMN_MODEL, DataInputAssociationInterface::BPMN_PROPERTY_ASSIGNMENT]], + DataInputAssociationInterface::BPMN_PROPERTY_TRANSFORMATION => ['1', [self::BPMN_MODEL, DataInputAssociationInterface::BPMN_PROPERTY_TRANSFORMATION]], + ], + ], + DataInputAssociationInterface::BPMN_PROPERTY_TARGET_REF => [self::IS_REFERENCE_OPTIONAL, []], + DataInputAssociationInterface::BPMN_PROPERTY_SOURCES_REF => [self::IS_REFERENCE_OPTIONAL, []], + DataInputAssociationInterface::BPMN_PROPERTY_TRANSFORMATION => [ + FormalExpressionInterface::class, + [ + FormalExpressionInterface::BPMN_PROPERTY_BODY => ['1', self::DOM_ELEMENT_BODY], + ], + ], + DataInputAssociationInterface::BPMN_PROPERTY_ASSIGNMENT => [ + AssignmentInterface::class, + [ + AssignmentInterface::BPMN_PROPERTY_FROM => ['1', [self::BPMN_MODEL, AssignmentInterface::BPMN_PROPERTY_FROM]], + AssignmentInterface::BPMN_PROPERTY_TO => ['1', [self::BPMN_MODEL, AssignmentInterface::BPMN_PROPERTY_TO]], + ], + ], + AssignmentInterface::BPMN_PROPERTY_FROM => [ + FormalExpressionInterface::class, + [ + FormalExpressionInterface::BPMN_PROPERTY_BODY => ['1', self::DOM_ELEMENT_BODY], + ], + ], + AssignmentInterface::BPMN_PROPERTY_TO => [ + FormalExpressionInterface::class, + [ + FormalExpressionInterface::BPMN_PROPERTY_BODY => ['1', self::DOM_ELEMENT_BODY], + ], + ], + CatchEventInterface::BPMN_PROPERTY_DATA_OUTPUT_ASSOCIATION => [ + DataOutputAssociationInterface::class, + [ + DataOutputAssociationInterface::BPMN_PROPERTY_TARGET_REF => ['1', [self::BPMN_MODEL, DataOutputAssociationInterface::BPMN_PROPERTY_TARGET_REF]], + DataOutputAssociationInterface::BPMN_PROPERTY_SOURCES_REF => ['1', [self::BPMN_MODEL, DataOutputAssociationInterface::BPMN_PROPERTY_SOURCES_REF]], + DataOutputAssociationInterface::BPMN_PROPERTY_ASSIGNMENT => ['n', [self::BPMN_MODEL, DataOutputAssociationInterface::BPMN_PROPERTY_ASSIGNMENT]], + DataOutputAssociationInterface::BPMN_PROPERTY_TRANSFORMATION => ['1', [self::BPMN_MODEL, DataOutputAssociationInterface::BPMN_PROPERTY_TRANSFORMATION]], ], ], 'signalEventDefinition' => [ @@ -414,6 +469,9 @@ class BpmnDocument extends DOMDocument implements BpmnDocumentInterface BoundaryEventInterface::BPMN_PROPERTY_ATTACHED_TO => ['1', [self::BPMN_MODEL, BoundaryEventInterface::BPMN_PROPERTY_ATTACHED_TO_REF]], CatchEventInterface::BPMN_PROPERTY_EVENT_DEFINITIONS => ['n', EventDefinitionInterface::class], FlowNodeInterface::BPMN_PROPERTY_OUTGOING => ['n', [self::BPMN_MODEL, FlowNodeInterface::BPMN_PROPERTY_OUTGOING]], + CatchEventInterface::BPMN_PROPERTY_DATA_OUTPUT => ['n', [self::BPMN_MODEL, CatchEventInterface::BPMN_PROPERTY_DATA_OUTPUT]], + CatchEventInterface::BPMN_PROPERTY_DATA_OUTPUT_SET => ['1', [self::BPMN_MODEL, CatchEventInterface::BPMN_PROPERTY_DATA_OUTPUT_SET]], + CatchEventInterface::BPMN_PROPERTY_DATA_OUTPUT_ASSOCIATION => ['n', [self::BPMN_MODEL, CatchEventInterface::BPMN_PROPERTY_DATA_OUTPUT_ASSOCIATION]], ], ], 'multiInstanceLoopCharacteristics' => [ @@ -488,6 +546,8 @@ class BpmnDocument extends DOMDocument implements BpmnDocumentInterface const IS_REFERENCE = 'isReference'; + const IS_REFERENCE_OPTIONAL = 'isReferenceOptional'; + const TEXT_PROPERTY = 'textProperty'; const IS_ARRAY = 'isArray'; @@ -645,7 +705,7 @@ public function hasBpmnInstance($id) * * @return \ProcessMaker\Nayra\Contracts\Bpmn\EntityInterface */ - public function getElementInstanceById($id) + public function getElementInstanceById($id, ?bool $isOptional = false) { $this->bpmnElements[$id] = isset($this->bpmnElements[$id]) ? $this->bpmnElements[$id] @@ -654,7 +714,7 @@ public function getElementInstanceById($id) ? $element->getBpmnElementInstance() : null ); - if ($this->bpmnElements[$id] === null) { + if ($this->bpmnElements[$id] === null && empty($element) && !$isOptional) { throw new ElementNotFoundException($id); } diff --git a/src/ProcessMaker/Nayra/Storage/BpmnElement.php b/src/ProcessMaker/Nayra/Storage/BpmnElement.php index c7b71e17..121736d4 100644 --- a/src/ProcessMaker/Nayra/Storage/BpmnElement.php +++ b/src/ProcessMaker/Nayra/Storage/BpmnElement.php @@ -44,9 +44,11 @@ public function getBpmnElementInstance($owner = null) return null; } list($classInterface, $mapProperties) = $map[$this->namespaceURI][$this->localName]; - if ($classInterface === BpmnDocument::IS_REFERENCE) { - $bpmnElement = $this->ownerDocument->getElementInstanceById($this->nodeValue); - $this->bpmn = $bpmnElement; + if ($classInterface === BpmnDocument::IS_REFERENCE || $classInterface === BpmnDocument::IS_REFERENCE_OPTIONAL) { + $bpmnElement = $this->ownerDocument->getElementInstanceById( + $this->nodeValue, + $classInterface === BpmnDocument::IS_REFERENCE_OPTIONAL + ); } elseif ($classInterface === BpmnDocument::TEXT_PROPERTY) { $bpmnElement = $this->nodeValue; $owner->setProperty($this->nodeName, $this->nodeValue); diff --git a/tests/Feature/Patterns/files/MessageEventToParent.bpmn b/tests/Feature/Patterns/files/MessageEventToParent.bpmn new file mode 100644 index 00000000..c3e777ff --- /dev/null +++ b/tests/Feature/Patterns/files/MessageEventToParent.bpmn @@ -0,0 +1,201 @@ + + + + + + + + + + SequenceFlow_0h21x7r + + + Flow_148wtcr + + + SequenceFlow_0h21x7r + Flow_1r89qx6 + + + Flow_1r89qx6 + Flow_148wtcr + + + Flow_0azzp4i + + + + + + + Flow_0azzp4i + + + + + + Flow_1a3yzli + + + Flow_09pcqxs + + + Flow_1a3yzli + Flow_0ntd3ys + + + + Flow_1r1agfn + Flow_09pcqxs + + + + + + + + Flow_0ntd3ys + Flow_1r1agfn + + + + + + + din_email + ds_email + + + + din_login + + + + + fullName + + + + + din_status + + + + status.name + + + + + status.description + + + + + din_email + din_login + din_status + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Feature/Patterns/files/MessageEventToParent.json b/tests/Feature/Patterns/files/MessageEventToParent.json new file mode 100644 index 00000000..19a447ad --- /dev/null +++ b/tests/Feature/Patterns/files/MessageEventToParent.json @@ -0,0 +1,23 @@ +[ + { + "comment": "Message event sent multiple data inputs to parent", + "startEvent": "StartEvent", + "data": { + }, + "result": [ + "Task_B", + "Task_D", + "Task_E", + "Task_F" + ], + "output": { + "email": "admin@example.com", + "login": "ADMIN", + "fullName": "admin user", + "status": { + "name": "COMPLETED", + "description": "The task has been completed" + } + } + } +] diff --git a/tests/Feature/Patterns/files/MessageEventToParentCatchMapped.bpmn b/tests/Feature/Patterns/files/MessageEventToParentCatchMapped.bpmn new file mode 100644 index 00000000..e8ccf28d --- /dev/null +++ b/tests/Feature/Patterns/files/MessageEventToParentCatchMapped.bpmn @@ -0,0 +1,223 @@ + + + + + + + + + + + SequenceFlow_0h21x7r + + + Flow_148wtcr + + + SequenceFlow_0h21x7r + Flow_1r89qx6 + + + Flow_1r89qx6 + Flow_148wtcr + + + Flow_0azzp4i + + + + + + + Flow_0azzp4i + + + + + ds_status + + status + status + + + + ds_client + + $data['email'], "login" => $data['login'], "fullName" => $data['fullName']]]]> + client + + + + ds_client + ds_status + + + + + + + Flow_1a3yzli + + + Flow_09pcqxs + + + Flow_1a3yzli + Flow_0ntd3ys + + + + Flow_1r1agfn + Flow_09pcqxs + + + + + + + + Flow_0ntd3ys + Flow_1r1agfn + + + + + + + din_email + ds_email + + + + din_login + + + + + fullName + + + + + din_status + + + + status.name + + + + + status.description + + + + + din_email + din_login + din_status + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Feature/Patterns/files/MessageEventToParentCatchMapped.json b/tests/Feature/Patterns/files/MessageEventToParentCatchMapped.json new file mode 100644 index 00000000..4d199c2e --- /dev/null +++ b/tests/Feature/Patterns/files/MessageEventToParentCatchMapped.json @@ -0,0 +1,25 @@ +[ + { + "comment": "Message event sent multiple data inputs to parent", + "startEvent": "StartEvent", + "data": { + }, + "result": [ + "Task_B", + "Task_D", + "Task_E", + "Task_F" + ], + "output": { + "client": { + "email": "admin@example.com", + "login": "ADMIN", + "fullName": "admin user" + }, + "status": { + "name": "COMPLETED", + "description": "The task has been completed" + } + } + } +] diff --git a/tests/unit/ProcessMaker/Nayra/Bpmn/DataStoreTest.php b/tests/unit/ProcessMaker/Nayra/Bpmn/DataStoreTest.php index 1cda857c..eb36138a 100644 --- a/tests/unit/ProcessMaker/Nayra/Bpmn/DataStoreTest.php +++ b/tests/unit/ProcessMaker/Nayra/Bpmn/DataStoreTest.php @@ -35,4 +35,209 @@ public function testDataStoreSettersAndGetters() //Assertion: the data store should have a non initialized item subject $this->assertNull($dataStore->getItemSubject()); } + + /** + * Tests the setDotData function with various scenarios + */ + public function testSetDotData() + { + $dataStore = $this->repository->createDataStore(); + + // Test simple dot notation + $dataStore->setDotData('user.name', 'John Doe'); + $userData = $dataStore->getData('user'); + $this->assertEquals('John Doe', $userData['name']); + + // Test nested dot notation + $dataStore->setDotData('user.profile.email', 'john@example.com'); + $userData = $dataStore->getData('user'); + $this->assertEquals('john@example.com', $userData['profile']['email']); + + // Test deeply nested structure + $dataStore->setDotData('company.departments.engineering.team.lead', 'Jane Smith'); + $companyData = $dataStore->getData('company'); + $this->assertEquals('Jane Smith', $companyData['departments']['engineering']['team']['lead']); + + // Test numeric keys + $dataStore->setDotData('items.0.name', 'First Item'); + $dataStore->setDotData('items.1.name', 'Second Item'); + $itemsData = $dataStore->getData('items'); + $this->assertEquals('First Item', $itemsData[0]['name']); + $this->assertEquals('Second Item', $itemsData[1]['name']); + + // Test numeric final key (to cover the is_numeric check for finalKey) + $dataStore->setDotData('scores.0', 100); + $dataStore->setDotData('scores.1', 200); + $scoresData = $dataStore->getData('scores'); + $this->assertEquals(100, $scoresData[0]); + $this->assertEquals(200, $scoresData[1]); + + // Test overwriting existing values + $dataStore->setDotData('user.name', 'Jane Doe'); + $userData = $dataStore->getData('user'); + $this->assertEquals('Jane Doe', $userData['name']); + + // Test setting complex values + $complexValue = ['type' => 'admin', 'permissions' => ['read', 'write']]; + $dataStore->setDotData('user.role', $complexValue); + $userData = $dataStore->getData('user'); + $this->assertEquals($complexValue, $userData['role']); + + // Test setting null values + $dataStore->setDotData('user.middleName', null); + $userData = $dataStore->getData('user'); + $this->assertNull($userData['middleName']); + + // Test setting boolean values + $dataStore->setDotData('user.active', true); + $userData = $dataStore->getData('user'); + $this->assertTrue($userData['active']); + + // Test setting numeric values + $dataStore->setDotData('user.age', 30); + $userData = $dataStore->getData('user'); + $this->assertEquals(30, $userData['age']); + + // Test that the method returns the data store instance for chaining + $result = $dataStore->setDotData('test.chain', 'value'); + $this->assertSame($dataStore, $result); + + // Verify the complete data structure + $expectedData = [ + 'user' => [ + 'name' => 'Jane Doe', + 'profile' => [ + 'email' => 'john@example.com' + ], + 'role' => [ + 'type' => 'admin', + 'permissions' => ['read', 'write'] + ], + 'middleName' => null, + 'active' => true, + 'age' => 30 + ], + 'company' => [ + 'departments' => [ + 'engineering' => [ + 'team' => [ + 'lead' => 'Jane Smith' + ] + ] + ] + ], + 'items' => [ + 0 => [ + 'name' => 'First Item' + ], + 1 => [ + 'name' => 'Second Item' + ] + ], + 'scores' => [ + 0 => 100, + 1 => 200 + ], + 'test' => [ + 'chain' => 'value' + ] + ]; + + $this->assertEquals($expectedData, $dataStore->getData()); + } + + /** + * Tests the getDotData function with various scenarios + */ + public function testGetDotData() + { + $dataStore = $this->repository->createDataStore(); + + // Set up test data using setDotData + $dataStore->setDotData('user.name', 'John Doe'); + $dataStore->setDotData('user.profile.email', 'john@example.com'); + $dataStore->setDotData('user.profile.age', 30); + $dataStore->setDotData('user.active', true); + $dataStore->setDotData('user.role', ['type' => 'admin', 'permissions' => ['read', 'write']]); + $dataStore->setDotData('company.departments.engineering.team.lead', 'Jane Smith'); + $dataStore->setDotData('items.0.name', 'First Item'); + $dataStore->setDotData('items.1.name', 'Second Item'); + $dataStore->setDotData('scores.0', 100); + $dataStore->setDotData('scores.1', 200); + $dataStore->setDotData('user.middleName', null); + + // Test simple dot notation retrieval + $this->assertEquals('John Doe', $dataStore->getDotData('user.name')); + $this->assertEquals('john@example.com', $dataStore->getDotData('user.profile.email')); + $this->assertEquals(30, $dataStore->getDotData('user.profile.age')); + $this->assertTrue($dataStore->getDotData('user.active')); + + // Test nested dot notation retrieval + $this->assertEquals('Jane Smith', $dataStore->getDotData('company.departments.engineering.team.lead')); + + // Test numeric keys + $this->assertEquals('First Item', $dataStore->getDotData('items.0.name')); + $this->assertEquals('Second Item', $dataStore->getDotData('items.1.name')); + + // Test numeric final keys + $this->assertEquals(100, $dataStore->getDotData('scores.0')); + $this->assertEquals(200, $dataStore->getDotData('scores.1')); + + // Test complex values + $expectedRole = ['type' => 'admin', 'permissions' => ['read', 'write']]; + $this->assertEquals($expectedRole, $dataStore->getDotData('user.role')); + + // Test null values + $this->assertNull($dataStore->getDotData('user.middleName')); + + // Test non-existent paths with default values + $this->assertNull($dataStore->getDotData('non.existent.path')); + $this->assertEquals('default', $dataStore->getDotData('non.existent.path', 'default')); + $this->assertEquals('fallback', $dataStore->getDotData('user.nonExistent', 'fallback')); + + // Test partial path that doesn't exist + $this->assertNull($dataStore->getDotData('user.profile.nonExistent')); + $this->assertEquals('not found', $dataStore->getDotData('user.profile.nonExistent', 'not found')); + + // Test deeply nested non-existent path + $this->assertNull($dataStore->getDotData('company.departments.marketing.team.lead')); + $this->assertEquals('no lead', $dataStore->getDotData('company.departments.marketing.team.lead', 'no lead')); + + // Test numeric key that doesn't exist + $this->assertNull($dataStore->getDotData('items.2.name')); + $this->assertEquals('missing', $dataStore->getDotData('items.2.name', 'missing')); + + // Test empty path + $this->assertNull($dataStore->getDotData('')); + $this->assertEquals('empty', $dataStore->getDotData('', 'empty')); + + // Test single key that exists (returns the entire user array) + $userData = $dataStore->getDotData('user'); + $this->assertIsArray($userData); + $this->assertEquals('John Doe', $userData['name']); + $this->assertEquals('john@example.com', $userData['profile']['email']); + $this->assertEquals(30, $userData['profile']['age']); + $this->assertTrue($userData['active']); + $this->assertNull($userData['middleName']); + + // Test single key that doesn't exist + $this->assertNull($dataStore->getDotData('nonexistent')); + $this->assertEquals('not found', $dataStore->getDotData('nonexistent', 'not found')); + + // Test boolean false value + $dataStore->setDotData('user.disabled', false); + $this->assertFalse($dataStore->getDotData('user.disabled')); + + // Test zero value + $dataStore->setDotData('user.score', 0); + $this->assertEquals(0, $dataStore->getDotData('user.score')); + + // Test empty string + $dataStore->setDotData('user.description', ''); + $this->assertEquals('', $dataStore->getDotData('user.description')); + + // Test empty array + $dataStore->setDotData('user.tags', []); + $this->assertEquals([], $dataStore->getDotData('user.tags')); + } }