├── testbench.yaml
├── .gitignore
├── src
├── Exceptions
│ ├── ParseException.php
│ ├── CamundaException.php
│ ├── InvalidArgumentException.php
│ ├── ObjectNotFoundException.php
│ └── UnexpectedResponseException.php
├── Dto
│ ├── Tenant.php
│ ├── VariableType
│ │ ├── BooleanType.php
│ │ ├── StringType.php
│ │ ├── JsonType.php
│ │ └── ObjectType.php
│ ├── Variable.php
│ ├── ProcessInstance.php
│ ├── ProcessDefinition.php
│ ├── Deployment.php
│ ├── ExternalTask.php
│ ├── Task.php
│ ├── ProcessInstanceHistory.php
│ └── TaskHistory.php
├── ServiceProvider.php
├── Collections
│ └── VariableCollection.php
├── Http
│ ├── CamundaClient.php
│ ├── TaskHistoryClient.php
│ ├── MessageEventClient.php
│ ├── TenantClient.php
│ ├── ProcessInstanceHistoryClient.php
│ ├── ProcessDefinitionClient.php
│ ├── DeploymentClient.php
│ ├── ProcessInstanceClient.php
│ ├── ExternalTaskClient.php
│ └── TaskClient.php
├── Commands
│ └── ConsumeExternalTaskCommand.php
└── BpmnReader.php
├── tests
├── BpmnTest.php
├── CamundaClientTest.php
├── TestCase.php
├── VariableTest.php
├── Dto
│ └── TaskDtoTest.php
├── DeploymentTest.php
├── ProcessDefinitionTest.php
├── ExternalTaskTest.php
├── TaskTest.php
└── ProcessInstanceTest.php
├── phpunit.dist.xml
├── composer.json
├── resources
├── invalid.bpmn
├── external-task.bpmn
├── sample.bpmn
├── sample2.bpmn
└── rekrutmen-sederhana.bpmn
├── .php-cs-fixer.php
└── README.md
/testbench.yaml:
--------------------------------------------------------------------------------
1 | providers:
2 | - Spatie\LaravelData\LaravelDataServiceProvider
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /reports
3 | phpunit.xml
4 | clover.xml
5 | composer.lock
6 | .phpunit.result.cache
7 | .php_cs.cache
8 | .php-cs-fixer.cache
9 |
--------------------------------------------------------------------------------
/src/Exceptions/ParseException.php:
--------------------------------------------------------------------------------
1 | (bool) $value, 'type' => 'Boolean'];
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Dto/VariableType/StringType.php:
--------------------------------------------------------------------------------
1 | (string) $value, 'type' => 'String'];
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Dto/Variable.php:
--------------------------------------------------------------------------------
1 | app->runningInConsole()) {
12 | $this->commands([ConsumeExternalTaskCommand::class]);
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Dto/VariableType/JsonType.php:
--------------------------------------------------------------------------------
1 | json_encode($value, JSON_THROW_ON_ERROR),
16 | 'type' => 'Json'
17 | ];
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dto/ProcessInstance.php:
--------------------------------------------------------------------------------
1 | getForms();
14 |
15 | $this->assertNotEmpty($forms);
16 | }
17 |
18 | public function test_parse_empty_form_definition(): void
19 | {
20 | $file = __DIR__.'/../resources/sample.bpmn';
21 | $reader = new BpmnReader($file);
22 | $forms = $reader->getForms();
23 |
24 | $this->assertEmpty($forms);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/CamundaClientTest.php:
--------------------------------------------------------------------------------
1 | get('version');
12 |
13 | $this->assertEquals(200, $response->status());
14 | $this->assertArrayHasKey('version', $response->json());
15 | $this->assertInstanceOf(\stdClass::class, $response->object());
16 | }
17 |
18 | public function test_call_invalid_endpoint(): void
19 | {
20 | $response = CamundaClient::make()->get('invalid-endpoint');
21 |
22 | $this->assertEquals(404, $response->status());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Dto/ProcessDefinition.php:
--------------------------------------------------------------------------------
1 | url = $url;
15 | $instance->payload = $payload;
16 | $instance->response = $response;
17 |
18 | return $instance;
19 | }
20 |
21 | public function context()
22 | {
23 | return [
24 | 'url' => $this->url,
25 | 'payload' => $this->payload,
26 | 'response' => $this->response,
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Dto/VariableType/ObjectType.php:
--------------------------------------------------------------------------------
1 | $value->toJson(),
19 | 'type' => 'Object',
20 | 'valueInfo' => [
21 | 'objectTypeName' => 'java.util.Collection',
22 | 'serializationDataFormat' => 'application/json',
23 | ],
24 | ];
25 | }
26 |
27 | return [
28 | 'value' => json_encode($value, JSON_THROW_ON_ERROR),
29 | 'type' => 'Object',
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Dto/Deployment.php:
--------------------------------------------------------------------------------
1 | items as $key => $value) {
17 | $valueType = gettype($value);
18 | $typeClass = match ($valueType) {
19 | 'array' => JsonType::class,
20 | 'boolean' => BooleanType::class,
21 | 'object' => ObjectType::class,
22 | default => StringType::class,
23 | };
24 |
25 | $variables[$key] = (new $typeClass())($value);
26 | }
27 |
28 | return $variables;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/phpunit.dist.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | src
10 |
11 |
12 | src/ServiceProvider.php
13 |
14 |
15 |
16 |
17 | tests
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | set('app.name', env('APP_NAME'));
23 | $app['config']->set('app.env', env('APP_ENV'));
24 | $app['config']->set('services.camunda.url', env('CAMUNDA_URL'));
25 | $app['config']->set('services.camunda.user', env('CAMUNDA_USER'));
26 | $app['config']->set('services.camunda.password', env('CAMUNDA_PASSWORD'));
27 | }
28 |
29 | protected function deploySampleBpmn(): Deployment
30 | {
31 | $files = [__DIR__.'/../resources/sample.bpmn'];
32 |
33 |
34 | return DeploymentClient::create('process_1', $files);
35 | }
36 |
37 | protected function truncateDeployment(): void
38 | {
39 | DeploymentClient::truncate(true);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Dto/ExternalTask.php:
--------------------------------------------------------------------------------
1 | =8.1",
17 | "ext-simplexml": "*",
18 | "guzzlehttp/guzzle": "^7.2",
19 | "spatie/laravel-data": "^3.0|^4.0"
20 | },
21 | "require-dev": {
22 | "orchestra/testbench": "^8.0",
23 | "brianium/paratest": "^6.2",
24 | "sempro/phpunit-pretty-print": "^1.4"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "Laravolt\\Camunda\\": "src"
29 | }
30 | },
31 | "autoload-dev": {
32 | "psr-4": {
33 | "Laravolt\\Camunda\\Tests\\": "tests"
34 | }
35 | },
36 | "extra": {
37 | "branch-alias": {
38 | "dev-master": "2.x-dev"
39 | },
40 | "laravel": {
41 | "providers": [
42 | "Laravolt\\Camunda\\ServiceProvider"
43 | ]
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/VariableTest.php:
--------------------------------------------------------------------------------
1 | deploySampleBpmn();
15 | }
16 |
17 | public function test_automatically_format_variables()
18 | {
19 | $rawVariables = [
20 | 'title' => 'Some String',
21 | 'isActive' => true,
22 | 'tags' => ['satu' => 'foo', 'dua', 'tiga'],
23 | 'keys' => collect([1, 2]),
24 | ];
25 | $variables = new VariableCollection($rawVariables);
26 | $processInstance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables->toArray());
27 | $camundaVariables = ProcessInstanceClient::variables($processInstance->id);
28 | $this->assertEquals('String', $camundaVariables['title']->type);
29 | $this->assertEquals('Boolean', $camundaVariables['isActive']->type);
30 | $this->assertEquals('Json', $camundaVariables['tags']->type);
31 | $this->assertEquals('Object', $camundaVariables['keys']->type);
32 | }
33 |
34 | protected function tearDown(): void
35 | {
36 | $this->truncateDeployment();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Dto/Task.php:
--------------------------------------------------------------------------------
1 | withBasicAuth($user, $password);
21 | }
22 |
23 | return $request;
24 | }
25 |
26 | protected static function makeIdentifierPath(string $path, array $args): string
27 | {
28 | // If no named parameters defined, we assume it is an ID
29 | if (count($args) === 1 && isset($args[0])) {
30 | $args['id'] = $args[0];
31 | }
32 |
33 | $args += ['id' => false, 'key' => false, 'tenantId' => false];
34 | $identifier = $args['id'];
35 | if ($args['key']) {
36 | $identifier = 'key/'.$args['key'];
37 | if ($args['tenantId']) {
38 | $identifier .= '/tenant-id/'.$args['tenantId'];
39 | }
40 | }
41 |
42 | if (! $identifier) {
43 | throw new InvalidArgumentException('');
44 | }
45 |
46 | return str_replace('{identifier}', $identifier, $path);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Commands/ConsumeExternalTaskCommand.php:
--------------------------------------------------------------------------------
1 | option('workerId');
24 | foreach ($subscribers as $topicName => $subscriber) {
25 | $topics[$topicName] = collect($subscriber)->only(['topicName', 'lockDuration'])->toArray();
26 | $summary[$topicName] = [$topicName, $subscriber['job'] ?? '-', 0];
27 | }
28 | $externalTasks = ExternalTaskClient::fetchAndLock($workerId, array_values($topics));
29 |
30 | /** @var \Laravolt\Camunda\Dto\ExternalTask $task */
31 | foreach ($externalTasks as $task) {
32 | $jobClass = $subscribers[$task->topicName]['job'] ?? false;
33 | if ($jobClass) {
34 | $jobClass::dispatch($workerId, $task);
35 | $summary[$task->topicName][2]++;
36 | }
37 | }
38 |
39 | $this->table(['topic', 'Job', 'Job Dispatched'], $summary);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Dto/TaskHistory.php:
--------------------------------------------------------------------------------
1 | get("history/task?taskId=$id");
15 |
16 | if ($response->status() === 200) {
17 | if (empty($response->json())) {
18 | throw new ObjectNotFoundException(sprintf('Cannot find task history with ID = %s', $id));
19 | }
20 |
21 | return TaskHistory::from(Arr::first($response->json()));
22 | }
23 |
24 | throw new CamundaException($response->json('message') ?? $response->body());
25 | }
26 |
27 | /**
28 | * @param string $processInstanceId
29 | *
30 | * @return TaskHistory[]
31 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
32 | */
33 | public static function getByProcessInstanceId(string $processInstanceId): array
34 | {
35 | $response = self::make()
36 | ->get(
37 | 'history/task',
38 | [
39 | 'processInstanceId' => $processInstanceId,
40 | 'finished' => true,
41 | ]
42 | );
43 |
44 | if ($response->successful()) {
45 | $data = collect();
46 | foreach ($response->json() as $task) {
47 | $data->push(TaskHistory::from($task));
48 | }
49 |
50 | return $data->sortBy('endTime')->toArray();
51 | }
52 |
53 | return [];
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Http/MessageEventClient.php:
--------------------------------------------------------------------------------
1 | toString();
37 |
38 | if ($businessKey) {
39 | $payload['businessKey'] = $businessKey;
40 | }
41 |
42 | $response = self::make()->post('message', $payload);
43 | if ($response->successful()) {
44 | return ProcessInstanceClient::findByBusniessKey($businessKey);
45 | // return new ProcessInstance($response->json());
46 | // return true;
47 | }
48 |
49 | throw new InvalidArgumentException($response->body());
50 | // return false;
51 | }
52 |
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/Http/TenantClient.php:
--------------------------------------------------------------------------------
1 | get("tenant/$id");
14 |
15 | if ($response->status() === 404) {
16 | throw new ObjectNotFoundException($response->json('message'));
17 | }
18 |
19 | return new Tenant($response->json());
20 | }
21 |
22 | public static function get(array $parameters = []): array
23 | {
24 | $response = self::make()->get('tenant', $parameters);
25 | $result = [];
26 | foreach ($response->json() as $data) {
27 | $result[] = new Tenant($data);
28 | }
29 |
30 | return $result;
31 | }
32 |
33 | public static function create(string $id, string $name): bool
34 | {
35 | $response = self::make()->post(
36 | "tenant/create",
37 | compact('id', 'name')
38 | );
39 |
40 | if ($response->status() === 204) {
41 | return true;
42 | }
43 |
44 | throw new CamundaException($response->body(), $response->status());
45 | }
46 |
47 | public static function delete(string $id): bool
48 | {
49 | $response = self::make()->delete("tenant/{$id}");
50 |
51 | if ($response->status() === 404) {
52 | throw new ObjectNotFoundException($response->json('message'));
53 | }
54 |
55 | return $response->status() === 204;
56 | }
57 |
58 | public static function truncate(): void
59 | {
60 | foreach (self::get() as $tenant) {
61 | self::delete($tenant->id);
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Http/ProcessInstanceHistoryClient.php:
--------------------------------------------------------------------------------
1 | get('history/process-instance', $parameters)->json() as $res) {
25 | $instances[] = ProcessInstanceHistory::from($res);
26 | }
27 |
28 | return $instances;
29 | }
30 |
31 | /**
32 | * @param string $id
33 | *
34 | * @return \Laravolt\Camunda\Dto\ProcessInstanceHistory
35 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
36 | */
37 | public static function find(string $id): ProcessInstanceHistory
38 | {
39 | $response = self::make()->get("history/process-instance/$id");
40 |
41 | if ($response->status() === 404) {
42 | throw new ObjectNotFoundException($response->json('message'));
43 | }
44 |
45 | return ProcessInstanceHistory::from($response->json());
46 | }
47 |
48 | public static function variables(string $id): array
49 | {
50 | $variables = self::make()->get("history/variable-instance", ['processInstanceId' => $id])->json();
51 |
52 | return collect($variables)->mapWithKeys(
53 | fn ($data) => [$data['name'] => new Variable(name: $data['name'], value: $data['value'], type: $data['type'])]
54 | )->toArray();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/BpmnReader.php:
--------------------------------------------------------------------------------
1 | xml = new \SimpleXMLElement(file_get_contents($file));
14 | $this->xml->registerXPathNamespace('bpmn', 'http://www.omg.org/spec/BPMN/20100524/MODEL');
15 | $this->xml->registerXPathNamespace('camunda', 'http://camunda.org/schema/1.0/bpmn');
16 | }
17 |
18 | public function getForms()
19 | {
20 | $nodes = $this->xml->xpath('//bpmn:startEvent') + $this->xml->xpath('//bpmn:userTask');
21 |
22 | $forms = [];
23 | foreach ($nodes as $node) {
24 | try {
25 | $fields = $node->xpath('bpmn:extensionElements/camunda:formData/camunda:formField');
26 | $formFields = [];
27 | foreach ($fields as $field) {
28 | $properties = collect($field->xpath('camunda:properties/camunda:property'))
29 | ->transform(
30 | fn ($node) => [(string) $node->attributes()->id => (string) $node->attributes()->value]
31 | )
32 | ->toArray();
33 |
34 | $formFields[] = [
35 | 'id' => (string) $field->attributes()->id,
36 | 'label' => (string) $field->attributes()->label,
37 | 'type' => (string) $field->attributes()->type,
38 | 'properties' => $properties,
39 | ];
40 | }
41 | if (!empty($formFields)) {
42 | $forms[] = [
43 | 'id' => (string) $node->attributes()->id,
44 | 'label' => (string) $node->attributes()->name,
45 | 'fields' => $formFields,
46 | ];
47 | }
48 | } catch (\ErrorException $exception) {
49 | }
50 | }
51 |
52 | return $forms;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/resources/invalid.bpmn:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Flow_0st8cgr
7 |
8 |
9 |
10 |
11 | Flow_02wkiqx_invalid
12 | Flow_0st8cgr_invalid
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/Http/ProcessDefinitionClient.php:
--------------------------------------------------------------------------------
1 | post($path, $payload);
36 | if ($response->successful()) {
37 | return ProcessInstance::from($response->json());
38 | }
39 |
40 | throw new InvalidArgumentException($response->body());
41 | }
42 |
43 | public static function xml(...$args): string
44 | {
45 | $path = self::makeIdentifierPath(path: 'process-definition/{identifier}/xml', args: $args);
46 |
47 | return self::make()->get($path)->json('bpmn20Xml');
48 | }
49 |
50 | public static function get(array $parameters = []): array
51 | {
52 | $processDefinition = [];
53 | foreach (self::make()->get('process-definition', $parameters)->json() as $res) {
54 | $processDefinition[] = ProcessDefinition::from($res);
55 | }
56 |
57 | return $processDefinition;
58 | }
59 |
60 | public static function find(...$args): ProcessDefinition
61 | {
62 | $response = self::make()->get(self::makeIdentifierPath('process-definition/{identifier}', $args));
63 |
64 | if ($response->status() === 404) {
65 | throw new ObjectNotFoundException($response->json('message'));
66 | }
67 |
68 | return ProcessDefinition::from($response->json());
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/resources/external-task.bpmn:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Flow_02wkiqx
6 |
7 |
8 | Flow_0st8cgr
9 |
10 |
11 |
12 |
13 | Flow_02wkiqx
14 | Flow_0st8cgr
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/tests/Dto/TaskDtoTest.php:
--------------------------------------------------------------------------------
1 | 'task-123',
15 | 'name' => 'Sample Task',
16 | 'assignee' => null,
17 | 'created' => '2025-08-09T06:50:00+00:00',
18 | 'due' => null,
19 | 'followUp' => null,
20 | 'lastUpdated' => '2025-08-09T07:10:11+00:00',
21 | 'delegationState' => null,
22 | 'description' => null,
23 | 'executionId' => 'exec-1',
24 | 'owner' => null,
25 | 'parentTaskId' => null,
26 | 'priority' => '50',
27 | 'processDefinitionId' => 'proc-def-1',
28 | 'processInstanceId' => 'proc-inst-1',
29 | 'taskDefinitionKey' => 'task_def_key',
30 | 'caseExecutionId' => null,
31 | 'caseInstanceId' => null,
32 | 'caseDefinitionId' => null,
33 | 'suspended' => false,
34 | 'formKey' => null,
35 | 'camundaFormRef' => null,
36 | 'tenantId' => null,
37 | 'taskState' => null,
38 | ];
39 |
40 | $task = Task::from($payload);
41 |
42 | $this->assertInstanceOf(Carbon::class, $task->created);
43 | $this->assertInstanceOf(Carbon::class, $task->lastUpdated);
44 | }
45 |
46 | public function test_last_updated_can_be_null(): void
47 | {
48 | $payload = [
49 | 'id' => 'task-456',
50 | 'name' => 'Another Task',
51 | 'assignee' => null,
52 | 'created' => '2025-08-09T06:50:00+00:00',
53 | 'due' => null,
54 | 'followUp' => null,
55 | 'lastUpdated' => null,
56 | 'delegationState' => null,
57 | 'description' => null,
58 | 'executionId' => 'exec-2',
59 | 'owner' => null,
60 | 'parentTaskId' => null,
61 | 'priority' => '10',
62 | 'processDefinitionId' => 'proc-def-2',
63 | 'processInstanceId' => 'proc-inst-2',
64 | 'taskDefinitionKey' => 'task_def_key_2',
65 | 'caseExecutionId' => null,
66 | 'caseInstanceId' => null,
67 | 'caseDefinitionId' => null,
68 | 'suspended' => true,
69 | 'formKey' => null,
70 | 'camundaFormRef' => null,
71 | 'tenantId' => null,
72 | 'taskState' => null,
73 | ];
74 |
75 | $task = Task::from($payload);
76 |
77 | $this->assertInstanceOf(Carbon::class, $task->created);
78 | $this->assertNull($task->lastUpdated);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Http/DeploymentClient.php:
--------------------------------------------------------------------------------
1 | 'deployment-name', 'contents' => $name],
17 | ['name' => 'deployment-source', 'contents' => sprintf('%s (%s)', config('app.name'), config('app.env'))],
18 | ['name' => 'enable-duplicate-filtering', 'contents' => 'true'],
19 | ];
20 |
21 | if (config('services.camunda.tenant_id')) {
22 | $multipart[] = [
23 | 'name' => 'tenant-id',
24 | 'contents' => config('services.camunda.tenant_id'),
25 | ];
26 | }
27 |
28 | $request = self::make()->asMultipart();
29 | foreach ((array) $bpmnFiles as $bpmn) {
30 | $filename = pathinfo($bpmn)['basename'];
31 | $request->attach($filename, file_get_contents($bpmn), $filename);
32 | }
33 |
34 | $response = $request->post('deployment/create', $multipart);
35 |
36 | if ($response->status() === 400) {
37 | throw new ParseException($response->json('message'));
38 | }
39 |
40 | return Deployment::from($response->json());
41 | }
42 |
43 | public static function find(string $id): Deployment
44 | {
45 | $response = self::make()->get("deployment/$id");
46 |
47 | if ($response->status() === 404) {
48 | throw new ObjectNotFoundException($response->json('message'));
49 | }
50 |
51 | return Deployment::from($response->json());
52 | }
53 |
54 | public static function get(array $parameters = []): array
55 | {
56 | $response = self::make()->get('deployment', $parameters);
57 | $result = [];
58 | foreach ($response->json() as $data) {
59 | $result[] = Deployment::from($data);
60 | }
61 |
62 | return $result;
63 | }
64 |
65 | public static function truncate(bool $cascade = false): void
66 | {
67 | $deployments = self::get();
68 | foreach ($deployments as $deployment) {
69 | self::delete($deployment->id, $cascade);
70 | }
71 | }
72 |
73 | public static function delete(string $id, bool $cascade = false): bool
74 | {
75 | $cascadeFlag = $cascade ? 'cascade=true' : '';
76 | $response = self::make()->delete("deployment/{$id}?".$cascadeFlag);
77 |
78 | if ($response->status() === 404) {
79 | throw new ObjectNotFoundException($response->json('message'));
80 | }
81 |
82 | return $response->status() === 204;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/resources/sample.bpmn:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 | Flow_02wkiqx
10 |
11 |
12 | Flow_0st8cgr
13 |
14 |
15 | Flow_02wkiqx
16 | Flow_0st8cgr
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/resources/sample2.bpmn:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Flow_02wkiqx
7 |
8 |
9 | Flow_1mpkw8w
10 |
11 |
12 |
13 | Flow_02wkiqx
14 | Flow_082h457
15 |
16 |
17 | Flow_082h457
18 | Flow_1mpkw8w
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/Http/ProcessInstanceClient.php:
--------------------------------------------------------------------------------
1 | get('process-instance');
18 | } else {
19 | $res = self::make()->post('process-instance', $parameters);
20 | }
21 | foreach ($res->json() as $res) {
22 | $instances[] = ProcessInstance::from($res);
23 | }
24 |
25 | return $instances;
26 | }
27 |
28 | /**
29 | * Get process instances by variables.
30 | *
31 | * @param array $variables
32 | * $variables format:
33 | * $variables = [
34 | * [
35 | * 'name' => 'varname',
36 | * 'operator' => "eq",
37 | * 'value' => 'varvalue',
38 | * ],
39 | * ];
40 | * `operator` can only contain `eq`, `neq`, `gt`, `gte`, `lt`, `lte`.
41 | *
42 | * @return ProcessInstance[]
43 | */
44 | public static function getByVariables(array $variables = []): array
45 | {
46 | $instances = [];
47 |
48 | if (! $variables) {
49 | $res = self::make()->get('process-instance');
50 | } else {
51 | $res = self::make()->post('process-instance', [
52 | 'variables' => $variables,
53 |
54 | ]);
55 | }
56 | foreach ($res->json() as $res) {
57 | $instances[] = ProcessInstance::from($res);
58 | }
59 |
60 | return $instances;
61 | }
62 |
63 | public static function find(string $id): ProcessInstance
64 | {
65 | $response = self::make()->get("process-instance/$id");
66 |
67 | if ($response->status() === 404) {
68 | throw new ObjectNotFoundException($response->json('message'));
69 | }
70 |
71 | return ProcessInstance::from($response->json());
72 | }
73 |
74 | public static function findByBusniessKey(string $businessKey): ProcessInstance
75 | {
76 | $response = self::make()->post("process-instance", [
77 | 'businessKey' => $businessKey,
78 | ]);
79 |
80 | if ($response->status() === 404) {
81 | throw new ObjectNotFoundException($response->json('message'));
82 | }
83 |
84 | $data = $response->json();
85 |
86 | if (count($data) == 0) {
87 | throw new ObjectNotFoundException("Process Instance Not Found");
88 | }
89 |
90 | return ProcessInstance::from($data[count($data) - 1]);
91 | }
92 |
93 | public static function variables(string $id): array
94 | {
95 | $variables = self::make()->get("process-instance/$id/variables", ['deserializeValues' => false])->json();
96 |
97 | $temp = collect($variables)->mapWithKeys(
98 | fn ($data, $name) => [
99 | $name => new Variable(
100 | name: $name,
101 | type: $data['type'],
102 | value: $data['value'],
103 | valueInfo: $data['valueInfo'] ?? []
104 | ),
105 | ]
106 | );
107 |
108 | $result = [];
109 | foreach ($temp as $name => $value) {
110 | $result[$name] = $value;
111 | }
112 |
113 | return $result;
114 | }
115 |
116 | public static function delete(string $id): bool
117 | {
118 | return self::make()->delete("process-instance/$id")->status() === 204;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/tests/DeploymentTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('test', $deployment->name);
21 | }
22 |
23 | public function test_deploy_bpmn_with_tenant_id(): void
24 | {
25 | config()->set('services.camunda.tenant_id', 'sample-tenant');
26 |
27 | $files = __DIR__.'/../resources/sample.bpmn';
28 | $deployment = DeploymentClient::create('test', $files);
29 |
30 | $this->assertEquals('test', $deployment->name);
31 | }
32 |
33 | public function test_deploy_multiple_bpmn(): void
34 | {
35 | $files = [
36 | __DIR__.'/../resources/sample.bpmn',
37 | __DIR__.'/../resources/sample2.bpmn',
38 | ];
39 | $deployment = DeploymentClient::create('test', $files);
40 |
41 | $this->assertEquals('test', $deployment->name);
42 | }
43 |
44 | public function test_deploy_invalid_bpmn(): void
45 | {
46 | $this->expectException(ParseException::class);
47 |
48 | $files = __DIR__.'/../resources/invalid.bpmn';
49 | DeploymentClient::create('test', $files);
50 | }
51 |
52 | public function test_get_deployment_by_id(): void
53 | {
54 | $files = __DIR__.'/../resources/sample.bpmn';
55 | $deployment1 = DeploymentClient::create('test', $files);
56 |
57 | $deployment2 = DeploymentClient::find($deployment1->id);
58 | $this->assertEquals($deployment1->id, $deployment2->id);
59 | }
60 |
61 | public function test_get_deployment_by_invalid_id(): void
62 | {
63 | $this->expectException(ObjectNotFoundException::class);
64 |
65 | DeploymentClient::find('some-invalid-id');
66 | }
67 |
68 | public function test_get_list_deployment(): void
69 | {
70 | $this->truncateDeployment();
71 | DeploymentClient::create('deployment1', __DIR__.'/../resources/sample.bpmn');
72 | DeploymentClient::create('deployment2', __DIR__.'/../resources/sample2.bpmn');
73 |
74 | $deployments = DeploymentClient::get();
75 | $this->assertCount(2, $deployments);
76 | }
77 |
78 | public function test_delete_deployment(): void
79 | {
80 | $deployment = DeploymentClient::create('deployment1', __DIR__.'/../resources/sample.bpmn');
81 | $deleted = DeploymentClient::delete($deployment->id);
82 |
83 | $this->assertTrue($deleted);
84 | }
85 |
86 | public function test_delete_invalid_deployment(): void
87 | {
88 | $this->expectException(ObjectNotFoundException::class);
89 |
90 | $deployment = new \Laravolt\Camunda\Dto\Deployment(
91 | id: 'invalid-id',
92 | tenantId: null,
93 | source: null,
94 | name: 'test',
95 | deploymentTime: now(),
96 | processDefinitions: []
97 | );
98 | DeploymentClient::delete($deployment->id);
99 | }
100 |
101 | public function test_truncate_deployment(): void
102 | {
103 | DeploymentClient::create('deployment1', __DIR__.'/../resources/sample.bpmn');
104 | DeploymentClient::create('deployment2', __DIR__.'/../resources/sample2.bpmn');
105 |
106 | DeploymentClient::truncate();
107 |
108 | $deployments = DeploymentClient::get();
109 | $this->assertCount(0, $deployments);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/tests/ProcessDefinitionTest.php:
--------------------------------------------------------------------------------
1 | truncateDeployment();
15 | }
16 |
17 | public function test_start_new_process_instance(): void
18 | {
19 | $this->deploySampleBpmn();
20 |
21 | $variables = ['title' => ['value' => 'Sample Title', 'type' => 'string']];
22 | $businessKey = 'key-1';
23 | $processInstance =
24 | ProcessDefinitionClient::start(key: 'process_1', variables: $variables, businessKey: $businessKey);
25 |
26 | $this->assertIsString($processInstance->id);
27 | }
28 |
29 | public function test_start_with_empty_variables(): void
30 | {
31 | $this->deploySampleBpmn();
32 | $processDefinition = ProcessDefinitionClient::find(key: 'process_1');
33 |
34 | $variables = [];
35 | $businessKey = 'key-1';
36 |
37 | $this->expectException(InvalidArgumentException::class);
38 | ProcessDefinitionClient::start($processDefinition->id, $variables, $businessKey);
39 | }
40 |
41 | public function test_get_list_process_definition(): void
42 | {
43 | $this->deploySampleBpmn();
44 | $processDefinitions = ProcessDefinitionClient::get();
45 | $this->assertCount(1, $processDefinitions);
46 | $this->assertInstanceOf(ProcessDefinition::class, $processDefinitions[0]);
47 | }
48 |
49 | public function test_find_by_id(): void
50 | {
51 | $this->deploySampleBpmn();
52 | $processDefinitions = ProcessDefinitionClient::get();
53 |
54 | $processDefinition = ProcessDefinitionClient::find($processDefinitions[0]->id);
55 | $this->assertNotNull($processDefinition);
56 |
57 | $processDefinition = ProcessDefinitionClient::find(id: $processDefinitions[0]->id);
58 | $this->assertNotNull($processDefinition);
59 | }
60 |
61 | public function test_find_by_invalid_id(): void
62 | {
63 | $this->expectException(ObjectNotFoundException::class);
64 | ProcessDefinitionClient::find(id: 'invalid-id');
65 | }
66 |
67 | public function test_find_by_key(): void
68 | {
69 | $this->deploySampleBpmn();
70 | $processDefinition = ProcessDefinitionClient::find(key: 'process_1');
71 | $this->assertInstanceOf(ProcessDefinition::class, $processDefinition);
72 | }
73 |
74 | public function test_find_by_invalid_key(): void
75 | {
76 | $this->expectException(ObjectNotFoundException::class);
77 | ProcessDefinitionClient::find(key: 'invalid-key');
78 | }
79 |
80 | public function test_find_by_key_and_tenant_id(): void
81 | {
82 | $tenantId = 'tenant-1';
83 | config()->set('services.camunda.tenant_id', $tenantId);
84 | $this->deploySampleBpmn();
85 |
86 | $processDefinition = ProcessDefinitionClient::find(key: 'process_1', tenantId: $tenantId);
87 | $this->assertEquals($tenantId, $processDefinition->tenantId);
88 | }
89 |
90 | public function test_find_xml_by_id(): void
91 | {
92 | $this->deploySampleBpmn();
93 | $processDefinitions = ProcessDefinitionClient::get();
94 | $id = $processDefinitions[0]->id;
95 | $xml = ProcessDefinitionClient::xml(id: $id);
96 | $this->assertNotNull($xml);
97 | }
98 |
99 | public function test_find_xml_by_key(): void
100 | {
101 | $this->deploySampleBpmn();
102 | $xml = ProcessDefinitionClient::xml(key: 'process_1');
103 | $this->assertNotNull($xml);
104 | }
105 |
106 | public function test_find_xml_without_id_or_key(): void
107 | {
108 | $this->expectException(InvalidArgumentException::class);
109 | ProcessDefinitionClient::xml();
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/tests/ExternalTaskTest.php:
--------------------------------------------------------------------------------
1 | createExternalProcess();
23 | $topics = [
24 | ['topicName' => 'pdf', 'lockDuration' => 600_000]
25 | ];
26 | $externalTasks = ExternalTaskClient::fetchAndLock('worker1', $topics);
27 | $this->assertCount(1, $externalTasks);
28 | }
29 |
30 | public function test_complete_task()
31 | {
32 | $this->createExternalProcess();
33 | $topics = [
34 | ['topicName' => 'pdf', 'lockDuration' => 600_000]
35 | ];
36 | $externalTasks = ExternalTaskClient::fetchAndLock('worker1', $topics);
37 | $this->assertCount(1, $externalTasks);
38 | $task = $externalTasks[0];
39 | $completed = ExternalTaskClient::complete($task->id, 'worker1');
40 | $this->assertTrue($completed);
41 | }
42 |
43 | public function test_lock_individual_task()
44 | {
45 | $process = $this->createExternalProcess();
46 | $topics = [
47 | ['topicName' => 'pdf', 'lockDuration' => 600_000]
48 | ];
49 | $externalTasks = ExternalTaskClient::getByProcessInstanceId($process->id);
50 | $this->assertCount(1, $externalTasks);
51 | $task = $externalTasks[0];
52 | ExternalTaskClient::lock($task->id, 'worker1', 600_000);
53 | $lockedTasks = ExternalTaskClient::getTaskLocked();
54 | $this->assertCount(1, $lockedTasks);
55 | }
56 |
57 | public function test_complete_task_with_variables()
58 | {
59 | $processInstance = $this->createExternalProcess();
60 | $topics = [
61 | ['topicName' => 'pdf', 'lockDuration' => 600_000]
62 | ];
63 | $externalTasks = ExternalTaskClient::fetchAndLock('worker1', $topics);
64 | $this->assertCount(1, $externalTasks);
65 | $task = $externalTasks[0];
66 | $completed = ExternalTaskClient::complete(
67 | $task->id,
68 | 'worker1',
69 | ['title' => ['value' => 'Sample Title', 'type' => 'string']]
70 | );
71 | $this->assertTrue($completed);
72 | $processInstanceHistory = ProcessInstanceHistoryClient::find($processInstance->id);
73 | $this->assertEquals('COMPLETED', $processInstanceHistory->state);
74 | $variables = ProcessInstanceHistoryClient::variables($processInstance->id);
75 | $this->assertArrayHasKey('signature', $variables);
76 | $this->assertArrayHasKey('title', $variables);
77 | }
78 |
79 | public function test_unlock_locked_task()
80 | {
81 | $this->createExternalProcess();
82 | $topics = [
83 | ['topicName' => 'pdf', 'lockDuration' => 600_000]
84 | ];
85 | $externalTasks = ExternalTaskClient::fetchAndLock('worker1', $topics);
86 | $task = $externalTasks[0];
87 | $unlocked = ExternalTaskClient::unlock($task->id);
88 | $this->assertTrue($unlocked);
89 | }
90 |
91 | public function test_unlock_unlocked_task()
92 | {
93 | $processInstance = $this->createExternalProcess();
94 | $tasks = ExternalTaskClient::getByProcessInstanceId($processInstance->id);
95 | $task = $tasks[0];
96 | $unlocked = ExternalTaskClient::unlock($task->id);
97 | $this->assertTrue($unlocked);
98 | }
99 |
100 | protected function tearDown(): void
101 | {
102 | $this->truncateDeployment();
103 | }
104 |
105 | private function createExternalProcess(): \Laravolt\Camunda\Dto\ProcessInstance
106 | {
107 | $variables = ['signature' => ['value' => 'Fulan', 'type' => 'string']];
108 | $businessKey = 'key-1';
109 |
110 | return ProcessDefinitionClient::start(key: 'processExternalTask', variables: $variables, businessKey: $businessKey);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | in('src')
5 | ->in('tests');
6 |
7 | $config = new PhpCsFixer\Config();
8 |
9 | return $config->setRules(
10 | [
11 | '@PSR12' => true,
12 | 'array_syntax' => ['syntax' => 'short'],
13 | 'binary_operator_spaces' => [
14 | 'default' => 'single_space',
15 | 'operators' => ['|' => 'no_space'],
16 | ],
17 | 'blank_line_after_namespace' => true,
18 | 'blank_line_after_opening_tag' => true,
19 | 'blank_line_before_statement' => [
20 | 'statements' => ['return'],
21 | ],
22 | 'braces' => true,
23 | 'cast_spaces' => true,
24 | 'class_definition' => true,
25 | 'concat_space' => [
26 | 'spacing' => 'none',
27 | ],
28 | 'declare_equal_normalize' => true,
29 | 'elseif' => true,
30 | 'encoding' => true,
31 | 'full_opening_tag' => true,
32 | 'fully_qualified_strict_types' => true, // added by Shift
33 | 'function_declaration' => true,
34 | 'function_typehint_space' => true,
35 | 'heredoc_to_nowdoc' => true,
36 | 'include' => true,
37 | 'increment_style' => ['style' => 'post'],
38 | 'indentation_type' => true,
39 | 'linebreak_after_opening_tag' => true,
40 | 'line_ending' => true,
41 | 'lowercase_cast' => true,
42 | 'lowercase_keywords' => true,
43 | 'lowercase_static_reference' => true, // added from Symfony
44 | 'magic_method_casing' => true, // added from Symfony
45 | 'magic_constant_casing' => true,
46 | 'method_argument_space' => true,
47 | 'native_function_casing' => true,
48 | 'no_extra_blank_lines' => [
49 | 'tokens' => [
50 | 'extra',
51 | 'throw',
52 | 'use',
53 | 'use_trait',
54 | ],
55 | ],
56 | 'no_blank_lines_after_class_opening' => true,
57 | 'no_blank_lines_after_phpdoc' => true,
58 | 'no_closing_tag' => true,
59 | 'no_empty_phpdoc' => true,
60 | 'no_empty_statement' => true,
61 | 'no_leading_import_slash' => true,
62 | 'no_leading_namespace_whitespace' => true,
63 | 'no_mixed_echo_print' => [
64 | 'use' => 'echo',
65 | ],
66 | 'no_multiline_whitespace_around_double_arrow' => true,
67 | 'multiline_whitespace_before_semicolons' => [
68 | 'strategy' => 'no_multi_line',
69 | ],
70 | 'no_short_bool_cast' => true,
71 | 'no_singleline_whitespace_before_semicolons' => true,
72 | 'no_spaces_after_function_name' => true,
73 | 'no_spaces_around_offset' => true,
74 | 'no_spaces_inside_parenthesis' => true,
75 | 'no_trailing_comma_in_list_call' => true,
76 | 'no_trailing_comma_in_singleline_array' => true,
77 | 'no_trailing_whitespace' => true,
78 | 'no_trailing_whitespace_in_comment' => true,
79 | 'no_unneeded_control_parentheses' => true,
80 | 'no_unused_imports' => true,
81 | 'no_useless_return' => true,
82 | 'no_whitespace_before_comma_in_array' => true,
83 | 'no_whitespace_in_blank_line' => true,
84 | 'normalize_index_brace' => true,
85 | 'not_operator_with_successor_space' => true,
86 | 'object_operator_without_whitespace' => true,
87 | 'ordered_imports' => ['sort_algorithm' => 'alpha'],
88 | 'phpdoc_indent' => true,
89 | 'phpdoc_no_access' => true,
90 | 'phpdoc_no_package' => true,
91 | 'phpdoc_no_useless_inheritdoc' => true,
92 | 'phpdoc_scalar' => true,
93 | 'phpdoc_single_line_var_spacing' => true,
94 | 'phpdoc_summary' => true,
95 | 'phpdoc_to_comment' => true,
96 | 'phpdoc_trim' => true,
97 | 'phpdoc_types' => true,
98 | 'phpdoc_var_without_name' => true,
99 | 'short_scalar_cast' => true,
100 | 'simplified_null_return' => false, // disabled by Shift
101 | 'single_blank_line_at_eof' => true,
102 | 'single_blank_line_before_namespace' => true,
103 | 'single_class_element_per_statement' => true,
104 | 'single_import_per_statement' => true,
105 | 'single_line_after_imports' => true,
106 | 'single_line_comment_style' => [
107 | 'comment_types' => ['hash'],
108 | ],
109 | 'single_quote' => true,
110 | 'space_after_semicolon' => true,
111 | 'standardize_not_equals' => true,
112 | 'switch_case_semicolon_to_colon' => true,
113 | 'switch_case_space' => true,
114 | 'ternary_operator_spaces' => true,
115 | 'trailing_comma_in_multiline_array' => true,
116 | 'trim_array_spaces' => true,
117 | 'visibility_required' => [
118 | 'elements' => ['method', 'property'],
119 | ],
120 | 'whitespace_after_comma_in_array' => true, ]
121 | )
122 | ->setFinder($finder);
123 |
--------------------------------------------------------------------------------
/src/Http/ExternalTaskClient.php:
--------------------------------------------------------------------------------
1 | $topic,
27 | 'job' => is_string($job) ? $job : $job['job'] ?? null,
28 | 'lockDuration' => $job['lockDuration'] ?? 600_000,
29 | ];
30 | }
31 |
32 | public static function find(string $id): ExternalTask
33 | {
34 | $response = self::make()->get("external-task/$id");
35 |
36 | if ($response->status() === 404) {
37 | throw new ObjectNotFoundException($response->json('message'));
38 | }
39 |
40 | return ExternalTask::from($response->json());
41 | }
42 |
43 | /**
44 | * @param string $processInstanceId
45 | *
46 | * @return ExternalTask[]
47 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
48 | */
49 | public static function getByProcessInstanceId(string $id): array
50 | {
51 | $response = self::make()->get("external-task?processInstanceId=$id");
52 |
53 | $data = [];
54 | if ($response->successful()) {
55 | foreach ($response->json() as $task) {
56 | $data[] = ExternalTask::from($task);
57 | }
58 | }
59 |
60 | return $data;
61 | }
62 |
63 | /**
64 | * @return ExternalTask[]
65 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
66 | */
67 | public static function getTaskLocked(): array
68 | {
69 | $response = self::make()->get("external-task?locked=true");
70 |
71 | $data = [];
72 | if ($response->successful()) {
73 | foreach ($response->json() as $task) {
74 | $data[] = ExternalTask::from($task);
75 | }
76 | }
77 |
78 | return $data;
79 | }
80 |
81 | /**
82 | * @param string $workerId
83 | * @param array $topics
84 | * @param int $maxTasks
85 | *
86 | * @return ExternalTask[]
87 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
88 | */
89 | public static function fetchAndLock(string $workerId, array $topics, int $maxTasks = 10): array
90 | {
91 | $payload = [
92 | 'workerId' => $workerId,
93 | 'maxTasks' => $maxTasks,
94 | 'topics' => $topics,
95 | ];
96 |
97 | $response = self::make()->post("external-task/fetchAndLock", $payload);
98 |
99 | if ($response->successful()) {
100 | $data = [];
101 | foreach ($response->json() as $raw) {
102 | $data[] = ExternalTask::from($raw);
103 | }
104 |
105 | return $data;
106 | }
107 |
108 | throw new CamundaException($response->json('message') ?? $response->body());
109 | }
110 |
111 | public static function complete(
112 | string $id,
113 | string $workerId,
114 | array $variables = [],
115 | array $localVariables = []
116 | ): bool {
117 | $payload = compact('workerId');
118 | if ($variables) {
119 | $payload['variables'] = $variables;
120 | }
121 | if ($localVariables) {
122 | $payload['localVariables'] = $localVariables;
123 | }
124 | $url = "external-task/$id/complete";
125 | $response = self::make()->post($url, $payload);
126 | $isSuccessful = $response->status() === 204;
127 |
128 | if (! $isSuccessful) {
129 | throw (new UnexpectedResponseException)->for($url, $payload, $response->json());
130 | }
131 |
132 | return $isSuccessful;
133 | }
134 |
135 |
136 | public static function fail(
137 | string $id,
138 | string $workerId,
139 | string $errorMessage = "Does not compute",
140 | int $retryTimeout = 60000,
141 | ): bool
142 | {
143 |
144 | $payload = compact('workerId', 'errorMessage', 'retryTimeout');
145 | $url = "external-task/$id/failure";
146 | $response = self::make()->post($url, $payload);
147 | $isSuccessful = $response->status() === 204;
148 |
149 | if (!$isSuccessful) {
150 | throw (new UnexpectedResponseException)->for($url, $payload, $response->json());
151 | }
152 |
153 | return $isSuccessful;
154 | }
155 |
156 | public static function lock(string $id, string $workerId, int $duration): bool
157 | {
158 | $response =
159 | self::make()->post("external-task/$id/lock", ['workerId' => $workerId, 'lockDuration' => $duration]);
160 |
161 | return $response->status() === 204;
162 | }
163 |
164 | public static function unlock(string $id): bool
165 | {
166 | $response = self::make()->post("external-task/$id/unlock");
167 |
168 | return $response->status() === 204;
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/tests/TaskTest.php:
--------------------------------------------------------------------------------
1 | deploySampleBpmn();
19 | }
20 |
21 | public function test_find_by_id()
22 | {
23 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
24 | $processInstance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
25 | $tasks = TaskClient::getByProcessInstanceId($processInstance->id);
26 |
27 | foreach ($tasks as $task) {
28 | $tastObject = TaskClient::find($task->id);
29 | $this->assertEquals($task->id, $tastObject->id);
30 | }
31 | }
32 |
33 | public function test_find_by_ids()
34 | {
35 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
36 | $processInstance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
37 | $tasks = TaskClient::getByProcessInstanceIds([$processInstance->id]);
38 |
39 | foreach ($tasks as $task) {
40 | $tastObject = TaskClient::find($task->id);
41 | $this->assertEquals($task->id, $tastObject->id);
42 | }
43 | }
44 |
45 |
46 | public function test_find_by_ids_is_empty()
47 | {
48 | $tasks = TaskClient::getByProcessInstanceIds([]);
49 | self::assertEmpty($tasks);
50 |
51 | }
52 |
53 | public function test_handle_invalid_id()
54 | {
55 | $this->expectException(ObjectNotFoundException::class);
56 | TaskClient::find('invalid-id');
57 | }
58 |
59 | public function test_get_completed_task()
60 | {
61 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
62 | $processInstance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
63 | $tasks = TaskClient::getByProcessInstanceId($processInstance->id);
64 |
65 | foreach ($tasks as $task) {
66 | TaskClient::submit(
67 | $task->id,
68 | ['email' => ['value' => 'uyab.exe@gmail.com', 'type' => 'string']]
69 | );
70 | $completedTask = TaskHistoryClient::find($task->id);
71 | $this->assertInstanceOf(TaskHistory::class, $completedTask);
72 | }
73 | }
74 |
75 | public function test_get_completed_task_with_invalid_id()
76 | {
77 | $this->expectException(ObjectNotFoundException::class);
78 | TaskHistoryClient::find('invalid-task-id');
79 | }
80 |
81 | public function test_get_completed_tasks()
82 | {
83 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
84 | $processInstance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
85 | $tasks = TaskClient::getByProcessInstanceId($processInstance->id);
86 |
87 | foreach ($tasks as $task) {
88 | TaskClient::submit(
89 | $task->id,
90 | ['email' => ['value' => 'uyab.exe@gmail.com', 'type' => 'string']]
91 | );
92 | }
93 |
94 | $completedTasks = TaskHistoryClient::getByProcessInstanceId($processInstance->id);
95 | $this->assertCount(1, $completedTasks);
96 | }
97 |
98 | public function test_submit_form()
99 | {
100 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
101 | $processInstance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
102 | $tasks = TaskClient::getByProcessInstanceId($processInstance->id);
103 |
104 | foreach ($tasks as $task) {
105 | $submitted = TaskClient::submit($task->id, ['email' => ['value' => 'uyab.exe@gmail.com', 'type' => 'string']]);
106 | $this->assertTrue($submitted);
107 | }
108 | }
109 |
110 | public function test_submit_form_and_return_variables()
111 | {
112 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
113 | $processInstance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
114 | $tasks = TaskClient::getByProcessInstanceId($processInstance->id);
115 |
116 | foreach ($tasks as $task) {
117 | $variables = TaskClient::submitAndReturnVariables($task->id, ['email' => ['value' => 'uyab.exe@gmail.com', 'type' => 'string']]);
118 |
119 | // 2 variables: 1 from start form (title), 1 from task form (email)
120 | $this->assertCount(2, $variables);
121 | $this->assertInstanceOf(Variable::class, $variables['title']);
122 | }
123 | }
124 |
125 | public function test_submit_form_with_invalid_id()
126 | {
127 | $this->expectException(CamundaException::class);
128 | TaskClient::submit('invalid-id', ['foo' => 'bar']);
129 | }
130 |
131 | public function test_submit_form_and_return_variables_with_invalid_id()
132 | {
133 | $this->expectException(CamundaException::class);
134 | TaskClient::submitAndReturnVariables('invalid-id', ['foo' => 'bar']);
135 | }
136 |
137 | protected function tearDown(): void
138 | {
139 | $this->truncateDeployment();
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/Http/TaskClient.php:
--------------------------------------------------------------------------------
1 | get("task/$id");
15 |
16 | if ($response->status() === 404) {
17 | throw new ObjectNotFoundException($response->json('message'));
18 | }
19 |
20 | return Task::from($response->json());
21 | }
22 |
23 | /**
24 | * @param string $processInstanceId
25 | *
26 | * @return Task[]
27 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
28 | */
29 | public static function getByProcessInstanceId(string $id): array
30 | {
31 | $response = self::make()->get("task?processInstanceId=$id");
32 |
33 | $data = [];
34 | if ($response->successful()) {
35 | foreach ($response->json() as $task) {
36 | $data[] = Task::from($task);
37 | }
38 | }
39 |
40 | return $data;
41 | }
42 |
43 |
44 | /**
45 | * @param array $payload
46 | *
47 | * @return Task[]
48 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
49 | */
50 | public static function get(array $payload): array
51 | {
52 | $response = self::make()->post("task", $payload);
53 |
54 | $data = [];
55 | if ($response->successful()) {
56 | foreach ($response->json() as $task) {
57 | $data[] = Task::from($task);
58 | }
59 | }
60 | return $data;
61 | }
62 | /**
63 | * @param array $payload
64 | *
65 | * @return Task[]
66 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
67 | */
68 | public static function unfinishedTask(array $payload): array
69 | {
70 | $payload['unfinished'] = true;
71 | return self::get($payload);
72 | }
73 |
74 |
75 | /**
76 | * @param string $processInstanceIds
77 | *
78 | * @return Task[]
79 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
80 | */
81 | public static function getByProcessInstanceIds(array $ids): array
82 | {
83 | if(empty($ids)){
84 | return [];
85 | }
86 | $response = self::make()->post("task", [
87 | "processInstanceIdIn" => $ids
88 | ]);
89 |
90 | $data = [];
91 | if ($response->successful()) {
92 | foreach ($response->json() as $task) {
93 | $data[] = Task::from($task);
94 | }
95 | }
96 |
97 | return $data;
98 | }
99 |
100 | public static function claim(string $id, string $userId): bool
101 | {
102 | $response = self::make()->post("task/$id/claim", [
103 | "userId" => $userId
104 | ]);
105 |
106 | if ($response->successful()) {
107 | return true;
108 | }
109 |
110 | return false;
111 | }
112 |
113 | public static function unclaim(string $id): bool
114 | {
115 | $response = self::make()->post("task/$id/unclam");
116 |
117 | if ($response->successful()) {
118 | return true;
119 | }
120 |
121 | return false;
122 | }
123 |
124 | public static function assign(string $id, string $userId): bool
125 | {
126 | $response = self::make()->post("task/$id/assignee", [
127 | "userId" => $userId
128 | ]);
129 |
130 | if ($response->successful()) {
131 | return true;
132 | }
133 |
134 | return false;
135 | }
136 |
137 |
138 | /**
139 | * @param string $processInstanceIds
140 | *
141 | * @return Task[]
142 | * @throws \Spatie\DataTransferObject\Exceptions\UnknownProperties
143 | */
144 | public static function getByAssignedAndProcessInstanceId($user_id, array $ids = []): array
145 | {
146 | $payload = [
147 | "assignee" => $user_id
148 | ];
149 | if ($ids != []) {
150 | $payload['processInstanceIdIn'] = implode(",", $ids);
151 | }
152 |
153 | $response = self::make()->post("task", $payload);
154 |
155 | $data = [];
156 | if ($response->successful()) {
157 | foreach ($response->json() as $task) {
158 | $data[] = Task::from($task);
159 | }
160 | }
161 |
162 | return $data;
163 | }
164 |
165 | public static function submit(string $id, array $variables): bool
166 | {
167 | $varData = (object)[];
168 | if (!empty($variables)) {
169 | $varData = (object)$variables;
170 | }
171 | $response = self::make()->post(
172 | "task/$id/submit-form",
173 | ['variables' => $varData]
174 | );
175 |
176 | if ($response->status() === 204) {
177 | return true;
178 | }
179 |
180 | throw new CamundaException($response->body(), $response->status());
181 | }
182 |
183 | public static function submitAndReturnVariables(string $id, array $variables): array
184 | {
185 | $response = self::make()->post(
186 | "task/$id/submit-form",
187 | ['variables' => $variables, 'withVariablesInReturn' => true]
188 | );
189 |
190 | if ($response->status() === 200) {
191 | $raws = $response->json();
192 | $variables = [];
193 | foreach ($raws as $name => $raw) {
194 | $raw['name'] = $name;
195 | $variables[$name] = Variable::from($raw);
196 | }
197 |
198 | return $variables;
199 | }
200 |
201 | throw new CamundaException($response->body(), $response->status());
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/tests/ProcessInstanceTest.php:
--------------------------------------------------------------------------------
1 | deploySampleBpmn();
17 | }
18 |
19 | public function test_find_by_id()
20 | {
21 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
22 | $processInstance1 = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
23 | $processInstance2 = ProcessInstanceClient::find(id: $processInstance1->id);
24 | $processInstance3 = ProcessInstanceClient::find($processInstance1->id);
25 |
26 | $this->assertEquals($processInstance1->id, $processInstance2->id);
27 | $this->assertEquals($processInstance2->id, $processInstance3->id);
28 | }
29 |
30 | public function test_get()
31 | {
32 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
33 | ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
34 |
35 | $processInstances = ProcessInstanceClient::get();
36 |
37 | $this->assertCount(1, $processInstances);
38 | }
39 | public function test_get_with_parameter()
40 | {
41 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
42 | $processInstances = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
43 | $params = [
44 | "processIsntanceIds" => [
45 | $processInstances->id,
46 | ]
47 | ];
48 | $processInstances = ProcessInstanceClient::get($params);
49 |
50 | $this->assertCount(1, $processInstances);
51 | }
52 | public function test_get_with_parameter_is_empty()
53 | {
54 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
55 | ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
56 | $processInstances = ProcessInstanceClient::get([]);
57 |
58 | $this->assertCount(1, $processInstances);
59 | }
60 | public function test_get_with_broken_parameter()
61 | {
62 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
63 | $processInstances = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
64 | $mapmany = [];
65 | for ($i = 0; $i < 500; $i++) {
66 | $mapmany[] = $processInstances->id;
67 | }
68 | $params = [
69 | "processIsntanceIds" => $mapmany
70 | ];
71 | $processInstances = ProcessInstanceClient::get($params);
72 |
73 | $this->assertCount(1, $processInstances);
74 | }
75 |
76 | public function test_get_by_parameters()
77 | {
78 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
79 | ProcessDefinitionClient::start(key: 'process_1', variables: $variables, businessKey: '001');
80 |
81 | $processInstances = ProcessInstanceClient::get(['businessKey' => '001']);
82 | $this->assertCount(1, $processInstances);
83 |
84 | $processInstances = ProcessInstanceClient::get(['businessKey' => '002']);
85 | $this->assertCount(0, $processInstances);
86 | }
87 |
88 | public function test_get_with_busniessKey()
89 | {
90 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
91 | ProcessDefinitionClient::start(key: 'process_1', variables: $variables, businessKey: '001');
92 | $processInstance = ProcessInstanceClient::findByBusniessKey('001');
93 | $this->assertEquals('001', $processInstance->businessKey);
94 | }
95 |
96 |
97 | public function test_get_with_variables()
98 | {
99 | $random = Random::generate();
100 | $variables = ['title' => ['value' => 'Foo' . $random, 'type' => 'string']];
101 | ProcessDefinitionClient::start(key: 'process_1', variables: $variables, businessKey: '001');
102 | $processInstance = ProcessInstanceClient::getByVariables([
103 | [
104 | 'name' => 'title',
105 | 'operator' => "eq",
106 | 'value' => $variables['title']['value'],
107 | ],
108 | ]);
109 | $this->assertCount(1, $processInstance);
110 | }
111 |
112 | public function test_get_with_variables_empty()
113 | {
114 | $random = Random::generate();
115 | $variables = ['title' => ['value' => 'Foo' . $random, 'type' => 'string']];
116 | ProcessDefinitionClient::start(key: 'process_1', variables: $variables, businessKey: Random::generate());
117 | $processInstance = ProcessInstanceClient::getByVariables([]);
118 | $this->assertCount(1, $processInstance);
119 | }
120 |
121 |
122 | public function test_get_variables()
123 | {
124 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
125 | $processInstance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
126 | $variables = ProcessInstanceClient::variables($processInstance->id);
127 | $this->assertCount(1, $variables);
128 | $this->assertInstanceOf(Variable::class, $variables['title']);
129 | $this->assertEquals('String', $variables['title']->type);
130 | $this->assertEquals('Foo', $variables['title']->value);
131 | }
132 |
133 | public function test_delete()
134 | {
135 | $variables = ['title' => ['value' => 'Foo', 'type' => 'string']];
136 | $processInstance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
137 | $deleted = ProcessInstanceClient::delete($processInstance->id);
138 | $this->assertTrue($deleted);
139 |
140 | $this->expectException(ObjectNotFoundException::class);
141 | ProcessInstanceClient::find($processInstance->id);
142 | }
143 |
144 | protected function tearDown(): void
145 | {
146 | $this->truncateDeployment();
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # laravolt/camunda
3 | Convenience Laravel HTTP client wrapper to interact with Camunda REST API.
4 |
5 | ## Installation
6 | `composer require laravolt/camunda`
7 |
8 | ## Configuration
9 |
10 | Prepare your `.env`:
11 |
12 | ```dotenv
13 | CAMUNDA_URL=http://localhost:8080/engine-rest
14 |
15 | #optional
16 | CAMUNDA_TENANT_ID=
17 | CAMUNDA_USER=
18 | CAMUNDA_PASSWORD=
19 | ```
20 |
21 | Add following entries to `config/services.php`:
22 | ```php
23 | 'camunda' => [
24 | 'url' => env('CAMUNDA_URL', 'https://localhost:8080/engine-rest'),
25 | 'user' => env('CAMUNDA_USER', 'demo'),
26 | 'password' => env('CAMUNDA_PASSWORD', 'demo'),
27 | 'tenant_id' => env('CAMUNDA_TENANT_ID', ''),
28 | ],
29 | ```
30 |
31 |
32 | ## Usage
33 |
34 | ### Process Definition
35 | ```php
36 | use Laravolt\Camunda\Http\ProcessDefinitionClient;
37 |
38 | $variables = ['title' => ['value' => 'Sample Title', 'type' => 'string']];
39 |
40 | // Start new process instance
41 | $instance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables);
42 |
43 | // Start new process instance with some business key
44 | $instance = ProcessDefinitionClient::start(key: 'process_1', variables: $variables, businessKey: 'somekey');
45 |
46 |
47 | // Get BPMN definition in XML format
48 | ProcessDefinitionClient::xml(key: 'process_1');
49 | ProcessDefinitionClient::xml(id: 'process_1:xxxx');
50 |
51 | // Get all definition
52 | ProcessDefinitionClient::get();
53 |
54 | // Get definitions based on some parameters
55 | $params = ['latestVersion' => true];
56 | ProcessDefinitionClient::get($params);
57 | ```
58 |
59 | Camunda API reference: https://docs.camunda.org/manual/latest/reference/rest/process-definition/
60 |
61 |
62 |
63 |
64 | ### Process Instance
65 | ```php
66 | use Laravolt\Camunda\Http\ProcessInstanceClient;
67 |
68 | // Find by ID
69 | $processInstance = ProcessInstanceClient::find(id: 'some-id');
70 |
71 | // Get all instances
72 | ProcessInstanceClient::get();
73 |
74 | // Get instances based on some parameters
75 | $params = ['businessKeyLike' => 'somekey'];
76 | ProcessInstanceClient::get($params);
77 |
78 | ProcessInstanceClient::variables(id: 'some-id');
79 | ProcessInstanceClient::delete(id: 'some-id');
80 | ```
81 |
82 | Camunda API reference: https://docs.camunda.org/manual/latest/reference/rest/process-instance/
83 |
84 |
85 | ### Message Event
86 |
87 | ```php
88 | use Laravolt\Camunda\Http\MessageEventClient;
89 | // Start processinstance with message event
90 | // Required
91 | // messageName : message event name
92 | // businessKey : Busniess key for process instance
93 |
94 | // Rerturn Process insntance from message event
95 |
96 | MessageEventClient::start(messageName: "testing", businessKey: "businessKey")
97 |
98 |
99 | ```
100 |
101 |
102 |
103 | ### Task
104 | ```php
105 | use Laravolt\Camunda\Http\TaskClient;
106 |
107 | $task = TaskClient::find(id: 'task-id');
108 | $tasks = TaskClient::getByProcessInstanceId(id: 'process-instance-id');
109 | $tasks = TaskClient::getByProcessInstanceIds(ids: 'arrayof-process-instance-ids');
110 | TaskClient::submit(id: 'task-id', variables: ['name' => ['value' => 'Foo', 'type' => 'String']]); // will return true or false
111 | $variables = TaskClient::submitAndReturnVariables(id: 'task-id', variables: ['name' => ['value' => 'Foo', 'type' => 'String']]) // will return array of variable
112 |
113 | // Claim a Task
114 | $tasks = TaskClient::claim($task_id, $user_id);
115 | // Unclaim a Task
116 | $tasks = TaskClient::unclaim($task_id);
117 | // Assign a Task
118 | $tasks = TaskClient::assign($task_id, $user_id);
119 |
120 |
121 |
122 | ```
123 |
124 | Camunda API reference: https://docs.camunda.org/manual/latest/reference/rest/task/
125 |
126 | ### External Task
127 | ```php
128 | use Laravolt\Camunda\Http\ExternalTaskClient;
129 |
130 | $topics = [
131 | ['topicName' => 'pdf', 'lockDuration' => 600_000]
132 | ];
133 | $externalTasks = ExternalTaskClient::fetchAndLock('worker1', $topics);
134 | foreach ($externalTasks as $externalTask) {
135 | // do something with $externalTask
136 | // Mark as complete after finished
137 | ExternalTaskClient::complete($externalTasks->id);
138 | }
139 |
140 | // Unlock some task
141 | ExternalTaskClient::unlock($task->id)
142 |
143 | // Get task locked
144 | $externalTaskLocked = ExternalTaskClient::getTaskLocked();
145 | ```
146 |
147 | Camunda API reference: https://docs.camunda.org/manual/latest/reference/rest/external-task/
148 |
149 | ### Consume External Task
150 | Create a new job to consume external task via `php artisan make:job ` and modify the skeleton:
151 |
152 | ```php
153 | use Laravolt\Camunda\Dto\ExternalTask;
154 | use Laravolt\Camunda\Http\ExternalTaskClient;
155 |
156 | public function __construct(
157 | public string $workerId,
158 | public ExternalTask $task
159 | ) {
160 | }
161 |
162 | public function handle()
163 | {
164 | // Do something with $this->task, e.g: get the variables and generate PDF
165 | $variables = \Laravolt\Camunda\Http\ProcessInstanceClient::variables($this->task->processDefinitionId);
166 | // PdfService::generate()
167 |
168 | // Complete the task
169 | $status = ExternalTaskClient::complete($this->task->id, $this->workerId);
170 | }
171 |
172 | ```
173 |
174 | Subscribe to some topic:
175 | ```php
176 | // AppServiceProvider.php
177 | use Laravolt\Camunda\Http\ExternalTaskClient;
178 |
179 | public function boot()
180 | {
181 | ExternalTaskClient::subscribe('pdf', GeneratePdf::class);
182 | }
183 | ```
184 |
185 | Register the scheduler:
186 | ```php
187 | // app/Console/Kernel.php
188 | protected function schedule(Schedule $schedule)
189 | {
190 | $schedule->command('camunda:consume-external-task --workerId=worker1')->everyMinute();
191 | }
192 | ```
193 |
194 | If you need shorter pooling time (sub-minute frequency), please check [Laravel Short Schedule](https://github.com/spatie/laravel-short-schedule).
195 |
196 | Reference:
197 | - https://laravel.com/docs/master/scheduling
198 | - https://laravel.com/docs/master/queues
199 | - https://github.com/spatie/laravel-short-schedule
200 |
201 | ### Task History (Completed Task)
202 |
203 | ```php
204 | use Laravolt\Camunda\Http\TaskHistoryClient;
205 |
206 | $completedTask = TaskHistoryClient::find(id: 'task-id');
207 | $completedTasks = TaskHistoryClient::getByProcessInstanceId(id: 'process-instance-id');
208 | ```
209 |
210 | Camunda API reference: https://docs.camunda.org/manual/latest/reference/rest/history/task/
211 |
212 |
213 |
214 | ### Deployment
215 |
216 | ```php
217 | use Laravolt\Camunda\Http\DeploymentClient;
218 |
219 | // Deploy bpmn file(s)
220 | DeploymentClient::create('test-deploy', '/path/to/file.bpmn');
221 | DeploymentClient::create('test-deploy', ['/path/to/file1.bpmn', '/path/to/file2.bpmn']);
222 |
223 | // Get deployment list
224 | DeploymentClient::get();
225 |
226 | // Find detailed info about some deployment
227 | DeploymentClient::find($id);
228 |
229 | // Truncate (delete all) deployments
230 | $cascade = true;
231 | DeploymentClient::truncate($cascade);
232 |
233 | // Delete single deployment
234 | DeploymentClient::delete(id: 'test-deploy', cascade: $cascade);
235 |
236 | ```
237 |
238 |
239 |
240 | ### Raw Endpoint
241 |
242 | You can utilize `Laravolt\Camunda\CamundaClient` to call any Camunda REST endpoint.
243 | ```php
244 | use Laravolt\Camunda\CamundaClient;
245 |
246 | $response = CamundaClient::make()->get('version');
247 | echo $response->status(); // 200
248 | echo $response->object(); // sdtClass
249 | echo $response->json(); // array, something like ["version" => "7.14.0"]
250 | ```
251 | > `CamundaClient::make()` is a wrapper for [Laravel HTTP Client](https://laravel.com/docs/master/http-client) with base URL already set based on your Camunda services configuration. Take a look at the documentation for more information.
252 |
--------------------------------------------------------------------------------
/resources/rekrutmen-sederhana.bpmn:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Flow_02wkiqx
16 | Flow_1bpz4e7
17 |
18 |
19 | Flow_1bpz4e7
20 | Flow_1xc9dz1
21 | Flow_0borlq2
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | Flow_02wkiqx
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Flow_0borlq2
60 | Flow_19x83oz
61 |
62 |
63 |
64 | ${status_administrasi} = true
65 |
66 |
67 |
68 |
69 | ${status_administrasi} = false
70 |
71 |
72 | ${lulus_wawancara = false}
73 |
74 |
75 | Flow_19x83oz
76 | Flow_1m5nwcq
77 | Flow_0lfjxwi
78 |
79 |
80 | ${lulus_wawancara = true}
81 |
82 |
83 | Flow_0lfjxwi
84 |
85 |
86 |
87 | Flow_1xc9dz1
88 | Flow_1m5nwcq
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------