Skip to content

Conversation

@roxblnfk
Copy link
Collaborator

@roxblnfk roxblnfk commented Oct 28, 2025

What was changed

See the documentation below

Checklist

  1. Closes [Feature Request] Environment Configuration #628
  2. How was this tested: added unit tests

Documentation: External Client Configuration Support

Common docs: https://docs.temporal.io/develop/environment-configuration

The PHP SDK now implements the Temporal external client configuration specification, enabling standardized configuration management across development, staging, and production environments.

This feature allows you to:

  • Store client configuration in TOML files with multiple named profiles (note: requires internal/toml package)
  • Override settings via environment variables
  • Switch between environments without code changes
  • Share configuration files across your team

Configuration File

Create a temporal.toml file in your platform-specific configuration directory:

  • Linux/Unix: ~/.config/temporalio/temporal.toml (or $XDG_CONFIG_HOME/temporalio/temporal.toml)
  • macOS: ~/Library/Application Support/temporalio/temporal.toml
  • Windows: %APPDATA%\temporalio\temporal.toml
[profile.development]
address = "localhost:7233"
namespace = "default"

[profile.production]
address = "my-namespace.a1b2c.tmprl.cloud:7233"
namespace = "my-namespace.a1b2c"
api_key = "your-api-key-here"

[profile.production.tls]
client_cert_path = "/path/to/client.pem"
client_key_path = "/path/to/client.pem"

[profile.production.grpc_meta]
custom-header = "custom-value"

Loading Configuration

The configuration system supports multiple loading strategies:

use Temporal\Common\EnvConfig\ConfigClient;

// Load a single profile from default location with environment overrides
$profile = ConfigClient::load(
    profileName: 'production',  // or use TEMPORAL_PROFILE env var
);

// Convert to ServiceClient and ClientOptions
$serviceClient = $profile->toServiceClient();
$clientOptions = $profile->toClientOptions();

// Create Temporal clients
use Temporal\Client\WorkflowClient;
use Temporal\Client\ScheduleClient;

$workflowClient = WorkflowClient::create($serviceClient, $clientOptions);
$scheduleClient = ScheduleClient::create($serviceClient, $clientOptions);

Environment Variables

All configuration keys can be overridden via environment variables using the TEMPORAL_* prefix:

# Connection settings
export TEMPORAL_ADDRESS="localhost:7233"
export TEMPORAL_NAMESPACE="my-namespace"
export TEMPORAL_API_KEY="my-secret-key"

# Profile selection
export TEMPORAL_PROFILE="production"

# Config file override
export TEMPORAL_CONFIG_FILE="/custom/path/to/config.toml"

# TLS configuration
export TEMPORAL_TLS="true"
export TEMPORAL_TLS_CLIENT_CERT_PATH="/path/to/cert.pem"
export TEMPORAL_TLS_CLIENT_KEY_PATH="/path/to/key.pem"
export TEMPORAL_TLS_SERVER_CA_CERT_PATH="/path/to/ca.pem"
export TEMPORAL_TLS_SERVER_NAME="custom-server"

# gRPC metadata headers
export TEMPORAL_GRPC_META_X_CUSTOM_HEADER="custom-value"
export TEMPORAL_GRPC_META_AUTHORIZATION="Bearer token"

Environment variables take precedence over configuration file values, enabling easy overrides for CI/CD pipelines and containerized deployments.

Configuration Hierarchy

Configuration is loaded with the following precedence (later values override earlier ones):

  1. Profile from TOML file (if file exists at default location or specified via TEMPORAL_CONFIG_FILE)
  2. Environment variable overrides (any TEMPORAL_* variables)
  3. SDK defaults (applied for unspecified values)

Default Profile Behavior

When loading a profile without explicitly specifying a name:

  • If no profile name is provided and TEMPORAL_PROFILE is not set, the SDK looks for a profile named "default"
  • If the "default" profile doesn't exist and was NOT explicitly requested, the SDK returns an empty profile (matches Rust SDK behavior)
  • If a profile is explicitly requested (via profileName parameter or TEMPORAL_PROFILE env var) but doesn't exist, a ProfileNotFoundException is thrown
// TOML file with only 'production' profile
[profile.production]
address = "prod.temporal.io:7233"

// Case 1: No explicit profile request - returns empty profile (no exception)
$profile = ConfigClient::load();  // Returns empty ConfigProfile

// Case 2: Explicit profile request - throws exception
$profile = ConfigClient::load(profileName: 'default');  // ❌ Throws ProfileNotFoundException

// Case 3: Profile requested via env var - throws exception
putenv('TEMPORAL_PROFILE=staging');
$profile = ConfigClient::load();  // ❌ Throws ProfileNotFoundException

Profile Management

Loading a Specific Profile

The load() method returns a single ConfigProfile instance:

// Load the 'production' profile
$profile = ConfigClient::load(profileName: 'production');

// Load using TEMPORAL_PROFILE environment variable (defaults to 'default')
$profile = ConfigClient::load();

// Load with custom TOML file
$profile = ConfigClient::load(
    profileName: 'staging',
    configFile: '/path/to/temporal.toml'
);

