Giter Club home page Giter Club logo

laravel-temporal's Introduction

Laravel temporal.io

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

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, activity and interceptor
  • Command to start the worker which will execute workflows and activities from the provided task queue
  • Command to start a temporal dev server
  • Testing helpers that allows mock of workflows and activities executions

Installation

You can install the package via composer:

composer require keepsuit/laravel-temporal

Then download the latest roadrunner executable for your platform:

php artisan temporal:install

or

./vendor/bin/rr get-binary

Note

You should run this command after every update to ensure that you have the latest version of roadrunner executable.

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,
        ],
    ],
    
    /**
     * Interceptors (middlewares) registered in the worker
     */
    'interceptors' => [
    ],

    /**
     * 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,
        ],
    ],
];

Usage

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.

Create workflows and activities

To create a new workflow, you can use the temporal: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 temporal: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 or with TemporalRegistry in your service provider.

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->callAfterResolving(\Keepsuit\LaravelTemporal\TemporalRegistry::class, function (\Keepsuit\LaravelTemporal\TemporalRegistry $registry) {
            $registry->registerWorkflows(YourWorkflowInterface::class)
                ->registerActivities(YourActivityInterface::class);
        }
        
        // or
        
        Temporal::registry()
            ->registerWorkflows(YourWorkflowInterface::class)
            ->registerActivities(YourActivityInterface::class);
    }
}

Build and start a workflow

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
Temporal::workflowClient()->start($workflow);

Build and start an activity

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();

Build and start a child workflow

Child workflows works like activity and like activities must be built inside a workflow.

$childWorkflow = Temporal::newChildWorkflow()
    ->build(YourChildWorkflowInterface::class);

$result = yield $childWorkflow->yourActivityMethod();

Input and output payloads

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 and TemporalEloquentSerialize trait.
  • spatie/laravel-data data objects are supported out of the box.

Spatie/Laravel-Data support

spatie/laravel-data is a package that provides a simple way to work with data objects in Laravel. In order to take full advantage of laravel-data, it is suggested to use v4.3.0 or higher.

Note

The provided TemporalSerializableCastAndTransformer is compatible only with laravel-data v4.3 or higher, if you are using an older version you can create your cast/transform.

Changes to be made in config/data.php:

    // Enable iterables cast/transform
    'features' => [
        'cast_and_transform_iterables' => true,
    ],

    // Add support for TemporalSerializable transform
    'transformers' => [
        //...
        \Keepsuit\LaravelTemporal\Contracts\TemporalSerializable::class => \Keepsuit\LaravelTemporal\Integrations\LaravelData\TemporalSerializableCastAndTransformer::class,
    ],

    // Add support for TemporalSerializable cast
    'casts' => [
        //...
        \Keepsuit\LaravelTemporal\Contracts\TemporalSerializable::class => \Keepsuit\LaravelTemporal\Integrations\LaravelData\TemporalSerializableCastAndTransformer::class,
    ],

Interceptors

Temporal interceptors are similar to laravel middleware and can be used to modify inbound and outbound SDK calls. Interceptors can be registered in the interceptors config key. See temporal sdk v2.7 release notes for more information. To create a new interceptor, you can use the temporal:make:interceptor {name} command, which will create a new interceptor class in the app/Temporal/Interceptors directory.

Run the temporal worker

To run the temporal worker, you can use the temporal:work {queue?} command.

Testing utilities

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 the WithTemporalWorker 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 set TEMPORAL_TESTING_SERVER env variable to false to disable the testing server and run only the worker.

Time skipping

The default temporal server implementation is the dev server included in the temporal cli and this doesn't support time skipping. In order to enable time skipping, you must:

  • Run the temporal:server command with the --enable-time-skipping flag.
  • Set TEMPORAL_TESTING_SERVER_TIME_SKIPPING env variable to true when using WithTemporal trait.

Mocking workflows

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

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';
});

Assertions

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';
});

PHPStan

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

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Credits

License

The MIT License (MIT). Please see License File for more information.

laravel-temporal's People

Contributors

cappuc avatar dependabot[bot] avatar michael-rubel avatar slnw avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

laravel-temporal's Issues

Relations of UUID models not working when using temporal serialize

The model -

class SmsCampaignSend extends Model implements TemporalSerializable
{
    use HasMeta;
    use SoftDeletes;
    use TemporalEloquentSerialize;
    use HasUuids;

    protected $fillable = [
        'id',
        'sms_campaign_id',
        'status',
        'meta',
    ];

    public function campaign(): BelongsTo
    {
        return $this->belongsTo(SmsCampaign::class);
    }
}

doesn't work when calling the $campaignSend->campaign relation in an activity after doing the serialize

Root folder for Temporal stuff

Hi @cappuc

Wouldn't it be better to have Workflows and Activities registered automatically/placed by artisan commands in the app/Temporal folder instead of the root? In bigger projects that's a good separation because you can instantly see it's Temporal-related, especially if you have Actions and other Laravel layers in the root.

Currently:

Screenshot_1

It could have been:

Screenshot_2

Running child workflows don't get picked up by the worker

Hi, after creating child workflows with

$smsWorkflow = Temporal::newChildWorkflow()
                    ->build(SendSmsWorkflow::class);
                $res = $smsWorkflow->handle($data);

Child workflow class -

#[WorkflowInterface]
class SendSmsWorkflow
{
    public function __construct()
    {
        //..
    }

    #[WorkflowMethod]
    public function handle(CampaignSendToBuildSmsData $data): \Generator
    {
        //..
    }
}

The child workflows don't get picked up by the worker -
artisan temporal:work

But when creating it as a new workflow it does get picked up. Am I missing something?

Random error in testing environment

Hi @cappuc!

Tests are randomly failing with the following error:

Error 'rpc: can't find service kv.Clear' on tcp://127.0.0.1:6001

Tried to bootstrap the server/workers after each test, but it didn't help.
Do you have any idea why this happen?

I have a fresh Laravel 10 & Sail dev environment with an additional GRPC extension installed.

Getting an error with the chokidar package when using php artisan temporal:work --watch

This is the full error:

Comand:
sail artisan temporal:work --watch

Watcher process has terminated. Please ensure Node and chokidar are installed.
file:///var/www/html/vendor/keepsuit/laravel-temporal/bin/file-watcher.js:1
const chokidar = require('chokidar');
ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/var/www/html/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///var/www/html/vendor/keepsuit/laravel-temporal/bin/file-watcher.js:1:18
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)

The solution in file-watcher.js file change: this: const chokidar = require('chokidar'); for this: import chokidar from "chokidar";

Allow in config/temporal.php to over ride the WorkerFactory::class that is being used by the worker

WorkerFactory::class allows to define many configurations in the Temporal workflow, it's important when using this package that a user can easily extend the WorkerFactory class and make changes as necessary.

Another option is to allow to change the default Worker command in WorkCommand:

            ...['-o', sprintf('server.command=%s ./vendor/bin/roadrunner-temporal-worker', (new PhpExecutableFinder())->find())],

add here an option to override the roadrunner-temportal-worker path.

Support for parallel testing?

About

Currently, the tests are failing when running in parallel (unable to start the worker).
To run tests in parallel, I'm using --parallel flag available in Pest v2 (uses brianium/paratest under the hood).


scr

Unable to find rr binary

Trying to start temporal:work I'm getting the warning

"Your RoadRunner binary version may be incompatible with laravel temporal."

And then the worker exits. Tracking it down a bit it seems that its unable to find the rr binary.

I've checked and it does exist in vender/bin is there a permission error or something I'm missing?

Temporal mock workflow unable to be picked up during test

I try to mock a workflow and test it with test utility. However, the mock workflow seems to be not being picked up.

My test file:

use WithTemporal;

public function setUp(): void
{   
        parent::setUp();

}

/**
 * @test
 * /
public function test_function():
{
        Temporal::fake();
        $myWorkflowMock = Temporal::mockWorkflow(MyWorkflowInterface::class)
            ->onTaskQueue('default')
            ->andReturn('result');

        $myService = app(MyServiceInterface::class);
        $myService->start();

        $myWorkflowMock->assertDispatched();
}

My service function:

public function start(): string
{
        $workflowStub = Temporal::newWorkflow()
            ->withRetryOptions(
                RetryOptions::new()->withMaximumAttempts(1)
            )
            ->build(MyWorkflowInterface::class);

        $workflow = app(WorkflowClient::class)
            ->start($workflowStub);

        return $workflow->getExecution()->getID();
}

When I dump $workflow->getExecution() in my service function, I am able to get an execution ID and workflow ID. However, my test is failing and saying the workflow is not dispatched. Looks like the original workflow is still getting dispatched.

Is there anyway to debug this?

RuntimeException: Failed to start Temporal test server: qemu-x86_64: Could not open '/lib64/ld-linux-x86-64.so.2': No such file or directory

Hi There Community,

I am running the latest Laravel version with the latest version of the package on an Apple M1 using Laravel Sail.

I wanted to use the Temporal server built-in on this package for testing purposes so I imported the trait WithTemporal into the testing and added the Temporal::fake(); at the top. However, when running the test, I found the following error:

Failed to start Temporal test server: qemu-x86_64: Could not open '/lib64/ld-linux-x86-64.so.2': No such file or directory

I have been reading about the issue and it seems that the docker image needs to be from a specific Linux version that is not what the Sail team is currently using.

Does this mean the testing server will not run on the Sail Docker environment running on Mac?
Thanks

Here is a copy of the full test:

class PayOutWorkflowTest extends TestCase
{
    use WithTemporal;

    /**
     * @test
     */
    public function it_can_run_payout_workflow()
    {
        Temporal::fake();

        $workflowMock = Temporal::mockWorkflow(PayOutWorkflowInterface::class)
            ->onTaskQueue('default')
            ->andReturn('result');

        $workflowMock->assertDispatched();
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.