Repository: ProcessMaker/nayra (php)

Issue:
Implement the Service Task Element
The Service Task inherits the attributes and model associations of an Activity, and includes the following additional properties defined in the standard:

**implementation** (String): This attribute specifies the technology that will be used to identify the  technology or coordination protocol that will execute the service.

![image](https://user-images.githubusercontent.com/8028650/48134158-7d864980-e26f-11e8-90ec-a714536503eb.png)


**Acceptance Criteria**
The element should pass the following tests:
- The element must work as an flow element
- The element could be loaded from a BPMN file
- The implementation property should be accessible
- The element should be callable like the script task
- The element could have a failure state


Pull request description:
Add Service Task flow element. Resolves #115
The following changes were make to implement the Service Task element:

- Created a Model class for the element
- Created an Interface class for the element
- Added to the BPMN Loader
- Added a getter for the implementation attribute

Add unit tests to:
- Load from a BPMN File
- Execute a process with Service Task
- Execute a process with Service Task and catch an exception state


Patch:
diff --git a/src/ProcessMaker/Nayra/Bpmn/Models/ServiceTask.php b/src/ProcessMaker/Nayra/Bpmn/Models/ServiceTask.php
new file mode 100644
index 0000000..98c995e
--- /dev/null
+++ b/src/ProcessMaker/Nayra/Bpmn/Models/ServiceTask.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace ProcessMaker\Nayra\Bpmn\Models;
+
+use Exception;
+use ProcessMaker\Nayra\Bpmn\ActivityTrait;
+use ProcessMaker\Nayra\Bpmn\Events\ActivityActivatedEvent;
+use ProcessMaker\Nayra\Bpmn\Events\ActivityClosedEvent;
+use ProcessMaker\Nayra\Bpmn\Events\ActivityCompletedEvent;
+use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
+use ProcessMaker\Nayra\Contracts\Bpmn\ServiceTaskInterface;
+use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface;
+
+/**
+ * This activity will raise an exception when executed.
+ *
+ */
+class ServiceTask implements ServiceTaskInterface
+{
+
+    use ActivityTrait;
+
+    /**
+     * Initialize the service task
+     *
+     */
+    protected function initServiceTask()
+    {
+        $this->attachEvent(
+            ActivityInterface::EVENT_ACTIVITY_ACTIVATED,
+            function ($self, TokenInterface $token) {
+                $this->notifyEvent(ServiceTaskInterface::EVENT_SERVICE_TASK_ACTIVATED, $this, $token);
+            }
+        );
+    }
+
+    /**
+     * Array map of custom event classes for the bpmn element.
+     *
+     * @return array
+     */
+    protected function getBpmnEventClasses()
+    {
+        return [
+            ActivityInterface::EVENT_ACTIVITY_ACTIVATED => ActivityActivatedEvent::class,
+            ActivityInterface::EVENT_ACTIVITY_COMPLETED => ActivityCompletedEvent::class,
+            ActivityInterface::EVENT_ACTIVITY_CLOSED => ActivityClosedEvent::class,
+        ];
+    }
+
+    /**
+     * Sets the service task implementation
+     *
+     * @param mixed $implementation
+     *
+     * @return $this
+     */
+    public function setImplementation($implementation)
+    {
+        $this->setProperty(ServiceTaskInterface::BPMN_PROPERTY_IMPLEMENTATION, $implementation);
+        return $this;
+    }
+
+    /**
+     * Returns the service task implementation
+     *
+     * @return mixed
+     */
+    public function getImplementation()
+    {
+        return $this->getProperty(ServiceTaskInterface::BPMN_PROPERTY_IMPLEMENTATION);
+    }
+
+    /**
+     * Runs the Service Task
+     *
+     * @param TokenInterface $token
+     *
+     * @return $this
+     */
+    public function run(TokenInterface $token)
+    {
+        //if the script runs correctly complete te activity, otherwise set the token to failed state
+        if ($this->executeService($token, $this->getImplementation())) {
+            $this->complete($token);
+        }
+        else {
+            $token->setStatus(ActivityInterface::TOKEN_STATE_FAILING);
+        }
+        return $this;
+    }
+
+    /**
+     * Service Task runner for testing purposes
+     *
+     * @param TokenInterface $token
+     * @param mixed $implementation
+     *
+     * @return bool
+     */
+    private function executeService(TokenInterface $token, $implementation)
+    {
+        $result = true;
+        try {
+            $callable = is_string($implementation) && strpos($implementation, '@')
+                ? explode('@', $implementation) : $implementation;
+            call_user_func($callable);
+        }
+        catch (Exception $exception) {
+            $result = false;
+        }
+        return $result;
+    }
+
+}
diff --git a/src/ProcessMaker/Nayra/Contracts/Bpmn/ServiceTaskInterface.php b/src/ProcessMaker/Nayra/Contracts/Bpmn/ServiceTaskInterface.php
new file mode 100644
index 0000000..c419cc8
--- /dev/null
+++ b/src/ProcessMaker/Nayra/Contracts/Bpmn/ServiceTaskInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace ProcessMaker\Nayra\Contracts\Bpmn;
+
+/**
+ * Defines the interface to be used by the ScriptTasks
+ *
+ * @package ProcessMaker\Nayra\Contracts\Bpmn
+ */
+interface ServiceTaskInterface extends ActivityInterface
+{
+
+    const BPMN_PROPERTY_IMPLEMENTATION = 'implementation';
+    const EVENT_SERVICE_TASK_ACTIVATED = 'ServiceTaskActivated';
+
+    /**
+     * Sets the service task implementation
+     *
+     * @param mixed $implementation
+     *
+     * @return $this
+     */
+    public function setImplementation($implementation);
+
+    /**
+     * Returns the service task implementation
+     *
+     * @return mixed
+     */
+    public function getImplementation();
+
+    /**
+     * Runs the Service Task
+     *
+     * @param TokenInterface $token
+     *
+     * @return $this
+     */
+    public function run(TokenInterface $token);
+}
diff --git a/src/ProcessMaker/Nayra/Contracts/Repositories/StorageInterface.php b/src/ProcessMaker/Nayra/Contracts/Repositories/StorageInterface.php
index 49963cb..284de62 100755
--- a/src/ProcessMaker/Nayra/Contracts/Repositories/StorageInterface.php
+++ b/src/ProcessMaker/Nayra/Contracts/Repositories/StorageInterface.php
@@ -392,6 +392,15 @@ interface StorageInterface
      */
     public function getScriptTask($id);
 
+    /**
+     * Get ServiceTask instance by id.
+     *
+     * @param string $id
+     *
+     * @return \ProcessMaker\Nayra\Contracts\Bpmn\ServiceTaskInterface
+     */
+    public function getServiceTask($id);
+
     /**
      * Get SignalEventDefinition instance by id.
      *
diff --git a/src/ProcessMaker/Nayra/RepositoryTrait.php b/src/ProcessMaker/Nayra/RepositoryTrait.php
index d3e81fe..f29c8f4 100644
--- a/src/ProcessMaker/Nayra/RepositoryTrait.php
+++ b/src/ProcessMaker/Nayra/RepositoryTrait.php
@@ -30,15 +30,14 @@ use ProcessMaker\Nayra\Bpmn\Models\ParallelGateway;
 use ProcessMaker\Nayra\Bpmn\Models\Participant;
 use ProcessMaker\Nayra\Bpmn\Models\Process;
 use ProcessMaker\Nayra\Bpmn\Models\ScriptTask;
+use ProcessMaker\Nayra\Bpmn\Models\ServiceTask;
 use ProcessMaker\Nayra\Bpmn\Models\Signal;
 use ProcessMaker\Nayra\Bpmn\Models\SignalEventDefinition;
 use ProcessMaker\Nayra\Bpmn\Models\StartEvent;
 use ProcessMaker\Nayra\Bpmn\Models\TerminateEventDefinition;
 use ProcessMaker\Nayra\Bpmn\Models\TimerEventDefinition;
-use ProcessMaker\Nayra\Bpmn\Models\Token;
 use ProcessMaker\Nayra\Bpmn\State;
 use ProcessMaker\Nayra\Contracts\Bpmn\FlowNodeInterface;
-use ProcessMaker\Test\Models\InstanceRepository;
 use ProcessMaker\Test\Models\TokenRepository;
 
 /**
@@ -341,6 +340,16 @@ trait RepositoryTrait
         return new ScriptTask();
     }
 
+    /**
+     * Create instance of ServiceTask.
+     *
+     * @return \ProcessMaker\Nayra\Contracts\Bpmn\ServiceTaskInterface
+     */
+    public function createServiceTask()
+    {
+        return new ServiceTask();
+    }
+
     /**
      * Create instance of SignalEventDefinition.
      *
diff --git a/src/ProcessMaker/Nayra/Storage/BpmnDocument.php b/src/ProcessMaker/Nayra/Storage/BpmnDocument.php
index 004bc93..22222bb 100644
--- a/src/ProcessMaker/Nayra/Storage/BpmnDocument.php
+++ b/src/ProcessMaker/Nayra/Storage/BpmnDocument.php
@@ -40,6 +40,7 @@ use ProcessMaker\Nayra\Contracts\Bpmn\ParticipantInterface;
 use ProcessMaker\Nayra\Contracts\Bpmn\ProcessInterface;
 use ProcessMaker\Nayra\Contracts\Bpmn\ScriptTaskInterface;
 use ProcessMaker\Nayra\Contracts\Bpmn\ServiceInterface;
+use ProcessMaker\Nayra\Contracts\Bpmn\ServiceTaskInterface;
 use ProcessMaker\Nayra\Contracts\Bpmn\SignalEventDefinitionInterface;
 use ProcessMaker\Nayra\Contracts\Bpmn\SignalInterface;
 use ProcessMaker\Nayra\Contracts\Bpmn\StartEventInterface;
@@ -123,6 +124,13 @@ class BpmnDocument extends DOMDocument implements BpmnDocumentInterface
                     ScriptTaskInterface::BPMN_PROPERTY_SCRIPT => ['1', [BpmnDocument::BPMN_MODEL, ScriptTaskInterface::BPMN_PROPERTY_SCRIPT]],
                 ]
             ],
+            'serviceTask'   => [
+                ServiceTaskInterface::class,
+                [
+                    FlowNodeInterface::BPMN_PROPERTY_INCOMING => ['n', [BpmnDocument::BPMN_MODEL, FlowNodeInterface::BPMN_PROPERTY_INCOMING]],
+                    FlowNodeInterface::BPMN_PROPERTY_OUTGOING => ['n', [BpmnDocument::BPMN_MODEL, FlowNodeInterface::BPMN_PROPERTY_OUTGOING]],
+                ]
+            ],
             FlowNodeInterface::BPMN_PROPERTY_OUTGOING     => [self::IS_REFERENCE, []],
             FlowNodeInterface::BPMN_PROPERTY_INCOMING     => [self::IS_REFERENCE, []],
             'sequenceFlow' => [
@@ -900,6 +908,18 @@ class BpmnDocument extends DOMDocument implements BpmnDocumentInterface
         return $this->getElementInstanceById($id);
     }
 
+    /**
+     * Get ServiceTask instance by id.
+     *
+     * @param string $id
+     *
+     * @return \ProcessMaker\Nayra\Contracts\Bpmn\ServiceTaskInterface
+     */
+    public function getServiceTask($id)
+    {
+        return $this->getElementInstanceById($id);
+    }
+
     /**
      * Get SignalEventDefinition instance by id.
      *


Test patch:
diff --git a/tests/Feature/Engine/ServiceTaskTest.php b/tests/Feature/Engine/ServiceTaskTest.php
new file mode 100644
index 0000000..93a33ac
--- /dev/null
+++ b/tests/Feature/Engine/ServiceTaskTest.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Tests\Feature\Engine;
+
+use Exception;
+use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
+use ProcessMaker\Nayra\Contracts\Bpmn\EndEventInterface;
+use ProcessMaker\Nayra\Contracts\Bpmn\EventInterface;
+use ProcessMaker\Nayra\Contracts\Bpmn\ProcessInterface;
+use ProcessMaker\Nayra\Contracts\Bpmn\ServiceTaskInterface;
+use ProcessMaker\Nayra\Storage\BpmnDocument;
+
+/**
+ * Tests for the ServiceTask element
+ *
+ */
+class ServiceTaskTest extends EngineTestCase
+{
+
+    // Property that is increased when the service task is executed.
+    private static $serviceCalls = 0;
+
+    /**
+     * Tests the a process with the sequence start->serviceTask->End executes correctly
+     */
+    public function testProcessWithServiceTask()
+    {
+        //Load a BpmnFile Repository
+        $bpmnRepository = new BpmnDocument();
+        $bpmnRepository->setEngine($this->engine);
+        $bpmnRepository->setFactory($this->repository);
+        $bpmnRepository->load(__DIR__ . '/files/ServiceTaskProcess.bpmn');
+
+        //Load a process from a bpmn repository by Id
+        $process = $bpmnRepository->getProcess('ServiceTaskProcess');
+        $dataStore = $this->repository->createDataStore();
+
+        //create an instance of the process
+        $instance = $this->engine->createExecutionInstance($process, $dataStore);
+
+        //Get References
+        $start = $process->getEvents()->item(0);
+        $serviceTask = $bpmnRepository->getServiceTask('_2');
+        static::$serviceCalls = 0;
+        $serviceCalls = static::$serviceCalls;
+
+        //start the process an instance of the process
+        $start->start();
+        $this->engine->runToNextState();
+
+        //Assert: that the process is stared and the service task is activated
+        $this->assertEvents([
+            ProcessInterface::EVENT_PROCESS_INSTANCE_CREATED,
+            EventInterface::EVENT_EVENT_TRIGGERED,
+            ActivityInterface::EVENT_ACTIVITY_ACTIVATED,
+            ServiceTaskInterface::EVENT_SERVICE_TASK_ACTIVATED,
+        ]);
+
+        //Execute the service task
+        $token = $serviceTask->getTokens($instance)->item(0);
+        $serviceTask->run($token);
+        $this->engine->runToNextState();
+
+        //Assertion: The service task must be completed
+        $this->assertEvents([
+            ActivityInterface::EVENT_ACTIVITY_COMPLETED,
+            ActivityInterface::EVENT_ACTIVITY_CLOSED,
+            EndEventInterface::EVENT_THROW_TOKEN_ARRIVES,
+            EndEventInterface::EVENT_THROW_TOKEN_CONSUMED,
+            EndEventInterface::EVENT_EVENT_TRIGGERED,
+            ProcessInterface::EVENT_PROCESS_INSTANCE_COMPLETED,
+        ]);
+
+        //Assertion: Verify the method convertToDoc was called
+        $this->assertEquals($serviceCalls + 1, static::$serviceCalls);
+    }
+
+    /**
+     * Tests a process with the sequence start->serviceTask->End when fails
+     * the task goes to a failure state.
+     *
+     */
+    public function testProcessWithServiceTaskFailure()
+    {
+        //Load a BpmnFile Repository
+        $bpmnRepository = new BpmnDocument();
+        $bpmnRepository->setEngine($this->engine);
+        $bpmnRepository->setFactory($this->repository);
+        $bpmnRepository->load(__DIR__ . '/files/ServiceTaskProcess.bpmn');
+
+        //Load a process from a bpmn repository by Id
+        $process = $bpmnRepository->getProcess('ServiceTaskProcess');
+        $dataStore = $this->repository->createDataStore();
+
+        //create an instance of the process
+        $instance = $this->engine->createExecutionInstance($process, $dataStore);
+
+        //Get References
+        $start = $process->getEvents()->item(0);
+        $serviceTask = $bpmnRepository->getServiceTask('_2');
+        
+        //Set to the limit of the service calls
+        static::$serviceCalls = 10;
+        $serviceCalls = static::$serviceCalls;
+
+        //start the process an instance of the process
+        $start->start();
+        $this->engine->runToNextState();
+
+        //Assert: that the process is stared and the service task is activated
+        $this->assertEvents([
+            ProcessInterface::EVENT_PROCESS_INSTANCE_CREATED,
+            EventInterface::EVENT_EVENT_TRIGGERED,
+            ActivityInterface::EVENT_ACTIVITY_ACTIVATED,
+            ServiceTaskInterface::EVENT_SERVICE_TASK_ACTIVATED,
+        ]);
+
+        //Execute the service task
+        $token = $serviceTask->getTokens($instance)->item(0);
+        $serviceTask->run($token);
+        $this->engine->runToNextState();
+
+        //Assertion: The service task must go to exception state
+        $this->assertEvents([
+            ActivityInterface::EVENT_ACTIVITY_EXCEPTION,
+        ]);
+    }
+
+    /**
+     * A public function that is called by the Service Task.
+     *
+     */
+    public static function convertToDoc()
+    {
+        if (static::$serviceCalls === 10) {
+            throw new Exception('Limit of executions reached.');
+        }
+        static::$serviceCalls++;
+    }
+}
diff --git a/tests/Feature/Engine/files/ServiceTaskProcess.bpmn b/tests/Feature/Engine/files/ServiceTaskProcess.bpmn
new file mode 100644
index 0000000..8828ca3
--- /dev/null
+++ b/tests/Feature/Engine/files/ServiceTaskProcess.bpmn
@@ -0,0 +1,60 @@
+﻿<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:tns="http://sourceforge.net/bpmn/definitions/_1541612237879" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:yaoqiang="http://bpmn.sourceforge.net" exporter="Yaoqiang BPMN Editor" exporterVersion="5.3" expressionLanguage="http://www.w3.org/1999/XPath" id="_1541612237879" name="" targetNamespace="http://sourceforge.net/bpmn/definitions/_1541612237879" typeLanguage="http://www.w3.org/2001/XMLSchema" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://bpmn.sourceforge.net/schemas/BPMN20.xsd">
+  <process id="ServiceTaskProcess" isClosed="false" isExecutable="true" processType="None">
+    <extensionElements>
+      <yaoqiang:description/>
+      <yaoqiang:pageFormat height="841.8897637795276" imageableHeight="831.8897637795276" imageableWidth="588.1102362204724" imageableX="5.0" imageableY="5.0" orientation="0" width="598.1102362204724"/>
+      <yaoqiang:page background="#FFFFFF" horizontalCount="1" verticalCount="1"/>
+    </extensionElements>
+    <serviceTask completionQuantity="1" id="_2" implementation="Tests\Feature\Engine\ServiceTaskTest@convertToDoc" isForCompensation="false" name="Service Task" startQuantity="1">
+      <incoming>_4</incoming>
+      <outgoing>_6</outgoing>
+    </serviceTask>
+    <sequenceFlow id="_4" sourceRef="_3" targetRef="_2"/>
+    <startEvent id="_3" isInterrupting="true" name="Start Event" parallelMultiple="false">
+      <outgoing>_4</outgoing>
+      <outputSet/>
+    </startEvent>
+    <endEvent id="_5" name="End Event">
+      <incoming>_6</incoming>
+      <inputSet/>
+    </endEvent>
+    <sequenceFlow id="_6" sourceRef="_2" targetRef="_5"/>
+  </process>
+  <bpmndi:BPMNDiagram id="Yaoqiang_Diagram-ServiceTaskProcess" name="Untitled Diagram" resolution="96.0">
+    <bpmndi:BPMNPlane bpmnElement="ServiceTaskProcess">
+      <bpmndi:BPMNShape bpmnElement="_2" id="Yaoqiang-_2">
+        <dc:Bounds height="55.0" width="85.0" x="149.24999999999994" y="73.5"/>
+        <bpmndi:BPMNLabel>
+          <dc:Bounds height="18.96" width="72.0" x="155.75" y="93.52"/>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="_3" id="Yaoqiang-_3">
+        <dc:Bounds height="32.0" width="32.0" x="67.0" y="85.0"/>
+        <bpmndi:BPMNLabel>
+          <dc:Bounds height="18.96" width="63.0" x="51.5" y="125.52"/>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="_5" id="Yaoqiang-_5">
+        <dc:Bounds height="32.0" width="32.0" x="280.0" y="85.0"/>
+        <bpmndi:BPMNLabel>
+          <dc:Bounds height="18.96" width="58.0" x="267.0" y="125.52"/>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge bpmnElement="_6" id="Yaoqiang-_6">
+        <di:waypoint x="234.0" y="101.0"/>
+        <di:waypoint x="280.0002432976976" y="101.0"/>
+        <bpmndi:BPMNLabel>
+          <dc:Bounds height="18.96" width="6.0" x="254.0" y="91.52"/>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="_4" id="Yaoqiang-_4">
+        <di:waypoint x="98.99975670230245" y="101.0"/>
+        <di:waypoint x="149.0" y="101.0"/>
+        <bpmndi:BPMNLabel>
+          <dc:Bounds height="18.96" width="6.0" x="121.0" y="91.52"/>
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>