// Load with custom environment variables
$profile = ConfigClient::load(
    profileName: 'production',
    env: ['TEMPORAL_ADDRESS' => 'localhost:7233', ...]
);

Working with Multiple Profiles

To access multiple profiles from a TOML file, use loadFromToml():

// Load all profiles from TOML
$config = ConfigClient::loadFromToml('/path/to/temporal.toml');

// Check if profile exists
if ($config->hasProfile('staging')) {
    $profile = $config->getProfile('staging');
}

// Profile names are case-insensitive
$profile = $config->getProfile('Production');  // matches "production"

// Access all profiles
$profiles = $config->profiles;  // array<string, ConfigProfile>

// Convert back to TOML
$tomlString = $config->toToml();

Loading Strategies

1. Load a single profile (recommended for most use cases):

// Load with automatic profile selection (TEMPORAL_PROFILE env var or 'default')
$profile = ConfigClient::load();

// Load specific profile from TOML + env overrides
$profile = ConfigClient::load(profileName: 'production');

// Load from inline TOML string
$tomlContent = <<<TOML
[profile.dev]
address = "localhost:7233"
namespace = "default"
TOML;
$profile = ConfigClient::load(
    profileName: 'dev',
    configFile: $tomlContent
);

2. Load from environment variables only:

// Returns a ConfigProfile directly from TEMPORAL_* env vars
$profile = ConfigClient::loadFromEnv();

// With custom environment array
$profile = ConfigClient::loadFromEnv([
    'TEMPORAL_ADDRESS' => 'localhost:7233',
    'TEMPORAL_NAMESPACE' => 'my-namespace',
    'TEMPORAL_API_KEY' => 'my-key',
]);

3. Load all profiles from TOML (for multi-profile scenarios):

// Load all profiles from file
$config = ConfigClient::loadFromToml('/path/to/temporal.toml');

// Access individual profiles
$devProfile = $config->getProfile('dev');
$prodProfile = $config->getProfile('production');

// Convert back to TOML format
$tomlString = $config->toToml();
file_put_contents('output.toml', $tomlString);

Remote Codec Support

Note

Remote codec configuration is not supported in the PHP SDK, following the same approach as TypeScript, Python, and .NET SDKs.

If you attempt to configure a remote codec either in the TOML file or via environment variables, the SDK will throw a CodecNotSupportedException to prevent silent failures:

# ❌ This will throw an exception
[profile.production.codec]
endpoint = "https://codec.example.com"
auth = "Bearer token"
# ❌ These will also throw an exception
export TEMPORAL_CODEC_ENDPOINT="https://codec.example.com"
export TEMPORAL_CODEC_AUTH="Bearer token"

This explicit error ensures users are aware that codec functionality is not available, rather than silently ignoring the configuration and causing unexpected behavior.

Practical Usage Examples

Example 1: Quick Start - Load and Use a Profile

use Temporal\Client\WorkflowClient;
use Temporal\Client\ScheduleClient;
use Temporal\Common\EnvConfig\ConfigClient;

// Load configuration profile (uses TEMPORAL_PROFILE env var or 'default')
$profile = ConfigClient::load();

// Convert to client options and service client
$serviceClient = $profile->toServiceClient();
$clientOptions = $profile->toClientOptions();

// Create Temporal clients
$workflowClient = WorkflowClient::create($serviceClient, $clientOptions);
$scheduleClient = ScheduleClient::create($serviceClient, $clientOptions);

Example 2: Load Specific Profile from TOML

use Temporal\Common\EnvConfig\ConfigClient;

// Load specific profile with environment variable overrides
$profile = ConfigClient::load(
    profileName: 'my_profile',
    configFile: '/path/to/temporal.toml',  // or TOML string
    env: $_ENV  // optional, defaults to getenv()
);

Example 3: Load from Environment Variables Only

use Temporal\Common\EnvConfig\ConfigClient;

// Set environment variables
$env = [
    'TEMPORAL_ADDRESS' => 'localhost:7233',
    'TEMPORAL_NAMESPACE' => 'my-namespace',
    'TEMPORAL_API_KEY' => 'my-secret-key',
    'TEMPORAL_TLS' => 'true',
];

// Load profile directly from environment
$profile = ConfigClient::loadFromEnv($env);

Example 4: Working with Multiple Profiles (TOML Export/Import)

use Temporal\Common\EnvConfig\ConfigClient;

// Load all profiles from TOML file
$config = ConfigClient::loadFromToml('/path/to/temporal.toml');

// Access specific profiles
$devProfile = $config->getProfile('development');
$prodProfile = $config->getProfile('production');

// Convert configuration back to TOML format
$tomlString = $config->toToml();

// Save to file
file_put_contents('/path/to/output.toml', $tomlString);

Example 5: Inline TOML Configuration

use Temporal\Common\EnvConfig\ConfigClient;

// Define TOML configuration inline
$tomlData = <<<'TOML'
[profile.dev]
address = "localhost:7233"
namespace = "default"

[profile.prod]
address = "prod.temporal.io:7233"
namespace = "production"
api_key = "secret-key"
TOML;

