webchick/temporal-http-php

A lightweight HTTP client for Temporal that works on any PHP host. No PECL extensions, no RoadRunner.

★ 1Forks 0PHPGitHub ↗Compare

README

temporal-http-php

A PHP HTTP client for Temporal that works on any PHP host. One composer require, three lines of code, durable workflows. No PECL extensions, no RoadRunner, no persistent processes. If your PHP can make an HTTP request, it can talk to Temporal.


The 60-second example

The killer pattern for LAMP: a Stripe webhook completes a long-running Temporal activity. Your PHP app is the cockpit; the workers live anywhere.

PHP — webhook handler (any framework):

<?php
// In any PHP framework's webhook route — Laravel, WordPress, Drupal, plain PHP

$temporal = Temporal\Http\Client::cloud(
    namespace: $_ENV['TEMPORAL_NAMESPACE'],
    apiKey:    $_ENV['TEMPORAL_API_KEY'],
);

$event = json_decode(file_get_contents('php://input'), true);

if ($event['type'] === 'payment_intent.succeeded') {
    $temporal->completeActivity(
        workflowId: $event['data']['object']['metadata']['workflow_id'],
        activityId: $event['data']['object']['metadata']['activity_id'],
        result:     ['paymentId' => $event['data']['object']['id']],
    );
}

http_response_code(200);

Go — worker side (shown for context):

func OrderWorkflow(ctx workflow.Context, orderID string) error {
    ao := workflow.ActivityOptions{
        ActivityID:          "stripe-payment-" + orderID,
        StartToCloseTimeout: 24 * time.Hour,
    }
    ctx = workflow.WithActivityOptions(ctx, ao)

    var result PaymentResult
    err := workflow.ExecuteActivity(ctx, WaitForStripePayment, orderID).Get(ctx, &result)
    if err != nil {
        return err
    }
    // ... continue workflow with result.PaymentId
    return nil
}

func WaitForStripePayment(ctx context.Context, orderID string) (PaymentResult, error) {
    activity.RecordHeartbeat(ctx, "waiting for stripe webhook")
    return PaymentResult{}, activity.ErrResultPending  // PHP will complete this via HTTP
}

What's happening: The Go worker registers an activity that immediately returns ErrResultPending, parking the workflow until an external caller completes it. Your PHP webhook handler calls completeActivity() over HTTP when Stripe posts the payment event. No PHP worker process, no persistent connection — just a single HTTP POST from your existing LAMP stack.


Installation

Note: This package is not yet listed on Packagist. Once registered, installation will be:

composer require webchick/temporal-http-php

Until then, install directly from GitHub by adding this to your composer.json:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/webchick/temporal-http-php"
        }
    ],
    "require": {
        "webchick/temporal-http-php": "dev-main"
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}

Then run composer install.

Requires PHP 8.1+. No PECL extensions. Guzzle is suggested but any PSR-18 HTTP client works.


Prerequisites: running a Temporal server

You need a Temporal server to talk to. The fastest way to get one locally is the Temporal CLI.

Install the Temporal CLI:

# macOS
brew install temporal

# Linux / WSL
curl -sSf https://temporal.download/cli.sh | sh
# Then add ~/.temporalio/bin to your PATH

# Windows (PowerShell)
winget install Temporal.CLI

Start the local dev server:

$ temporal server start-dev

CLI 1.0.0 (Server 1.24.0, UI 2.26.0)

Server:  localhost:7233
UI:      http://localhost:8233

When you see those lines, the server is ready. Data is in-memory and lost on restart — expected for local dev. The default namespace is created automatically.

Open http://localhost:8233 to see the Web UI, where you can inspect running workflows, view history, and debug.

If you're using Temporal Cloud instead of a local server, skip this section. You'll need a namespace and API key from cloud.temporal.io.


Quickstart

Local dev server:

<?php

require 'vendor/autoload.php';

$temporal = Temporal\Http\Client::selfHosted(
    baseUri:   'http://localhost:7233',
    namespace: 'default',
);

// Start a workflow
$handle = $temporal->startWorkflow(
    workflowType: 'GreetingWorkflow',
    workflowId:   'hello-' . uniqid(),
    taskQueue:    'greeting',
    args:         [['name' => 'World']],
);

echo "Started: " . $handle->workflowId() . " / " . $handle->runId() . PHP_EOL;

// Wait for the result (long-polls until the workflow completes)
$result = $handle->result(timeoutSeconds: 30);

echo "Result: " . json_encode($result) . PHP_EOL;

Temporal Cloud:

<?php

require 'vendor/autoload.php';

$temporal = Temporal\Http\Client::cloud(
    namespace: getenv('TEMPORAL_NAMESPACE'),  // e.g. "my-app.a1b2c"
    apiKey:    getenv('TEMPORAL_API_KEY'),
);

$handle = $temporal->startWorkflow(
    workflowType: 'OrderFulfillmentWorkflow',
    workflowId:   'order-' . $orderId,
    taskQueue:    'fulfillment',
    args:         [['orderId' => $orderId, 'items' => $items]],
);

