GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat!: support Docker Engine API v1.45 to v1.56 · docker-php/docker-php@f2148a9

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • .github/workflows
    • continuous-integration.yml
  • composer.json
  • src
    • Context
      • Context.php
    • Docker.php
    • DockerClientFactory.php
    • Endpoint
      • ContainerStats.php
      • SystemEvents.php
    • Exception
      • UnexpectedStatusCodeException.php
    • Http
      • ApiVersionNegotiationPlugin.php
    • Serializer
      • LargeIntegerJsonDecode.php
    • Stream
      • FrameError.php
      • StatsStream.php
  • tests
    • Context
      • ContextTest.php
    • DockerClientFactoryTest.php
    • DockerClientFactoryTransportTest.php
    • DocumentationExamplesTest.php
    • Endpoint
      • ContainerStatsTest.php
    • GeneratedNormalizerTest.php
    • GuzzleClientTest.php
    • HttpClientDocumentationTest.php
    • Http
      • ApiVersionNegotiationPluginTest.php
    • Resource
      • LiveStreamTest.php
    • Serializer
      • LargeIntegerJsonDecodeTest.php
    • StaticAnalysis
      • DockerReturnTypes.php
    • UnexpectedStatusTest.php
    • fixtures
      • socket-server.php
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎.github/workflows/continuous-integration.yml‎

Copy file name to clipboardExpand all lines: .github/workflows/continuous-integration.yml
+59Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,62 @@ jobs:
5353

5454
- name: Run PHPUnit test suite
5555
run: composer run-script test-ci
56+
57+
api-versions:
58+
name: API ${{ matrix.api-line }} (Docker ${{ matrix.docker }})
59+
60+
runs-on: ubuntu-latest
61+
62+
strategy:
63+
fail-fast: false
64+
matrix:
65+
api-line: ["7.1.45", "7.1.46", "7.1.47", "7.1.48", "7.1.49", "7.1.50", "7.1.51", "7.1.52", "7.1.53", "7.1.54", "7.1.55", "7.1.56"]
66+
# The runner's daemon is older than the newest lines, so those negotiate down.
67+
docker: [runner]
68+
include:
69+
# Run the newest line against a daemon that supports it without negotiation.
70+
- api-line: "7.1.56"
71+
docker: "29"
72+
73+
services:
74+
dind:
75+
image: ${{ matrix.docker == '29' && 'docker:29-dind' || '' }}
76+
options: --privileged
77+
env:
78+
DOCKER_TLS_CERTDIR: ""
79+
ports:
80+
- 2375:2375
81+
82+
env:
83+
DOCKER_HOST: ${{ matrix.docker == '29' && 'tcp://127.0.0.1:2375' || '' }}
84+
85+
steps:
86+
- name: Repository checkout
87+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
88+
89+
- name: Setup PHP with PECL extension
90+
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2
91+
with:
92+
php-version: "8.5"
93+
extensions: curl, openssl
94+
tools: composer:v2
95+
coverage: none
96+
97+
- name: Select the API line
98+
run: composer require --no-update "docker-php/docker-php-api:>=${{ matrix.api-line }}.0 <${{ matrix.api-line }}.99999"
99+
100+
- name: Install dependencies
101+
uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0
102+
with:
103+
dependency-versions: highest
104+
105+
- name: Wait for the daemon
106+
run: |
107+
for attempt in $(seq 1 30); do docker version && break; sleep 2; done
108+
docker version --format 'Daemon API {{.Server.APIVersion}}'
109+
110+
- name: Pull the docker image used by the tests.
111+
run: docker pull busybox:latest
112+
113+
- name: Run PHPUnit test suite
114+
run: composer run-script test-ci
Collapse file

‎composer.json‎

Copy file name to clipboardExpand all lines: composer.json
+1-2Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111
],
1212
"require": {
1313
"php": ">=8.1",
14-
"docker-php/docker-php-api": ">=7.1.45.0 <7.1.46.0",
14+
"docker-php/docker-php-api": ">=7.1.45.0 <7.1.57.0",
1515
"nyholm/psr7": "^1.8",
1616
"php-http/client-common": "^2.7",
1717
"php-http/discovery": "^1.19",
1818
"php-http/socket-client": "^2.3",
1919
"psr/http-message": "^2.0",
20-
"symfony/deprecation-contracts": "^2.5 || ^3.0",
2120
"symfony/filesystem": "^6.3 || ^7.0 || ^8.0",
2221
"symfony/process": "^6.3 || ^7.0 || ^8.0",
2322
"symfony/serializer": "^6.3 || ^7.0 || ^8.0"
Collapse file

‎src/Context/Context.php‎

Copy file name to clipboardExpand all lines: src/Context/Context.php
+5-16Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class Context implements ContextInterface
4747
*/
4848
private $format = self::FORMAT_STREAM;
4949

50-
private bool $applyDockerignore = false;
50+
private bool $applyDockerignore = true;
5151

5252
/** Temporary NUL-separated list of the paths to archive. */
5353
private ?string $pathList = null;
@@ -95,10 +95,10 @@ public function getDockerfileContent()
9595
}
9696

