Skip to content

Commit 75bca05

Browse files
authored
Merge pull request #69 from testcontainers/feat/user-agent-header
feat: set tc-php/<version> User-Agent header for Docker API requests
2 parents b376be3 + d2f2691 commit 75bca05

3 files changed

Lines changed: 391 additions & 2 deletions

File tree

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"require": {
1717
"ext-curl": "*",
1818
"php": ">= 8.1",
19-
"beluga-php/docker-php": "^1.45"
19+
"beluga-php/docker-php": "^1.45",
20+
"php-http/client-common": "^2.7"
2021
},
2122
"require-dev": {
2223
"ext-pdo": "*",

src/ContainerClient/DockerContainerClient.php

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
namespace Testcontainers\ContainerClient;
66

7+
use Composer\InstalledVersions;
78
use Docker\Docker as DockerClient;
9+
use Docker\DockerClientFactory;
10+
use Http\Client\Common\Plugin\HeaderDefaultsPlugin;
11+
use Http\Client\Common\PluginClient;
12+
use Psr\Http\Client\ClientInterface;
813

914
class DockerContainerClient
1015
{
@@ -13,6 +18,18 @@ class DockerContainerClient
1318
*/
1419
private static ?DockerClient $dockerClient = null;
1520

21+
/**
22+
* @var (callable(): ClientInterface)|null Factory for the base HTTP client.
23+
* When null, DockerClientFactory::createFromEnv() is used.
24+
*/
25+
private static $httpClientFactory = null;
26+
27+
/**
28+
* @var (callable(ClientInterface): DockerClient)|null Factory for the Docker client.
29+
* When null, DockerClient::create() is used.
30+
*/
31+
private static $dockerClientFactory = null;
32+
1633
private function __construct()
1734
{
1835
}
@@ -26,19 +43,93 @@ private function __construct()
2643
public static function getDockerClient(): DockerClient
2744
{
2845
if (self::$dockerClient === null) {
29-
self::$dockerClient = DockerClient::create();
46+
$version = static::resolveVersion();
47+
48+
$baseHttpClient = self::createHttpClient();
49+
50+
$httpClient = new PluginClient(
51+
$baseHttpClient,
52+
[new HeaderDefaultsPlugin(['User-Agent' => 'tc-php/' . $version])]
53+
);
54+
55+
self::$dockerClient = self::createDockerClient($httpClient);
3056
}
3157

3258
return self::$dockerClient;
3359
}
3460

61+
/**
62+
* Resolves the package version string used in the User-Agent header.
63+
*
64+
* Returns the pretty version of the installed package, with the
65+
* '+no-version-set' build-metadata suffix stripped. Falls back to
66+
* 'unknown' if the package is not found in the Composer runtime data.
67+
*
68+
* @param string $package Composer package name to resolve; override in tests
69+
* to exercise the OutOfBoundsException fallback path.
70+
*/
71+
protected static function resolveVersion(string $package = 'testcontainers/testcontainers'): string
72+
{
73+
try {
74+
$version = InstalledVersions::getPrettyVersion($package) ?? 'unknown';
75+
} catch (\OutOfBoundsException) {
76+
$version = 'unknown';
77+
}
78+
79+
return str_replace('+no-version-set', '', $version);
80+
}
81+
82+
/**
83+
* Returns the base HTTP client, using the injected factory if set.
84+
*/
85+
private static function createHttpClient(): ClientInterface
86+
{
87+
return self::$httpClientFactory !== null
88+
? (self::$httpClientFactory)()
89+
: DockerClientFactory::createFromEnv();
90+
}
91+
92+
/**
93+
* Builds the DockerClient from the given HTTP client, using the injected factory if set.
94+
*/
95+
private static function createDockerClient(ClientInterface $httpClient): DockerClient
96+
{
97+
return self::$dockerClientFactory !== null
98+
? (self::$dockerClientFactory)($httpClient)
99+
: DockerClient::create($httpClient);
100+
}
101+
35102
/**
36103
* Injects a DockerClient instance for testing or special use cases.
104+
* Note: clients injected via this method will not have the tc-php User-Agent header applied automatically.
37105
*
38106
* @param DockerClient $client The DockerClient instance to set.
39107
*/
40108
public static function setDockerClient(DockerClient $client): void
41109
{
42110
self::$dockerClient = $client;
43111
}
112+
113+
/**
114+
* Resets the injectable factories to their defaults.
115+
* For use in tests only — do not call in production code.
116+
*
117+
* @param (callable(): ClientInterface)|null $httpClientFactory
118+
* @param (callable(ClientInterface): DockerClient)|null $dockerClientFactory
119+
*/
120+
public static function setFactories(?callable $httpClientFactory, ?callable $dockerClientFactory): void
121+
{
122+
self::$httpClientFactory = $httpClientFactory;
123+
self::$dockerClientFactory = $dockerClientFactory;
124+
}
125+
126+
/**
127+
* Resets the injectable factories to their defaults (both null).
128+
* For use in tests only — do not call in production code.
129+
*/
130+
public static function resetFactories(): void
131+
{
132+
self::$httpClientFactory = null;
133+
self::$dockerClientFactory = null;
134+
}
44135
}

0 commit comments

Comments
 (0)