Skip to content

Commit e51b70b

Browse files
committed
feat(envconfig): Add codec configuration support with validation and exceptions
1 parent a001a2b commit e51b70b

File tree

9 files changed

+501
-43
lines changed

9 files changed

+501
-43
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
/**
18+
* @param non-empty-string|null $endpoint Endpoint URL for the remote codec service
19+
* @param non-empty-string|null $auth Authorization header value for codec authentication
20+
*/
21+
public function __construct(
22+
public readonly ?string $endpoint = null,
23+
public readonly ?string $auth = null,
24+
) {}
25+
26+
/**
27+
* Merge this codec config with another, with the other config's values taking precedence.
28+
*
29+
* @param self $from Codec config to merge (values from this take precedence)
30+
* @return self New merged codec config
31+
*/
32+
public function mergeWith(self $from): self
33+
{
34+
return new self(
35+
endpoint: $from->endpoint ?? $this->endpoint,
36+
auth: $from->auth ?? $this->auth,
37+
);
38+
}
39+
}

src/Common/EnvConfig/Client/ConfigEnv.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@
2626
* - TEMPORAL_TLS_SERVER_CA_CERT_PATH - Path to server CA certificate file
2727
* - TEMPORAL_TLS_SERVER_CA_CERT_DATA - Server CA certificate data (PEM format)
2828
* - TEMPORAL_TLS_SERVER_NAME - Server name for TLS verification (SNI override)
29+
* - TEMPORAL_CODEC_ENDPOINT - Remote codec endpoint URL (NOT SUPPORTED - throws exception)
30+
* - TEMPORAL_CODEC_AUTH - Authorization header for remote codec (NOT SUPPORTED - throws exception)
2931
* - TEMPORAL_GRPC_META_* - gRPC metadata headers (e.g., TEMPORAL_GRPC_META_X_CUSTOM_HEADER)
3032
*
3133
* TLS Configuration Rules:
3234
* - Cannot specify both *_PATH and *_DATA variants for the same certificate (throws exception)
3335
* - *_PATH takes precedence over *_DATA if both are set (with strict validation)
3436
*
37+
* Codec Configuration:
38+
* - Remote codec configuration is NOT SUPPORTED in PHP SDK
39+
* - If TEMPORAL_CODEC_ENDPOINT or TEMPORAL_CODEC_AUTH is set, an exception will be thrown
40+
*
3541
* @link https://github.com/temporalio/proposals/blob/master/all-sdk/external-client-configuration.md#environment-variables
3642
* @internal
3743
*/
@@ -66,6 +72,7 @@ public static function fromEnvProvider(EnvProvider $env): self
6672
apiKey: $env->get('TEMPORAL_API_KEY'),
6773
tlsConfig: self::fetchTlsConfig($env),
6874
grpcMeta: self::fetchGrpcMeta($env),
75+
codecConfig: self::fetchCodecConfig($env),
6976
),
7077
$profile,
7178
$env->get('TEMPORAL_CONFIG_FILE'),
@@ -138,4 +145,27 @@ private static function fetchGrpcMeta(EnvProvider $env): array
138145

139146
return $result;
140147
}
148+
149+
/**
150+
* Fetch codec configuration from environment variables.
151+
*
152+
* Reads TEMPORAL_CODEC_ENDPOINT and TEMPORAL_CODEC_AUTH environment variables.
153+
*
154+
* @return ConfigCodec|null Codec configuration or null if no codec env vars are set
155+
*/
156+
private static function fetchCodecConfig(EnvProvider $env): ?ConfigCodec
157+
{
158+
$endpoint = $env->get('TEMPORAL_CODEC_ENDPOINT');
159+
$auth = $env->get('TEMPORAL_CODEC_AUTH');
160+
161+
// Return null if both are not set
162+
if ($endpoint === null && $auth === null) {
163+
return null;
164+
}
165+
166+
return new ConfigCodec(
167+
endpoint: $endpoint,
168+
auth: $auth,
169+
);
170+
}
141171
}

src/Common/EnvConfig/Client/ConfigProfile.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Temporal\Client\ClientOptions;
88
use Temporal\Client\GRPC\ServiceClient;
9+
use Temporal\Common\EnvConfig\Exception\CodecNotSupportedException;
910