9797
/**
98-
* Leave out paths matched by the context's .dockerignore file, as `docker build` does.
98+
* Whether to leave out paths matched by the context's .dockerignore file,
99+
* as `docker build` does (the default).
99100
*
100-
* Without this, the whole directory is archived and a .dockerignore file
101-
* triggers a deprecation notice. Applying it becomes the default in 4.0.
101+
* Pass false to archive the whole directory, as 3.x did by default.
102102
*/
103103
public function applyDockerignore(bool $enabled = true): static
104104
{
@@ -182,18 +182,7 @@ public function __destruct()
182182
private function tarCommand(): array
183183
{
184184
$dockerignore = $this->directory.\DIRECTORY_SEPARATOR.'.dockerignore';
185-
if (!is_file($dockerignore)) {
186-
return ['/usr/bin/env', 'tar', '-c', '.'];
187-
}
188-
189-
if (!$this->applyDockerignore) {
190-
trigger_deprecation(
191-
'docker-php/docker-php',
192-
'3.3',
193-
'The build context %s has a .dockerignore file that is not applied; from 4.0 it will be. Call Context::applyDockerignore() to opt in now.',
194-
$this->directory
195-
);
196-
185+
if (!$this->applyDockerignore || !is_file($dockerignore)) {
197186
return ['/usr/bin/env', 'tar', '-c', '.'];
198187
}
199188

Collapse file

‎src/Docker.php‎

Copy file name to clipboardExpand all lines: src/Docker.php
+23-20Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
use Docker\Endpoint\ImagePush;
1919
use Docker\Endpoint\InteractiveExecStart;
2020
use Docker\Endpoint\SystemEvents;
21+
use Docker\API\Normalizer\JaneObjectNormalizer;
2122
use Docker\API\Runtime\Client\Endpoint;
23+
use Docker\API\Runtime\Client\FormEncoder;
2224
use Docker\Exception\BadRequestException;
2325
use Docker\Exception\UnexpectedStatusCodeException;
2426
use Docker\Http\InteractiveHttpClient;
27+
use Docker\Serializer\LargeIntegerJsonDecode;
2528
use Docker\Stream\AttachWebsocketStream;
2629
use Docker\Stream\BuildStream;
2730
use Docker\Stream\CreateImageStream;
@@ -34,6 +37,10 @@
3437
use Http\Client\Socket\Stream as SocketStream;
3538
use Nyholm\Psr7\Stream;
3639
use Psr\Http\Message\ResponseInterface;
40+
use Symfony\Component\Serializer\Encoder\JsonEncode;
41+
use Symfony\Component\Serializer\Encoder\JsonEncoder;
42+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
43+
use Symfony\Component\Serializer\Serializer;
3744

3845
/**
3946
* Docker\Docker.
@@ -42,14 +49,13 @@ class Docker extends Client
4249
{
4350
private bool $interactiveExecEnabled = false;
4451

45-
private bool $throwOnUnexpectedStatus = false;
52+
private bool $throwOnUnexpectedStatus = true;
4653

4754
/**
48-
* Throw an UnexpectedStatusCodeException when Docker returns an error
49-
* status that the generated endpoint does not handle.
55+
* Whether an error status that the generated endpoint does not handle
56+
* throws an UnexpectedStatusCodeException (the default).
5057
*
51-
* Without this, such calls return null as in earlier releases and trigger
52-
* a deprecation notice. Throwing becomes the default in 4.0.
58+
* Pass false to return null for those responses, as 3.x did by default.
5359
*/
5460
public function throwOnUnexpectedStatus(bool $enabled = true): static
5561
{
@@ -77,19 +83,10 @@ public function executeEndpoint(Endpoint $endpoint, string $fetch = self::FETCH_
7783
return $result;
7884
}
7985

80-
$exception = UnexpectedStatusCodeException::fromResponse($endpoint->getMethod(), $endpoint->getUri(), $response);
8186
if ($this->throwOnUnexpectedStatus) {
82-
throw $exception;
87+
throw UnexpectedStatusCodeException::fromResponse($endpoint->getMethod(), $endpoint->getUri(), $response);
8388
}
8489

85-
trigger_deprecation(
86-
'docker-php/docker-php',
87-
'3.3',
88-
'%s The call returned null; from 4.0 it will throw %s. Call Docker::throwOnUnexpectedStatus() to opt in now.',
89-
$exception->getMessage(),
90-
$exception::class
91-
);
92-
9390
return null;
9491
}
9592

@@ -137,10 +134,11 @@ public function containerLogs(string $id, array $queryParameters = [], string $f
137134
* {@inheritdoc}
138135
*
139136
* With `stream => true` (the default) the result is a StatsStream that
140-
* delivers one sample per frame; with `stream => false` it is one decoded
141-
* sample.
137+
* delivers one sample per frame; with `stream => false` it is one sample.
138+
* Samples are ContainerStatsResponse models from API 1.48, and decoded
139+
* \stdClass objects for earlier API versions.
142140
*
143-
* @return ($fetch is 'object' ? StatsStream|\stdClass|null : ResponseInterface)
141+
* @return ($fetch is 'object' ? StatsStream|object|null : ResponseInterface)
144142
*/
145143
public function containerStats(string $id, array $queryParameters = [], string $fetch = self::FETCH_OBJECT)
146144
{
@@ -270,9 +268,10 @@ public function imagePush(string $name, array $queryParameters = [], array $head
270268
*
271269
* @return ($fetch is 'object' ? EventStream|null : ResponseInterface)
272270
*/
273-
public function systemEvents(array $queryParameters = [], string $fetch = self::FETCH_OBJECT)
271+
public function systemEvents(array $queryParameters = [], string $fetch = self::FETCH_OBJECT, array $accept = [])
274272
{
275-
return $this->executeEndpoint(new SystemEvents($queryParameters), $fetch);
273+
// API 1.52 and later accept an Accept header; earlier endpoints ignore it.
274+
return $this->executeEndpoint(new SystemEvents($queryParameters, $accept), $fetch);
276275
}
277276

278277
public static function create(
@@ -287,6 +286,10 @@ public static function create(
287286
}
288287

289288
$client = parent::create($httpClient, $additionalPlugins, $additionalNormalizers, $applyServerPlugins);
289+
$client->serializer = new Serializer(
290+
[new ArrayDenormalizer(), new JaneObjectNormalizer(), ...$additionalNormalizers],
291+
[new JsonEncoder(new JsonEncode(), new LargeIntegerJsonDecode(['json_decode_associative' => true])), new FormEncoder()]
292+
);
290293
$client->interactiveExecEnabled = $httpClient instanceof InteractiveHttpClient;
291294
$response = $client->executeRawEndpoint(new SystemInfo());
292295
$testClient = $response->getBody()->getContents();
Collapse file

‎src/DockerClientFactory.php‎

Copy file name to clipboardExpand all lines: src/DockerClientFactory.php
+57-7Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Docker;
66

7+
use Docker\API\Client as GeneratedClient;
8+
use Docker\Http\ApiVersionNegotiationPlugin;
79
use Docker\Http\InteractiveHttpClient;
810
use Docker\Http\StreamingDecoderPlugin;
911
use Http\Client\Common\Plugin\AddHostPlugin;
@@ -14,12 +16,55 @@
1416
use Http\Client\Common\PluginClientFactory;
1517
use Http\Client\Socket\Client;
1618
use Http\Discovery\Psr17FactoryDiscovery;
19+
use Psr\Http\Client\ClientInterface;
20+
use Psr\Http\Message\RequestInterface;
21+
use Psr\Http\Message\ResponseInterface;
1722

1823
final class DockerClientFactory
1924
{
25+
private static ?string $defaultApiVersion = null;
26+
27+
/**
28+
* The Docker API version of the installed docker-php/docker-php-api package,
29+
* for example "1.56". Requests use it unless api_version or DOCKER_API_VERSION
30+
* says otherwise.
31+
*/
32+
public static function defaultApiVersion(): string
33+
{
34+
if (null !== self::$defaultApiVersion) {
35+
return self::$defaultApiVersion;
36+
}
37+
38+
// The generated client adds its specification's base path to requests.
39+
// Capture one request instead of trusting Composer metadata, so the
40+
// version always matches the classes that are actually loaded.
41+
$recorder = new class implements ClientInterface {
42+
public ?RequestInterface $request = null;
43+
44+
public function sendRequest(RequestInterface $request): ResponseInterface
45+
{
46+
$this->request = $request;
47+
48+
return Psr17FactoryDiscovery::findResponseFactory()->createResponse(204);
49+
}
50+
};
51+
$client = GeneratedClient::create($recorder);
52+
$httpClient = (fn (): ClientInterface => $this->httpClient)->call($client);
53+
$httpClient->sendRequest(Psr17FactoryDiscovery::findRequestFactory()->createRequest('GET', '/'));
54+
55+
$path = (string) $recorder->request?->getUri()->getPath();
56+
if (!preg_match('#^/v([0-9]+\.[0-9]+)/#', $path, $matches)) {
57+
throw new \LogicException('Cannot determine the Docker API version of the installed docker-php/docker-php-api package.');
58+
}
59+
60+
return self::$defaultApiVersion = $matches[1];
61+
}
62+
2063
/**
21-
* The api_version option overrides DOCKER_API_VERSION; other options are
22-
* passed to the socket client. Neither setting changes the generated models.
64+
* The api_version option overrides DOCKER_API_VERSION. Without either, the
65+
* client negotiates: it uses the installed API package's version, or the
66+
* daemon's maximum if that is lower. Other options are passed to the socket
67+
* client. No setting changes the generated models.
2368
*/
2469
public static function create(array $config = [], ?PluginClientFactory $pluginClientFactory = null): PluginClient
2570
{
@@ -30,11 +75,14 @@ public static function create(array $config = [], ?PluginClientFactory $pluginCl
3075
$dockerApiVersion = $config['api_version'];
3176
unset($config['api_version']);
3277
} else {
33-
$dockerApiVersion = getenv('DOCKER_API_VERSION') ?: 'v1.45';
78+
// Null negotiates with the daemon, up to the installed API package's version.
79+
$dockerApiVersion = getenv('DOCKER_API_VERSION') ?: null;
3480
}
35-
$dockerApiVersion = ltrim($dockerApiVersion, '/');
36-
if (!str_starts_with($dockerApiVersion, 'v')) {
37-
$dockerApiVersion = 'v'.$dockerApiVersion;
81+
if (null !== $dockerApiVersion) {
82+
$dockerApiVersion = ltrim($dockerApiVersion, '/');
83+
if (!str_starts_with($dockerApiVersion, 'v')) {
84+
$dockerApiVersion = 'v'.$dockerApiVersion;
85+
}
3886
}
3987

4088
if (!\array_key_exists('remote_socket', $config)) {
@@ -69,7 +117,9 @@ public static function create(array $config = [], ?PluginClientFactory $pluginCl
69117
[
70118
new ContentLengthPlugin(),
71119
new StreamingDecoderPlugin(),
72-
new AddPathPlugin($uriFactory->createUri('/'.$dockerApiVersion)),
120+
null === $dockerApiVersion
121+
? new ApiVersionNegotiationPlugin(self::defaultApiVersion(), $uriFactory, Psr17FactoryDiscovery::findRequestFactory())
122+
: new AddPathPlugin($uriFactory->createUri('/'.$dockerApiVersion)),
73123
new AddHostPlugin($host),
74124
new HeaderDefaultsPlugin([
75125
'host' => $host->withUserInfo('')->getAuthority(),
Collapse file

‎src/Endpoint/ContainerStats.php‎

Copy file name to clipboardExpand all lines: src/Endpoint/ContainerStats.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ protected function transformResponseBody(ResponseInterface $response, Serializer
1616
// Both modes use application/json, so the request decides: `stream`
1717
// keeps the response open and sends one sample per document.
1818
if (200 === $response->getStatusCode() && $this->getQueryOptionsResolver()->resolve($this->queryParameters)['stream']) {
19-
return new StatsStream($response->getBody());
19+
return new StatsStream($response->getBody(), $serializer);
2020
}
2121

2222
return parent::transformResponseBody($response, $serializer, $contentType);
Collapse file

‎src/Endpoint/SystemEvents.php‎

Copy file name to clipboardExpand all lines: src/Endpoint/SystemEvents.php
+12Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@
1111

1212
class SystemEvents extends BaseEndpoint
1313
{
14+
/**
15+
* @param array $accept Accept header values; API 1.52 added this parameter
16+
*/
17+
public function __construct(array $queryParameters = [], array $accept = [])
18+
{
19+
parent::__construct($queryParameters);
20+
// From API 1.52 the generated endpoint stores the Accept values here.
21+
if (property_exists($this, 'accept')) {
22+
$this->accept = $accept;
23+
}
24+
}
25+
1426
protected function transformResponseBody(ResponseInterface $response, SerializerInterface $serializer, ?string $contentType = null)
1527
{
1628
if (200 === $response->getStatusCode()) {
Collapse file

‎src/Exception/UnexpectedStatusCodeException.php‎

Copy file name to clipboardExpand all lines: src/Exception/UnexpectedStatusCodeException.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
/**
1212
* Docker returned an error status that the generated endpoint does not handle.
1313
*
14-
* Thrown when Docker::throwOnUnexpectedStatus() is enabled; catch
14+
* Thrown unless Docker::throwOnUnexpectedStatus(false) is set; catch
1515
* UnexpectedClientErrorException or UnexpectedServerErrorException to tell 4xx
1616
* and 5xx responses apart.
1717
*/

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.