Skip to content

Sample Symfony apps to run an API and workers for Temporal workflows

Notifications You must be signed in to change notification settings

feuzeu/temporal-symfony-samples

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

52 Commits
 
 
 
 
 
 

Repository files navigation

Temporal samples for Symfony

This repo provides sample applications and Docker Compose config to easily get started with Temporal durable workflows and Symfony.

The Symfony applications

There are 3 applications in the temporal-apps subdir.

  • An API to interact (start, query, signal) with the workflows: temporal-apps\workflow-api.
  • A first worker to execute Temporal workflow functions: temporal-apps\workflow-worker.
  • A second worker to execute Temporal activity functions: temporal-apps\activity-worker.

The workers are powered by the RoadRunner application server. The workflow workers and activity workers are configured to listen on two separate queues on the Temporal server.

The API run either with Nginx Unit, FrankenPHP or Nginx+PHP-FPM.

The workflow examples are taken from the Temporal PHP SDK sampes, and modified to adapt to the Symfony applications.

Configuration

There are two config files for Temporal in each Symfony app, config/temporal/runtime.yaml for the Temporal SDK runtime, and config/temporal/services.yaml for the worflows.

Other options are set in the environment section of the containers in the docker/temporal-apps/docker-compose.yml file.

Running the samples

The docker/temporal-server/docker-compose.yml file will start the Temporal server. It is the same as in the Temporal PHP SDK sampes, but without the PHP application container. It needs to be started before running the Symfony applications.

The docker/temporal-apps/docker-compose.yml file will start the 3 Symfony applications, which need to connect to the Temporal server, using the address or hostname set in the environment section in the docker-compose file.

Before starting the applications, first build the containers, then install the PHP packages in each container with Composer.

cd docker/temporal-apps/

docker-compose build
docker-compose run --rm --user temporal activity-worker composer install
docker-compose run --rm --user temporal workflow-worker composer install
docker-compose run --rm --user temporal workflow-api-frankenphp composer install

docker-compose up -d

By default, the workflow-api app will be started in the FrankenPHP container. The other application servers (PHP-FPM and Nginx Unit) can be enabled by uncommenting their definition in the docker-compose.yml file.

Each application server is configured to be available on a separate port:

Swagger

The workflow-api app also provides a Swagger webpage, which is configured in the temporal-apps/workflow-api/config/packages/nelmio_api_doc.yaml file.

The page is available at http://localhost:9300/api/doc.

How it works

