Skip to content

Commit e1c66dc

Browse files
authored
Merge pull request #661: Expose Environment Configuration
2 parents c7cdd6b + 7ee0064 commit e1c66dc

19 files changed

+4547
-2
lines changed

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"dereuromark/composer-prefer-lowest": "^0.1.10",
6262
"doctrine/annotations": "^1.14.4 || ^2.0.2",
6363
"internal/dload": "^1.2.0",
64+
"internal/toml": "^1.0.3",
6465
"jetbrains/phpstorm-attributes": "dev-master",
6566
"laminas/laminas-code": "^4.16",
6667
"phpunit/phpunit": "10.5.45",
@@ -84,7 +85,8 @@
8485
"ext-grpc": "For Client calls",
8586
"ext-protobuf": "For better performance",
8687
"buggregator/trap": "For better debugging",
87-
"roadrunner/psr-logger": "RoadRunner PSR-3 logger integration"
88+
"roadrunner/psr-logger": "RoadRunner PSR-3 logger integration",
89+
"internal/toml": "To load TOML config files"
8890
},
8991
"scripts": {
9092
"get:binaries": [
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Common\EnvConfig\Client;
6+
7+
/**
8+
* Remote codec configuration.
9+
*
10+
* Specifies endpoint and authentication for remote data encoding/decoding.
11+
* Remote codecs allow offloading payload encoding/decoding to an external service.
12+
*
13+
* @internal
14+
*/
15+
final class ConfigCodec
16+
{
17+
/** @var non-empty-string|null $endpoint Endpoint URL for the remote codec service */
18+
public readonly ?string $endpoint;
19+
20+
/** @var non-empty-string|null $auth Authorization header value for codec authentication */
21+
public readonly ?string $auth;
22+
23+
/**
24+
* @param string|null $endpoint Endpoint URL for the remote codec service
25+
* @param string|null $auth Authorization header value for codec authentication
26+
*/
27+
public function __construct(
28+
?string $endpoint = null,
29+
?string $auth = null,
30+
) {
31+
$this->auth = $auth === '' ? null : $auth;
32+
$this->endpoint = $endpoint === '' ? null : $endpoint;
33+
}
34+
35+
/**
36+
* Merge this codec config with another, with the other config's values taking precedence.
37+
*
38+
* @param self $from Codec config to merge (values from this take precedence)
39+
* @return self New merged codec config
40+
*/
41+
public function mergeWith(self $from): self
42+
{
43+
return new self(
44+
endpoint: $from->endpoint ?? $this->endpoint,
45+
auth: $from->auth ?? $this->auth,
46+
);
47+
}
48+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Common\EnvConfig\Client;
6+
7+
/**
8+
* Environment variable configuration parser for Temporal client.
9+
*
10+
* Reads Temporal client configuration from environment variables following
11+
* the naming convention: TEMPORAL_* (e.g., TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE).
12+
*
13+
* Supported environment variables:
14+
* - TEMPORAL_ADDRESS - Temporal server address (host:port)
15+
* - TEMPORAL_NAMESPACE - Temporal namespace
16+
* - TEMPORAL_API_KEY - API key for authentication
17+
* - TEMPORAL_PROFILE - Active profile name
18+
* - TEMPORAL_CONFIG_FILE - Path to TOML configuration file
19+
* - TEMPORAL_TLS - Enable/disable TLS (boolean: true/false, 1/0, yes/no, on/off)
20+
* - TEMPORAL_TLS_CLIENT_CERT_PATH - Path to client certificate file
21+
* - TEMPORAL_TLS_CLIENT_CERT_DATA - Client certificate data (PEM format)
22+
* - TEMPORAL_TLS_CLIENT_KEY_PATH - Path to client private key file
23+
* - TEMPORAL_TLS_CLIENT_KEY_DATA - Client private key data (PEM format)
24+
* - TEMPORAL_TLS_SERVER_CA_CERT_PATH - Path to server CA certificate file
25+
* - TEMPORAL_TLS_SERVER_CA_CERT_DATA - Server CA certificate data (PEM format)
26+
* - TEMPORAL_TLS_SERVER_NAME - Server name for TLS verification (SNI override)
27+
* - TEMPORAL_CODEC_ENDPOINT - Remote codec endpoint URL (NOT SUPPORTED - throws exception)
28+
* - TEMPORAL_CODEC_AUTH - Authorization header for remote codec (NOT SUPPORTED - throws exception)
29+
* - TEMPORAL_GRPC_META_* - gRPC metadata headers (e.g., TEMPORAL_GRPC_META_X_CUSTOM_HEADER)
30+
*
31+
* TLS Configuration Rules:
32+
* - Cannot specify both *_PATH and *_DATA variants for the same certificate (throws exception)
33+
* - *_PATH takes precedence over *_DATA if both are set (with strict validation)
34+
*
35+
* Codec Configuration:
36+
* - Remote codec configuration is NOT SUPPORTED in PHP SDK
37+
* - If TEMPORAL_CODEC_ENDPOINT or TEMPORAL_CODEC_AUTH is set, an exception will be thrown
38+
*
39+
* @link https://github.com/temporalio/proposals/blob/master/all-sdk/external-client-configuration.md#environment-variables
40+
* @internal
41+
*/
42+
final class ConfigEnv
43+
{
44+
/**
45+
* Current active profile name from TEMPORAL_PROFILE
46+
* @var non-empty-lowercase-string|null
47+
*/
48+
public readonly ?string $currentProfile;
49+
50+
/**
51+
* Path to TOML configuration file from TEMPORAL_CONFIG_FILE
52+
* @var non-empty-string|null
53+
*/
54+
public readonly ?string $configFile;
55+
56+
/**
57+
* @param ConfigProfile $profile Profile constructed from environment variables
58+
* @param string|null $currentProfile Current active profile name
59+
* @param string|null $configFile Path to TOML configuration file
60+
*/
61+
private function __construct(
62+
/**
63+
* Profile constructed from environment variables
64+
*/
65+
public readonly ConfigProfile $profile,
66+
?string $currentProfile = null,
67+
?string $configFile = null,
68+
) {
69+
$this->currentProfile = $currentProfile === '' || $currentProfile === null
70+
? null
71+
: \strtolower($currentProfile);
72+
$this->configFile = $configFile === '' ? null : $configFile;
73+
}
74+
75+
public static function fromEnv(array $env): self
76+
{
77+
return new self(
78+
new ConfigProfile(
79+
address: $env['TEMPORAL_ADDRESS'] ?? null,
80+
namespace: $env['TEMPORAL_NAMESPACE'] ?? null,
81+
apiKey: $env['TEMPORAL_API_KEY'] ?? null,
82+
tlsConfig: self::fetchTlsConfig($env),
83+
grpcMeta: self::fetchGrpcMeta($env),
84+
codecConfig: self::fetchCodecConfig($env),
85+
),
86+
$env['TEMPORAL_PROFILE'] ?? null,
87+
$env['TEMPORAL_CONFIG_FILE'] ?? null,
88+
);
89+
}
90+
91+
private static function fetchTlsConfig(array $env): ?ConfigTls
92+
{
93+
$tls = $env['TEMPORAL_TLS'] ?? null;
94+
$tlsVars = self::getByPrefix($env, 'TEMPORAL_TLS_', stripPrefix: true);
95+
96+
// If no TLS-related variables are set, return null
97+
if ($tls === null && $tlsVars === []) {
98+
return null;
99+
}
100+
101+
// Parse TEMPORAL_TLS as boolean
102+
$disabled = null;
103+
if ($tls !== null) {
104+
$tlsEnabled = \filter_var($tls, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE);
105+
$disabled = $tlsEnabled === null ? null : !$tlsEnabled;
106+
}
107+
108+
// Check for conflicts: *_PATH and *_DATA cannot be used together
109+
isset($tlsVars['SERVER_CA_CERT_PATH'], $tlsVars['SERVER_CA_CERT_DATA']) and throw new \InvalidArgumentException(
110+
'Cannot specify both TEMPORAL_TLS_SERVER_CA_CERT_PATH and TEMPORAL_TLS_SERVER_CA_CERT_DATA.',
111+
);
112+
isset($tlsVars['CLIENT_KEY_PATH'], $tlsVars['CLIENT_KEY_DATA']) and throw new \InvalidArgumentException(
113+
'Cannot specify both TEMPORAL_TLS_CLIENT_KEY_PATH and TEMPORAL_TLS_CLIENT_KEY_DATA.',
114+
);
115+
isset($tlsVars['CLIENT_CERT_PATH'], $tlsVars['CLIENT_CERT_DATA']) and throw new \InvalidArgumentException(
116+
'Cannot specify both TEMPORAL_TLS_CLIENT_CERT_PATH and TEMPORAL_TLS_CLIENT_CERT_DATA.',
117+
);
118+
119+
// Priority: *_PATH over *_DATA (same as ConfigToml)
120+
return new ConfigTls(
121+
disabled: $disabled,
122+
rootCerts: $tlsVars['SERVER_CA_CERT_PATH'] ?? $tlsVars['SERVER_CA_CERT_DATA'] ?? null,
123+
privateKey: $tlsVars['CLIENT_KEY_PATH'] ?? $tlsVars['CLIENT_KEY_DATA'] ?? null,
124+
certChain: $tlsVars['CLIENT_CERT_PATH'] ?? $tlsVars['CLIENT_CERT_DATA'] ?? null,
125+
serverName: $tlsVars['SERVER_NAME'] ?? null,
126+
);
127+
}
128+
129+
/**
130+
* Fetch gRPC metadata from environment variables.
131+
*
132+
* Reads all environment variables with prefix TEMPORAL_GRPC_META_
133+
* and converts them to gRPC metadata headers.
134+
*
135+
* Header names are transformed:
136+
* - Converted to lowercase
137+
* - Underscores (_) are replaced with hyphens (-)
138+
*
139+
* Example: TEMPORAL_GRPC_META_X_CUSTOM_HEADER=value
140+
* Results in: ['x-custom-header' => 'value']
141+
*
142+
* @return array<non-empty-string, string>
143+
*/
144+
private static function fetchGrpcMeta(array $env): array
145+
{
146+
$meta = self::getByPrefix($env, 'TEMPORAL_GRPC_META_', stripPrefix: true);
147+
$result = [];
148+
149+
foreach ($meta as $key => $value) {
150+
// Transform header name: lowercase and replace _ with -
151+
/** @var non-empty-string $headerName */
152+
$headerName = \str_replace('_', '-', $key);
153+
$result[$headerName] = $value;
154+
}
155+
156+
return $result;
157+
}
158+
159+
/**
160+
* Fetch codec configuration from environment variables.
161+
*
162+
* Reads TEMPORAL_CODEC_ENDPOINT and TEMPORAL_CODEC_AUTH environment variables.
163+
*
164+
* @return ConfigCodec|null Codec configuration or null if no codec env vars are set
165+
*/
166+
private static function fetchCodecConfig(array $env): ?ConfigCodec
167+
{
168+
$endpoint = $env['TEMPORAL_CODEC_ENDPOINT'] ?? null;
169+
$auth = $env['TEMPORAL_CODEC_AUTH'] ?? null;
170+
171+
// Return null if both are not set
172+
if ($endpoint === null && $auth === null) {
173+
return null;
174+
}
175+
176+
return new ConfigCodec(
177+
endpoint: $endpoint,
178+
auth: $auth,
179+
);
180+
}
181+
182+
/**
183+
* Get environment variables by prefix.
184+
*
185+
* @param array $env Environment variables array
186+
* @param string $prefix Prefix to filter by
187+
* @param bool $stripPrefix Whether to strip the prefix from result keys
188+
* @return array<string, string>
189+
*/
190+
private static function getByPrefix(array $env, string $prefix, bool $stripPrefix = false): array
191+
{
192+
$result = [];
193+
$prefixLen = \strlen($prefix);
194+
195+
foreach ($env as $key => $value) {
196+
if (\str_starts_with($key, $prefix)) {
197+
$resultKey = $stripPrefix ? \substr($key, $prefixLen) : $key;
198+
$result[$resultKey] = $value;
199+
}
200+
}
201+
202+
return $result;
203+
}
204+
}

0 commit comments

Comments
 (0)