// Load specific profile from inline TOML
$profile = ConfigClient::load(
    profileName: 'dev',
    configFile: $tomlData
);

@roxblnfk roxblnfk requested a review from wolfy-j as a code owner October 28, 2025 18:18
@vercel
Copy link

vercel bot commented Oct 28, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
php Ready Ready Preview, Comment Dec 26, 2025 6:09pm

Copy link

@THardy98 THardy98 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @roxblnfk !

Thanks for opening the PR.

Took a quick look through and it looks like the public interface deviates pretty notably from other implementations, see Typescript.

We have typically offered an interface that allows you to:

  • load a client configuration (e.g. all profiles in a configuration) from TOML file and overridden by env vars if set
  • load a client configuration, but from provided TOML data and overridden by env vars if set
  • convert a client configuration back to its TOML representation (as bytes)
  • load a single profile
  • load a profile and convert it into a representation that can be immediately used by client/connection (for a quick shorthand way to load and configure your client)

I'm requesting changes such that we keep feature parity here and maintain the same interface in the PHP SDK.

I would also keep in mind the control flow, as it differs in the existing implementations as well (i.e. we only throw ProfileNotFound if a profile name was provided by the user, if not, we return an empty profile)

@roxblnfk
Copy link
Collaborator Author

@THardy98 hi. The specification didn't help me understand some mechanics. Could you clarify these questions:

Question 1:

What is the expected behavior if:

  • I override some profile settings via ENV variables
  • I set TEMPORAL_PROFILE="default"
  • When loading the configuration, I explicitly select a different profile via code

Will the ENV variable values override the options of the different profile, even though the profile is not the configured TEMPORAL_PROFILE?
In other words, is it correct to say that ENV variables override options for all profiles, not just the one specified in TEMPORAL_PROFILE?

Question 2:

If I load configuration from both ENV variables and a TOML config file, and then save the loaded
state back to TOML, will the TOML data contain the values that were overridden from ENV?

@THardy98
Copy link

THardy98 commented Nov 19, 2025

Yeah, good questions, I could've clarified this earlier.

Will the ENV variable values override the options of the different profile, even though the profile is not the configured TEMPORAL_PROFILE?

Yes, the order of precedence is:

  1. provided in code
  2. env var
  3. config file

With profile name, if you specify it in code, we will use it. Other env vars should apply on that profile.
If you do not specify a profile name, we will fallback to the TEMPORAL_PROFILE env var.
If you do not specify TEMPORAL_PROFILE we will fallback to the default profile, "default"

This TS code snippet should be a good reference (that file generally should be a good reference).

In other words, is it correct to say that ENV variables override options for all profiles, not just the one specified in TEMPORAL_PROFILE?

Yes :)
Env vars will always be applied against the profile you are loading. If you do not want env vars to apply against your profile, you can always specify the overrideEnvVars option with an empty object.

If I load configuration from both ENV variables and a TOML config file, and then save the loaded
state back to TOML, will the TOML data contain the values that were overridden from ENV?

The TOML data will contain the values of whatever configuration you provide it.
If that is a configuration from a config file + env var overrides, then that will be what the TOML serializer sees.

@roxblnfk
Copy link
Collaborator Author

roxblnfk commented Dec 5, 2025

@THardy98 after some refactoring, it looks like this:

use Temporal\Client\ScheduleClient;
use Temporal\Client\WorkflowClient;
use Temporal\Common\EnvConfig\ConfigClient;

# load a client configuration (e.g. all profiles in a configuration) from TOML file and overridden by env vars if set
$profile = ConfigClient::load(env: $env);   // ConfigProfile


# load a client configuration, but from provided TOML data and overridden by env vars if set
$profile = ConfigClient::load(profileName: 'my_profile', configFile: $pathOrData);   // ConfigProfile


# convert a client configuration back to its TOML representation (as bytes)
$tomlConfig = ConfigClient::loadFromToml($pathOrData);   // ConfigClient
$tomlData = $tomlConfig->toToml();                       // string


# load a single profile
$profile = ConfigClient::load(profileName: 'my_profile');   // ConfigProfile


# load a profile and convert it into a representation that can be immediately used by client/connection
# (for a quick shorthand way to load and configure your client)
$profile = $profile = ConfigClient::load();    // ConfigProfile
$serviceClient = $profile->toServiceClient();  // ServiceClient
$clientOptions = $profile->toClientOptions();  // ClientOptions

$workflowClient = WorkflowClient::create($serviceClient, $clientOptions);  // WorkflowClient
$scheduleClient = ScheduleClient::create($serviceClient, $clientOptions);  // ScheduleClient

Copy link

@THardy98 THardy98 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally LGTM - left a note about TLS interaction with API key

roxblnfk and others added 24 commits December 26, 2025 22:07
ConfigClient: rename `loadFromFile()` to `loadFromToml()`
… Windows in default config path resolving
@roxblnfk roxblnfk merged commit e1c66dc into master Dec 26, 2025
103 of 114 checks passed
@roxblnfk roxblnfk deleted the env-config branch December 26, 2025 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Environment Configuration

4 participants