Implementing a workflow in these Symfony applications requires to define interfaces, classes and facades (with the https://github.com/lagdo/symfony-facades package), resp. for workflows and activities, together with their respective options.

The facades are required here because we are in a case where dependency injection simply doesn't work.

Workflows and activities

The interfaces and classes for the workflows and activities are the basis when working with Temporal. Many examples of these can be found in the Temporal PHP SDK sampes repo.

In the Symfony applications, the workflow and activity classes are located in the src\Workflow\Service\Workflow and src\Workflow\Service\Activity subdirs. Of course, their namespaces are changed accordingly. They must be tagged resp. with temporal.service.workflow and temporal.service.activity in the Symfony service container, so they are automatically registered to the Temporal server.

See the Summary section below for how to deploy the workflows and activities code in the Symfony apps.

Stubs and facades

When a workflow or an activity function is called, the Temporal library actually uses a proxy class to forward the call to its server, which will in turn forward the same call to an available worker. These proxy classes are called stubs.

It can be supposed that they implement the interfaces of the workflows and activities they are proxying, althougth they actually do not. That's why the Symfony dependency injection cannot be used to inject a stub where a workflow or an activity interface is required.

As a consequence, a facade is used anytime a call to a workflow or an activity function needs to be made. That means:

  • When a workflow is started or called in the workflow-api app.
  • When an activity is called or a child workflow started in the workflow-worker app.

In summary, a facade will use a workflow or activity interface as service identifier, and forward its calls to a Temporal stub that it has picked in the Symfony service container.

The workflow and activity facades are also located in the src\Workflow\Service\Workflow and src\Workflow\Service\Activity subdirs. They must also be tagged resp. with temporal.service.workflow and temporal.service.activity, so the corresponding stubs are automatically registered in the Symfony service container.

Options and attributes

With Temporal, the options of a workflow or an activity function are defined when making the call to the server. In this case, that also means when making a call to a stub using a facade.

In the Symfony apps, the workflow and activity options are defined in the config/temporal/services.yaml config file.

    moneyBatchWorkflowOptions:
        class: 'Temporal\Client\WorkflowOptions'
        factory: ['App\Temporal\Factory\WorkflowFactory', 'moneyBatchOptions']
namespace App\Temporal\Factory;

use Carbon\CarbonInterval;
use Temporal\Client\WorkflowOptions;

class WorkflowFactory
{
    public static function moneyBatchOptions(string $workflowTaskQueue): WorkflowOptions
    {
        return WorkflowOptions::new()
            ->withTaskQueue($workflowTaskQueue)
            ->withWorkflowExecutionTimeout(CarbonInterval::hour());
    }
}

The options are then applied to a stub (or a facade) using an attribute on the corresponding interface.

namespace App\Workflow\Service\Workflow\MoneyBatch;

use App\Temporal\Attribute\WorkflowOptions;
use Temporal\Workflow\WorkflowInterface;

#[WorkflowInterface]
#[WorkflowOptions(serviceId: "moneyBatchWorkflowOptions")]
interface MoneyBatchWorkflowInterface
{
    //
}

Three classes are defined for attributes:

  • App\Temporal\Attribute\WorkflowOptions for workflow options
  • App\Temporal\Attribute\ActivityOptions for activity options
  • App\Temporal\Attribute\ChildWorkflowOptions for child workflow options.

Summary

In summary, here's the steps to implement a new function. We'll take the MoneyBatch workflow as example.

For the workflow

  1. Add the workflow interface and class in the workflow-worker app.

In the src\Workflow\Service\Workflow\MoneyBatch\MoneyBatchWorkflowInterface.php file,

namespace App\Workflow\Service\Workflow\MoneyBatch;

use Temporal\Workflow\WorkflowInterface;

#[WorkflowInterface]
interface MoneyBatchWorkflowInterface
{
}

In the src\Workflow\Service\Workflow\MoneyBatch\MoneyBatchWorkflow.php file,

namespace App\Workflow\Service\Workflow\MoneyBatch;

class MoneyBatchWorkflow implements MoneyBatchWorkflowInterface
{
}
  1. For a main workflow,
  • Define the workflow options and add an attribute to the workflow interface in the workflow-api app.

In the src/Temporal/Factory/WorkflowFactory.php file,

namespace App\Temporal\Factory;

use Carbon\CarbonInterval;
use Temporal\Client\WorkflowOptions;

class WorkflowFactory
{
    public static function moneyBatchOptions(string $workflowTaskQueue): WorkflowOptions
    {
        return WorkflowOptions::new()
            ->withTaskQueue($workflowTaskQueue)
            ->withWorkflowExecutionTimeout(CarbonInterval::hour());
    }
}

In the config\temporal\services.yaml file,

services:
    moneyBatchWorkflowOptions:
        class: 'Temporal\Client\WorkflowOptions'
        factory: ['App\Temporal\Factory\WorkflowFactory', 'moneyBatchOptions']

In the src\Workflow\Service\Workflow\MoneyBatch\MoneyBatchWorkflowInterface.php file,

namespace App\Workflow\Service\Workflow\MoneyBatch;

use App\Temporal\Attribute\WorkflowOptions;
use Temporal\Workflow\WorkflowInterface;

#[WorkflowInterface]
#[WorkflowOptions(serviceId: "moneyBatchWorkflowOptions")]
interface MoneyBatchWorkflowInterface
{
}
  • Add the workflow facade in the workflow-api app.

In the src\Workflow\Service\Workflow\MoneyBatch\MoneyBatchWorkflowFacade.php file,

namespace App\Workflow\Service\Workflow\MoneyBatch;

use App\Temporal\Factory\WorkflowClientTrait;
use Lagdo\Symfony\Facades\AbstractFacade;

class MoneyBatchWorkflowFacade extends AbstractFacade
{
    use WorkflowClientTrait;

    /**
     * @inheritDoc
     */
    protected static function getServiceIdentifier(): string
    {
        return MoneyBatchWorkflowInterface::class;
    }
}

The WorkflowClientTrait trait provides additional helper functions to start a new workflow and get a running workflow. See the src\Controller\MoneyBatchController.php file for examples.

  1. For a child workflow,
  • Define the child workflow options and add an attribute to the workflow interface in the workflow-worker app.

In the src/Temporal/Factory/WorkflowFactory.php file,

namespace App\Temporal\Factory;

use Temporal\Workflow\ChildWorkflowOptions;

class WorkflowFactory
{
    public static function defaultOptions(string $workflowTaskQueue): ChildWorkflowOptions
    {
        return ChildWorkflowOptions::new()
            ->withTaskQueue($workflowTaskQueue);
    }
}

In the config\temporal\services.yaml file,

services:
    defaultChildWorkflowOptions:
        class: 'Temporal\Workflow\ChildWorkflowOptions'
        factory: ['App\Temporal\Factory\WorkflowFactory', 'defaultOptions']

In the src\Workflow\Service\Workflow\Child\ChildWorkflowInterface.php file,

namespace App\Workflow\Service\Workflow\Child;

use App\Temporal\Attribute\ChildWorkflowOptions;
use Temporal\Workflow\WorkflowInterface;

#[WorkflowInterface]
#[ChildWorkflowOptions(serviceId: "defaultChildWorkflowOptions")]
interface ChildWorkflowInterface
{
}
  • Add the child workflow facade in the workflow-worker app.

In the src\Workflow\Service\Workflow\Child\ChildWorkflowFacade.php file,

namespace App\Workflow\Service\Workflow\Child;

use Lagdo\Symfony\Facades\AbstractFacade;

class ChildWorkflowFacade extends AbstractFacade
{
    /**
     * @inheritDoc
     */
    protected static function getServiceIdentifier(): string
    {
        return ChildWorkflowInterface::class;
    }
}

For the activity

  1. Add the activity interface and class in the activity-worker app.

In the src\Workflow\Service\Activity\MoneyBatch\AccountActivityInterface.php file,

namespace App\Workflow\Service\Activity\MoneyBatch;

use Temporal\Activity\ActivityInterface;

#[ActivityInterface(prefix: "MoneyBatch.")]
interface AccountActivityInterface
{
}

In the src\Workflow\Service\Activity\MoneyBatch\AccountActivity.php file,

namespace App\Workflow\Service\Activity\MoneyBatch;

class AccountActivity implements AccountActivityInterface
{
}
  1. Define the activity options and add an attribute to the activity interface in the workflow-worker app.

In the src/Temporal/Factory/ActivityFactory.php file,

namespace App\Temporal\Factory;

use Carbon\CarbonInterval;
use Temporal\Activity\ActivityOptions;
use Temporal\Common\RetryOptions;

class ActivityFactory
{
    public static function moneyBatchOptions(string $activityTaskQueue): ActivityOptions
    {
        return ActivityOptions::new()
            ->withTaskQueue($activityTaskQueue)
            ->withStartToCloseTimeout(CarbonInterval::seconds(15))
            ->withScheduleToCloseTimeout(CarbonInterval::hour(1))
            ->withRetryOptions(
                RetryOptions::new()
                    ->withMaximumAttempts(10)
                    ->withInitialInterval(CarbonInterval::second(1))
                    ->withMaximumInterval(CarbonInterval::seconds(10))
            );
    }
}

In the config\temporal\services.yaml file,

services:
    moneyBatchActivityOptions:
        class: 'Temporal\Activity\ActivityOptions'
        factory: ['App\Temporal\Factory\ActivityFactory', 'moneyBatchOptions']

In the src\Workflow\Service\Activity\MoneyBatch\AccountActivityInterface.php file,

namespace App\Workflow\Service\Activity\MoneyBatch;

use App\Temporal\Attribute\ActivityOptions;
use Temporal\Activity\ActivityInterface;

#[ActivityInterface(prefix: "MoneyBatch.")]
#[ActivityOptions(serviceId: "moneyBatchActivityOptions")]
interface AccountActivityInterface
{
}
  1. Add the activity facade in the workflow-worker app.

In the Workflow\Service\Activity\MoneyBatch\AccountActivityFacade.php file,

namespace App\Workflow\Service\Activity\MoneyBatch;

use Lagdo\Symfony\Facades\AbstractFacade;

class AccountActivityFacade extends AbstractFacade
{
    /**
     * @inheritDoc
     */
    protected static function getServiceIdentifier(): string
    {
        return AccountActivityInterface::class;
    }
}

How it is implemented

Configuration

Runtimes

Compiler passes

Factories

Attributes

PHP application servers

Credits

Temporal PHP SDK samples

Other Temporal Symfony packages

Docker Nginx Unit PHP

Docker PHP

About

Sample Symfony apps to run an API and workers for Temporal workflows

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published