echo "Started: " . $handle->workflowId() . " / " . $handle->runId() . PHP_EOL;

$result = $handle->result(timeoutSeconds: 30);

echo "Result: " . json_encode($result) . PHP_EOL;

Note: The quickstart assumes a worker is already registered for the workflow type and task queue you reference. The PHP library is a client — it drives workflows, but does not run them. See What this isn't for details.


Seeing it in action: a complete working example

A worker is a process that actually runs your workflow code. Think of it as the back end of Temporal: it polls for tasks, executes the workflow logic, and reports results back. Your PHP code is the front end — it triggers and observes workflows but never runs them directly.

To see a round-trip, you need a worker running alongside your PHP script. Here's a minimal one using the Temporal TypeScript SDK — if you've ever used npm for front-end tooling, you already have everything you need:

1. Create a new directory and install dependencies:

mkdir greeting-worker && cd greeting-worker
npm init -y
npm install @temporalio/worker @temporalio/workflow
npm install --save-dev typescript ts-node

2. Save these two files:

workflows.ts — the workflow logic:

export async function GreetingWorkflow(input: { name: string }): Promise<string> {
  return `Hello, ${input.name}!`;
}

worker.ts — the process that runs it:

import { Worker } from '@temporalio/worker';

async function run(): Promise<void> {
  const worker = await Worker.create({
    workflowsPath: require.resolve('./workflows'),
    taskQueue: 'greeting',
  });
  console.log('Worker running. Press Ctrl+C to stop.');
  await worker.run();
}

run().catch(err => { console.error(err); process.exit(1); });

The workflow lives in its own file because Temporal runs it in an isolated sandbox to guarantee determinism. The worker file is just the harness that connects it to the server.

3. Open three terminals:

Terminal Command What it does
1 temporal server start-dev Runs the Temporal server
2 npx ts-node worker.ts (inside greeting-worker/) Executes your workflow logic
3 php quickstart.php Triggers the workflow from PHP

Terminal 3 should print Result: "Hello, World!". Open http://localhost:8233 to see the full execution timeline in the Web UI.

Workers can be written in any language with a Temporal SDK — Go, Python, TypeScript, Java, .NET. Your PHP client talks to any of them without modification.


Common patterns

Triggering from a Laravel controller

// TODO: placeholder — add Laravel controller example

Handling a webhook with completeActivity

// TODO: placeholder — see "60-second example" above for full walkthrough

Querying running workflows from a WordPress admin page

// TODO: placeholder — add WordPress admin example using listWorkflows() / queryWorkflow()

Authentication

Temporal Cloud (API key)

$temporal = Temporal\Http\Client::cloud(
    namespace: $_ENV['TEMPORAL_NAMESPACE'],  // e.g. "my-app.a1b2c"
    apiKey:    $_ENV['TEMPORAL_API_KEY'],
);

Recommended env vars: TEMPORAL_NAMESPACE, TEMPORAL_API_KEY.

Self-hosted (no auth)

$temporal = Temporal\Http\Client::selfHosted(
    baseUri:   'http://localhost:7233',
    namespace: 'default',
);

Self-hosted with mTLS

$temporal = Temporal\Http\Client::selfHostedMtls(
    baseUri:   'https://temporal.internal:7233',
    namespace: 'production',
    certPath:  '/etc/temporal/client.pem',
    keyPath:   '/etc/temporal/client.key',
    caPath:    '/etc/temporal/ca.pem',   // optional
);

API reference

Full method-level documentation is generated from docblocks. See the generated reference at:

TODO: link to generated API docs (e.g. GitHub Pages / php.watch)

For a quick overview, see src/ClientInterface.php.


What this isn't

This is a client library — it lets PHP applications trigger and interact with Temporal workflows. It does not:

  • Let you author workflows in PHP. For that, use the official temporalio/sdk-php (requires ext-grpc and RoadRunner).
  • Let you run PHP activity workers. Activities must live in a language with a full SDK (Go, Python, TypeScript, Java, .NET). Framework integration packages (temporal-http-php-laravel, etc.) are planned but not yet built.
  • Cover the Operator Service (namespace administration, cluster management).

If you need to write workflow logic in PHP itself, temporalio/sdk-php is the right tool. If you're on a shared host or a traditional LAMP stack and just need to drive Temporal from PHP, this library is for you.


Comparison: temporal-http-php vs temporalio/sdk-php

webchick/temporal-http-php temporalio/sdk-php
Works on shared hosting Yes No
Requires ext-grpc No Yes
Requires RoadRunner No Yes
Start / signal / query workflows Yes Yes
Author workflow logic in PHP No Yes
Run PHP activity workers No Yes
Async activity completion Yes Yes
Schedules Yes Yes
Dependencies PSR-18 only grpc, protobuf, RoadRunner
PHP version 8.1+ 8.1+

Contributing

TODO: placeholder — add CONTRIBUTING.md link and development setup instructions.

License

MIT — see LICENSE.

Contributors

webchick

Issues