1011
/**
1112
* Profile-level configuration for a Temporal client.
@@ -60,13 +61,17 @@ final class ConfigProfile
6061
* @param string|\Stringable|null $apiKey API key (empty string converted to null)
6162
* @param ConfigTls|null $tlsConfig TLS/mTLS configuration
6263
* @param array<non-empty-string, string|list<string>> $grpcMeta gRPC metadata headers
64+
* @param ConfigCodec|null $codecConfig Remote codec configuration (NOT SUPPORTED - will throw exception if used)
65+
*
66+
* @throws CodecNotSupportedException If codec configuration is provided (not supported in PHP SDK)
6367
*/
6468
public function __construct(
6569
?string $address,
6670
?string $namespace,
6771
null|string|\Stringable $apiKey,
6872
public readonly ?ConfigTls $tlsConfig = null,
6973
array $grpcMeta = [],
74+
public readonly ?ConfigCodec $codecConfig = null,
7075
) {
7176
// Normalize empty strings to null
7277
$this->address = $address === '' ? null : $address;
@@ -79,13 +84,16 @@ public function __construct(
7984
$meta[\strtolower($key)] = \is_array($value) ? $value : [$value];
8085
}
8186
$this->grpcMeta = $meta;
87+
88+
// Validate codec is not configured (not supported in PHP SDK)
89+
$codecConfig?->endpoint === null && $codecConfig?->auth === null or throw new CodecNotSupportedException();
8290
}
8391

8492
/**
8593
* Merge this profile with another profile, with the other profile's values taking precedence.
8694
*
8795
* Creates a new profile by combining settings from both profiles. Non-null values from the
88-
* provided config override values from this profile. TLS configurations are deeply merged.
96+
* provided config override values from this profile. TLS and codec configurations are deeply merged.
8997
* gRPC metadata arrays are merged with keys normalized to lowercase (per gRPC spec), with
9098
* the other profile's values replacing this profile's values for duplicate keys.
9199
*
@@ -100,6 +108,7 @@ public function mergeWith(self $config): self
100108
apiKey: $config->apiKey ?? $this->apiKey,
101109
tlsConfig: self::mergeTlsConfigs($this->tlsConfig, $config->tlsConfig),
102110
grpcMeta: self::mergeGrpcMeta($this->grpcMeta, $config->grpcMeta),
111+
codecConfig: self::mergeCodecConfigs($this->codecConfig, $config->codecConfig),
103112
);
104113
}
105114

@@ -195,4 +204,20 @@ private static function mergeGrpcMeta(array $to, array $from): array
195204
}
196205
return $merged;
197206
}
207+
208+
/**
209+
* Merge two codec configurations with the second taking precedence.
210+
*
211+
* @param ConfigCodec|null $to Base codec configuration
212+
* @param ConfigCodec|null $from Codec configuration to merge (takes precedence)
213+
* @return ConfigCodec|null Merged codec configuration or null if both are null
214+
*/
215+
private static function mergeCodecConfigs(?ConfigCodec $to, ?ConfigCodec $from): ?ConfigCodec
216+
{
217+
return match (true) {
218+
$to === null => $from,
219+
$from === null => $to,
220+
default => $to->mergeWith($from),
221+
};
222+
}
198223
}

src/Common/EnvConfig/Client/ConfigToml.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ private function parseProfiles(mixed $profile): array
109109
apiKey: $apiKey,
110110
tlsConfig: $tlsConfig,
111111
grpcMeta: $config['grpc_meta'] ?? [],
112+
codecConfig: isset($config['codec']) && \is_array($config['codec']) ? $this->parseCodec($config['codec']) : null,
112113
);
113114
}
114115

