keepsuit/laravel-temporal

Laravel temporal.io

1.0.0-beta5 2024-08-10 13:09 UTC

README

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

此包允许轻松地将 Laravel 应用与 temporal.io 集成,temporal.io 是一个用于微服务架构中异步长运行业务逻辑的分布式、可扩展、持久化和高可用编排引擎。

此包提供以下功能

  • 创建新工作流、活动和拦截器的命令
  • 启动工作器的命令,该工作器将从提供的任务队列中执行工作流和活动
  • 启动 temporal 开发服务器的命令
  • 测试辅助工具,允许模拟工作流和活动的执行

安装

您可以通过 composer 安装此包

composer require keepsuit/laravel-temporal

然后下载您平台的最新 roadrunner 可执行文件

php artisan temporal:install

./vendor/bin/rr get-binary

注意

您应该在每次更新后运行此命令,以确保您有最新的 roadrunner 可执行文件。

您可以使用以下命令发布配置文件

php artisan vendor:publish --tag="temporal-config"

这是已发布配置文件的内容

<?php

return [
    /**
     * Temporal server address
     */
    'address' => env('TEMPORAL_ADDRESS', 'localhost:7233'),

    /**
     * TLS configuration (optional)
     * Allows to configure the client to use a secure connection to the server.
     */
    'tls' => [
        /**
         * Path to the client key file (/path/to/client.key)
         */
        'client_key' => env('TEMPORAL_TLS_CLIENT_KEY'),
        /**
         * Path to the client cert file (/path/to/client.pem)
         */
        'client_cert' => env('TEMPORAL_TLS_CLIENT_CERT'),
        /**
         * Path to the root CA certificate file (/path/to/ca.cert)
         */
        'root_ca' => env('TEMPORAL_TLS_ROOT_CA'),
        /**
         * Override server name (default is hostname) to verify against the server certificate
         */
        'server_name' => env('TEMPORAL_TLS_SERVER_NAME'),
    ],

    /**
     * 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 additional metadata fields (`__exists`, `__dirty`) to the serialized model to improve deserialization.
             * `__exists`: indicate that the model is saved to database.
             * `__dirty`: indicate that the model has unsaved changes. (original values are not included in the serialized payload but the deserialized model will be marked as dirty)
             */
            'include_metadata_field' => false,
        ],
    ],
];

用法

以下我们将看到此包提供的工具。有关 Temporal 以及工作流/活动选项的更多信息,请参阅 官方文档

创建工作流和活动

要创建新的工作流,您可以使用 temporal:make:workflow {name} 命令,这将创建一个新工作流接口和相关类,位于 app/Temporal/Workflows 目录中。

要创建新的活动,您可以使用 temporal:make:activity {name} 命令,这将创建一个新活动接口和相关类,位于 app/Temporal/Activities 目录中。

注意

如果您已经在 app/Workflowsapp/Activities 目录中创建了工作流/活动,make 命令将在这些目录中创建新的工作流/活动。

app/Temporal/Workflowsapp/Workflows 目录中的工作流以及 app/Temporal/Activitiesapp/Activitiesapp/Temporal/Workflowsapp/Workflows 目录中的活动会自动注册。如果您将工作流和活动放在其他目录中,您可以在 workflowsactivities 配置键中或使用服务提供者中的 TemporalRegistry 手动注册它们。

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

构建并启动工作流

要启动工作流,您必须通过 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);

构建并启动活动

要启动活动,您必须通过 Temporal Facade 构建存根(注意,活动必须在工作流内部构建)。活动方法返回一个生成器,因此您必须使用 yield 关键字等待结果。

$activity = Temporal::newActivity()
    ->withTaskQueue('custom-task-queue') // Activity options can be provided with fluent methods
    ->build(YourActivityInterface::class);

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

构建并启动子工作流

子工作流的工作方式类似于活动,并且必须在工作流内部构建活动。

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

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

输入和输出有效载荷

