This package allow an easy integration of a Laravel app with a temporal.io, which is a distributed, scalable, durable, and highly available orchestration engine for asynchronous long-running business logic in a microservice architecture.
This package provides:
- Commands to create a new workflow and activity
- Command to start the worker which will execute workflows and activities from the provided task queue
- Testing helpers that allows mock of workflows and activities executions
You can install the package via composer:
composer require keepsuit/laravel-temporal
You can publish the config file with:
php artisan vendor:publish --tag="temporal-config"
This is the contents of the published config file:
<?php
return [
/**
* Temporal server address
*/
'address' => env('TEMPORAL_ADDRESS', 'localhost:7233'),
/**
* Temporal namespace
*/
'namespace' => env('TEMPORAL_NAMESPACE', \Temporal\Client\ClientOptions::DEFAULT_NAMESPACE),
/**
* Default task queue
*/
'queue' => \Temporal\WorkerFactory::DEFAULT_TASK_QUEUE,
/**
* Default retry policy
*/
'retry' => [
/**
* Default retry policy for workflows
*/
'workflow' => [
/**
* Initial retry interval (in seconds)
* Default: 1
*/
'initial_interval' => null,
/**
* Retry interval increment
* Default: 2.0
*/
'backoff_coefficient' => null,
/**
* Maximum interval before fail
* Default: 100 x initial_interval
*/
'maximum_interval' => null,
/**
* Maximum attempts
* Default: unlimited
*/
'maximum_attempts' => null,
],
/**
* Default retry policy for activities
*/
'activity' => [
/**
* Initial retry interval (in seconds)
* Default: 1
*/
'initial_interval' => null,
/**
* Retry interval increment
* Default: 2.0
*/
'backoff_coefficient' => null,
/**
* Maximum interval before fail
* Default: 100 x initial_interval
*/
'maximum_interval' => null,
/**
* Maximum attempts
* Default: unlimited
*/
'maximum_attempts' => null,
],
],
/**
* Manual register workflows
*/
'workflows' => [
],
/**
* Manual register activities
*/
'activities' => [
],
/**
* Directories to watch when server is started with `--watch` flag
*/
'watch' => [
'app',
'config',
],
/**
* Integrations options
*/
'integrations' => [
/**
* Eloquent models serialization/deserialization options
*/
'eloquent' => [
/**
* Default attribute key case conversion when serialize a model before sending to temporal.
* Supported values: 'snake', 'camel', null.
*/
'serialize_attribute_case' => null,
/**
* Default attribute key case conversion when deserializing payload received from temporal.
* Supported values: 'snake', 'camel', null.
*/
'deserialize_attribute_case' => null,
/*
* If true adds a `__exists` attribute to the serialized model
* which indicate that the model is saved to database and it is used on deserialization when creating the model.
* If false (or `__exists` is not present) the model will be created as existing model if primary key is present.
*/
'include_exists_field' => false,
],
],
];
Here we will see the utilities provided by this package. For more information about Temporal and Workflow/Activity options please refer to the official documentation.
To create a new workflow, you can use the make:workflow {name}
command, which will create a new workflow interface & relative class in
the app/Temporal/Workflows
directory.
To create a new activity, you can use the make:activity {name}
command, which will create a new activity interface & relative class in
the app/Temporal/Activities
directory.
Note
If you already have workflow/activities in app/Workflows
and app/Activities
directories,
the make commands will create the new workflow/activity in the these directories.
Workflows in app/Temporal/Workflows
and app/Workflows
and activities in app/Temporal/Activities
, app/Activities
, app/Temporal/Workflows
and app/Workflows
are automatically registered.
If you put your workflows and activities in other directories, you can register them manually in the workflows
and activities
config keys.
To start a workflow, you must build a stub through the Temporal
Facade.
$workflow = Temporal::newWorkflow()
->withTaskQueue('custom-task-queue') // Workflow options can be provided with fluent methods
->build(YourWorkflowInterface::class);
// This will start a new workflow execution and wait for the result
$result = $workflow->yourMethod();
// This will start a new workflow execution and return immediately
app(\Temporal\Client\WorkflowClientInterface::class)->start($workflow);
To start an activity, you must build a stub through the Temporal
Facade (note that activities must be built inside a workflow).
Activity methods returns a Generator, so you must use the yield
keyword to wait for the result.
$activity = Temporal::newActivity()
->withTaskQueue('custom-task-queue') // Activity options can be provided with fluent methods
->build(YourActivityInterface::class);
$result = yield $activity->yourActivityMethod();
Child workflows works like activity and like activities must be built inside a workflow.
$childWorkflow = Temporal::newChildWorkflow()
->build(YourChildWorkflowInterface::class);
$result = yield $childWorkflow->yourActivityMethod();
Payloads provided to workflows/activities as params and returned from them must be serialized, sent to the Temporal server and deserialized by the worker. Activities can be executed by workers written in different languages, so the payload must be serialized in a common format. Out of the box temporal sdk supports native php types and protobuf messages. This package adds some laravel specific options for serialization/deserialization of objects:
TemporalSerializable
interface can be implemented to add support for custom serialization/deserialization.- Eloquent models can be correctly serialized/deserialized (with relations) adding
TemporalSerializable
interface andTemporalEloquentSerialize
trait. - spatie/laravel-data data objects are supported out of the box.
To improve laravel-data support, this package provides
TemporalSerializableCast
andTemporalSerializableTransformer
to add support for serialization/deserialization ofTemporalSerializable
objects used asData
properties. You can add them todata.casts
anddata.transformers
config to add support globally.
To run the temporal worker, you can use the temporal:work {queue?}
command.
In order to test workflows end-to-end, you need a temporal server running. This package provides two options to run a temporal server for testing purposes:
- Run
temporal:server
command, which will start a temporal testing server and use theWithTemporalWorker
trait which will start a test worker - Use the
WithTemporal
trait, which will start a temporal testing server and the test worker when running test and stop it on finish
When using
WithTemporal
trait, you can setTEMPORAL_TESTING_SERVER
env variable tofalse
to disable the testing server and run only the worker.
Mocking a workflow can be useful when the workflow should be executed in another service or simply when you want to test other parts of your code without running the workflow. This works for child workflows too.
Temporal::fake();
$workflowMock = Temporal::mockWorkflow(YourWorkflowInterface::class)
->onTaskQueue('custom-queue'); // not required but useful for mocking and asserting that workflow is executed on the correct queue
->andReturn('result');
// Your test code...
$workflowMock->assertDispatched();
$workflowMock->assertDispatchedTimes(1);
$workflowMock->assertNotDispatched();
// All assertion method support a callback to assert the workflow input
$workflowMock->assertDispatched(function ($input) {
return $input['foo'] === 'bar';
});
Mocking activities works like workflows, but for activity you must provide interface and the method to mock.
Temporal::fake();
$activityMock = Temporal::mockActivity([YourActivityInterface::class, 'activityMethod'])
->onTaskQueue('custom-queue'); // not required but useful for mocking and asserting that activity is executed on the correct queue
->andReturn('result');
// Your test code...
$activityMock->assertDispatched();
$activityMock->assertDispatchedTimes(1);
$activityMock->assertNotDispatched();
// All assertion method support a callback to assert the activity input
$activityMock->assertDispatched(function ($input) {
return $input['foo'] === 'bar';
});
Dispatches assertions can be done through the Temporal
facade but there are some downsides compared to the options above:
- You must provide the workflow/activity interface and method name, so this is duplicated
- If you want to ensure that the workflow/activity is executed on the correct queue, you must check the task queue yourself
Temporal::assertWorkflowDispatched(YourWorkflowInterface::class, function($workflowInput, string $taskQueue) {
return $workflowInput['foo'] === 'bar' && $taskQueue === 'custom-queue';
});
Temporal::assertActivityDispatched([YourActivityInterface::class, 'activityMethod'], function($activityInput, string $taskQueue) {
return $activityInput['foo'] === 'bar' && $taskQueue === 'custom-queue';
});
This package provides a PHPStan extension to improve the experience when working with Temporal proxy classes.
If you have phpstan/extension-installer
installed, you are ready to go.
Otherwise, you have to add the extension to your phpstan.neon
file:
includes:
- ./vendor/keepsuit/laravel-temporal/extension.neon
composer test
Please see CHANGELOG for more information on what has changed recently.
The MIT License (MIT). Please see License File for more information.