@@ -147,4 +148,26 @@ private function parseTls(array $tls): ?ConfigTls
147148
serverName: $tls['server_name'] ?? null,
148149
);
149150
}
151+
152+
/**
153+
* Parse codec configuration from TOML array.
154+
*
155+
* @param array $codec Codec configuration array
156+
* @return ConfigCodec|null Parsed codec configuration or null if empty
157+
*/
158+
private function parseCodec(array $codec): ?ConfigCodec
159+
{
160+
$endpoint = $codec['endpoint'] ?? null;
161+
$auth = $codec['auth'] ?? null;
162+
163+
// Return null if both fields are empty
164+
if ($endpoint === null && $auth === null) {
165+
return null;
166+
}
167+
168+
return new ConfigCodec(
169+
endpoint: $endpoint,
170+
auth: $auth,
171+
);
172+
}
150173
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Common\EnvConfig\Exception;
6+
7+
/**
8+
* Thrown when codec configuration is provided but not supported by the PHP SDK.
9+
*
10+
* Per the Temporal external client configuration specification, PHP SDK (like TypeScript, Python, and .NET)
11+
* does not support remote codec configuration. This exception is raised to prevent silent failures
12+
* when users expect codec functionality.
13+
*
14+
* @link https://github.com/temporalio/proposals/blob/master/all-sdk/external-client-configuration.md
15+
*/
16+
final class CodecNotSupportedException extends ConfigException
17+
{
18+
public function __construct()
19+
{
20+
parent::__construct(
21+
'Remote codec configuration is not supported in the PHP SDK. ' .
22+
'Please remove codec settings from your configuration file or environment variables.',
23+
);
24+
}
25+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Tests\Unit\Common\EnvConfig\Client;
6+
7+
use PHPUnit\Framework\Attributes\CoversClass;
8+
use PHPUnit\Framework\TestCase;
9+
use Temporal\Common\EnvConfig\Client\ConfigCodec;
10+
11+
#[CoversClass(ConfigCodec::class)]
12+
final class ConfigCodecTest extends TestCase
13+
{
14+
public function testConstructorInitializesProperties(): void
15+
{
16+
// Arrange & Act
17+
$codec = new ConfigCodec(
18+
endpoint: 'https://codec.example.com',
19+
auth: 'Bearer token123',
20+
);
21+
22+
// Assert
23+
self::assertSame('https://codec.example.com', $codec->endpoint);
24+
self::assertSame('Bearer token123', $codec->auth);
25+
}
26+
27+
public function testConstructorWithNullValues(): void
28+
{
29+
// Arrange & Act
30+
$codec = new ConfigCodec();
31+
32+
// Assert
33+
self::assertNull($codec->endpoint);
34+
self::assertNull($codec->auth);
35+
}
36+
37+
public function testMergeWithOverridesEndpoint(): void
38+
{
39+
// Arrange
40+
$base = new ConfigCodec(
41+
endpoint: 'https://old.example.com',
42+
auth: 'Bearer old-token',
43+
);
44+
$override = new ConfigCodec(
45+
endpoint: 'https://new.example.com',
46+
auth: null,
47+
);
48+
49+
// Act
50+
$merged = $base->mergeWith($override);
51+
52+
// Assert
53+
self::assertSame('https://new.example.com', $merged->endpoint);
54+
self::assertSame('Bearer old-token', $merged->auth);
55+
}
56+
57+
public function testMergeWithOverridesAuth(): void
58+
{
59+
// Arrange
60+
$base = new ConfigCodec(
61+
endpoint: 'https://codec.example.com',
62+
auth: 'Bearer old-token',
63+
);
64+
$override = new ConfigCodec(
65+
endpoint: null,
66+
auth: 'Bearer new-token',
67+
);
68+
69+
// Act
70+
$merged = $base->mergeWith($override);
71+
72+
// Assert
73+
self::assertSame('https://codec.example.com', $merged->endpoint);
74+
self::assertSame('Bearer new-token', $merged->auth);
75+
}
76+
77+
public function testMergeWithOverridesBothValues(): void
78+
{
79+
// Arrange
80+
$base = new ConfigCodec(
81+
endpoint: 'https://old.example.com',
82+
auth: 'Bearer old-token',
83+
);
84+
$override = new ConfigCodec(
85+
endpoint: 'https://new.example.com',
86+
auth: 'Bearer new-token',
87+
);
88+
89+
// Act
90+
$merged = $base->mergeWith($override);
91+
92+
// Assert
93+
self::assertSame('https://new.example.com', $merged->endpoint);
94+
self::assertSame('Bearer new-token', $merged->auth);
95+
}
96+
97+
public function testMergeWithNullOverrideKeepsBaseValues(): void
98+
{
99+
// Arrange
100+
$base = new ConfigCodec(
101+
endpoint: 'https://codec.example.com',
102+
auth: 'Bearer token123',
103+
);
104+
$override = new ConfigCodec();
105+
106+
// Act
107+
$merged = $base->mergeWith($override);
108+
109+
// Assert
110+
self::assertSame('https://codec.example.com', $merged->endpoint);
111+
self::assertSame('Bearer token123', $merged->auth);
112+
}
113+
114+
public function testPropertiesAreReadonly(): void
115+
{
116+
// Arrange
117+
$codec = new ConfigCodec(
118+
endpoint: 'https://codec.example.com',
119+
auth: 'Bearer token123',
120+
);
121+
122+
// Assert
123+
$reflection = new \ReflectionClass($codec);
124+
$endpointProperty = $reflection->getProperty('endpoint');
125+
$authProperty = $reflection->getProperty('auth');
126+
127+
self::assertTrue($endpointProperty->isReadOnly());
128+
self::assertTrue($authProperty->isReadOnly());
129+
}
130+
}

0 commit comments

Comments
 (0)