44
55namespace Testcontainers \ContainerClient ;
66
7+ use Composer \InstalledVersions ;
78use 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
914class 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