提供给工作流/活动的参数以及从它们返回的有效载荷必须进行序列化,发送到 Temporal 服务器并由工作器反序列化。活动可以由用不同语言编写的工人执行,因此有效载荷必须以公共格式序列化。默认情况下,temporal sdk 支持原生 PHP 类型以及 protobuf 消息。此包为对象的序列化和反序列化添加了一些 Laravel 特定选项

  • TemporalSerializable 接口可以被实现以添加对自定义序列化/反序列化的支持。
  • 通过添加 TemporalSerializable 接口和 TemporalEloquentSerialize 特性,Eloquent 模型可以正确地进行序列化/反序列化(包含关系)。
  • 默认支持 spatie/laravel-data 数据对象。

Spatie/Laravel-Data 支持

spatie/laravel-data 是一个提供在 Laravel 中简单处理数据对象方式的包。为了充分利用 laravel-data,建议使用 v4.3.0 或更高版本。

注意

提供的 TemporalSerializableCastAndTransformer 仅与 laravel-data v4.3 或更高版本兼容,如果您使用的是旧版本,您可以创建自己的转换/转换器。

需要在 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,
    ],

拦截器

Temporal 拦截器与 Laravel 中间件类似,可用于修改 SDK 的入站和出站调用。拦截器可以在 interceptors 配置键中注册。有关更多信息,请参阅 temporal sdk v2.7 发布说明。要创建新的拦截器,可以使用 temporal:make:interceptor {name} 命令,它将在 app/Temporal/Interceptors 目录中创建一个新的拦截器类。

运行 temporal 工作者

要运行 temporal 工作者,可以使用 temporal:work {queue?} 命令。

如果您想自定义 temporal 工作者选项,可以在服务提供程序中调用 Temporal::buildWorkerOptionsUsing

class AppServiceProvider extends ServiceProvider
{
    public function boot(): vodi {
        \Keepsuit\LaravelTemporal\Facade\Temporal::buildWorkerOptionsUsing(function (string $taskQueue) {
            // you can build different worker options based on the task queue
            return \Temporal\Worker\WorkerOptions::new()
                ->withMaxConcurrentActivityTaskPollers(10)
                ->withMaxConcurrentWorkflowTaskPollers(10);
        });
    }
}

测试工具

为了端到端测试工作流,需要运行一个 temporal 服务器。此包提供了两种选项来运行用于测试目的的 temporal 服务器

  • 运行 temporal:server 命令,这将启动一个 temporal 测试服务器并使用 WithTemporalWorker 特性启动一个测试工作者
  • 使用 WithTemporal 特性,这将启动一个 temporal 测试服务器和测试工作者,并在测试完成后停止它

当使用 WithTemporal 特性时,您可以将 TEMPORAL_TESTING_SERVER 环境变量设置为 false 以禁用测试服务器并仅运行工作者。

时间跳过

默认的 temporal 服务器实现是包含在 temporal 命令行工具中的 dev 服务器,它不支持时间跳过。为了启用时间跳过,您必须

  • 使用带有 --enable-time-skipping 标志的 temporal:server 命令。
  • 当使用 WithTemporal 特性时,将 TEMPORAL_TESTING_SERVER_TIME_SKIPPING 环境变量设置为 true

模拟工作流

当工作流应在另一个服务中执行或您只想在不运行工作流的情况下测试代码的其他部分时,模拟工作流很有用。这也适用于子工作流。

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

模拟活动

模拟活动的工作方式与工作流类似,但对于活动,您必须提供要模拟的接口和方法。

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

断言

可以通过 Temporal 门面执行断言,但与上述选项相比,有一些缺点

  • 您必须提供工作流/活动的接口和方法名称,因此这是重复的
  • 如果您想确保工作流/活动在正确的队列上执行,您必须自己检查任务队列
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

此包提供 PHPStan 扩展以提高使用 Temporal 代理类的体验。

如果您已安装 phpstan/extension-installer,则已准备好使用。否则,您必须将扩展添加到您的 phpstan.neon 文件中

includes:
    - ./vendor/keepsuit/laravel-temporal/extension.neon

测试

composer test

更改日志

有关最近更改的更多信息,请参阅 CHANGELOG

致谢

许可

MIT许可证。请参阅许可文件以获取更多信息。