Skip to content

Commit f6bc5d0

Browse files
authored
Release/2.2.0 (#18)
1 parent 0c0f2e4 commit f6bc5d0

24 files changed

Lines changed: 1283 additions & 140 deletions

.claude/rules/code-style.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ Verify every item before producing any PHP code. If any item fails, revise befor
6565
## Comparisons
6666

6767
1. Null checks: use `is_null($variable)`, never `$variable === null`.
68-
2. Empty string checks on typed `string` parameters: use `$variable === ''`. Avoid `empty()` on typed strings because
69-
`empty('0')` returns `true`.
68+
2. Empty string checks on typed `string` parameters: use `empty($variable)`, never `$variable === ''`.
7069
3. Mixed or untyped checks (value may be `null`, empty string, `0`, or `false`): use `empty($variable)`.
7170

7271
## American English

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ unit-test-no-coverage: ## Run unit tests without coverage
4141
.PHONY: configure-test-environment
4242
configure-test-environment:
4343
@if ! docker network inspect tiny-blocks > /dev/null 2>&1; then \
44-
docker network create tiny-blocks > /dev/null 2>&1; \
44+
docker network create --label tiny-blocks.docker-container=true tiny-blocks > /dev/null 2>&1; \
4545
fi
46-
@docker volume create test-adm-migrations > /dev/null 2>&1
4746

4847
.PHONY: review
4948
review: ## Run static code analysis

README.md

Lines changed: 135 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,26 @@
88
* [Creating a container](#creating-a-container)
99
* [Running a container](#running-a-container)
1010
* [Running if not exists](#running-if-not-exists)
11-
* [Pulling an image](#pulling-an-image)
11+
* [Pulling images in parallel](#pulling-images-in-parallel)
1212
* [Setting network](#setting-network)
1313
* [Setting port mappings](#setting-port-mappings)
1414
* [Setting volume mappings](#setting-volume-mappings)
1515
* [Setting environment variables](#setting-environment-variables)
1616
* [Disabling auto-remove](#disabling-auto-remove)
1717
* [Copying files to a container](#copying-files-to-a-container)
1818
* [Stopping a container](#stopping-a-container)
19+
* [Stopping on shutdown](#stopping-on-shutdown)
1920
* [Executing commands after startup](#executing-commands-after-startup)
2021
* [Wait strategies](#wait-strategies)
2122
* [MySQL container](#mysql-container)
2223
* [Configuring MySQL options](#configuring-mysql-options)
2324
* [Setting readiness timeout](#setting-readiness-timeout)
2425
* [Retrieving connection data](#retrieving-connection-data)
26+
* [Flyway container](#flyway-container)
27+
* [Setting the database source](#setting-the-database-source)
28+
* [Configuring migrations](#configuring-migrations)
29+
* [Configuring Flyway options](#configuring-flyway-options)
30+
* [Running Flyway commands](#running-flyway-commands)
2531
* [Usage examples](#usage-examples)
2632
* [MySQL with Flyway migrations](#mysql-with-flyway-migrations)
2733
* [License](#license)
@@ -80,57 +86,69 @@ Starts a container only if a container with the same name is not already running
8086
$container->runIfNotExists();
8187
```
8288

83-
### Pulling an image
89+
### Pulling images in parallel
8490

85-
Starts pulling the container image in the background. When `run()` or `runIfNotExists()` is called, it waits for
86-
the pull to complete before starting the container. Calling this on multiple containers before running them enables
87-
parallel image pulls.
91+
Calling `pullImage()` starts downloading the image in the background via a non-blocking process. When `run()` or
92+
`runIfNotExists()` is called, it waits for the pull to complete before starting the container.
93+
94+
To pull multiple images in parallel, call `pullImage()` on all containers **before** calling `run()` on any of
95+
them. This way the downloads happen concurrently:
8896

8997
```php
90-
$alpine = GenericDockerContainer::from(image: 'alpine:latest')->pullImage();
91-
$nginx = GenericDockerContainer::from(image: 'nginx:latest')->pullImage();
98+
$mysql = MySQLDockerContainer::from(image: 'mysql:8.4', name: 'my-database')
99+
->pullImage()
100+
->withRootPassword(rootPassword: 'root');
101+
102+
$flyway = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine')
103+
->pullImage()
104+
->withMigrations(pathOnHost: '/path/to/migrations');
92105

93-
$alpineStarted = $alpine->run();
94-
$nginxStarted = $nginx->run();
106+
// Both images are downloading in the background.
107+
// MySQL pull completes here, container starts and becomes ready.
108+
$mySQLStarted = $mysql->runIfNotExists();
109+
110+
// Flyway pull already finished while MySQL was starting.
111+
$flyway->withSource(container: $mySQLStarted, username: 'root', password: 'root')
112+
->cleanAndMigrate();
95113
```
96114

97115
### Setting network
98116

99117
Sets the Docker network the container should join. The network is created automatically when the container is
100-
started via `run()` or `runIfNotExists()`, if it does not already exist.
118+
started via `run()` or `runIfNotExists()`, if it does not already exist. Networks created by the library are
119+
labeled with `tiny-blocks.docker-container=true` for safe cleanup.
101120

102121
```php
103122
$container->withNetwork(name: 'my-network');
104123
```
105124

106125
### Setting port mappings
107126

108-
Maps ports between the host and the container. Multiple port mappings are supported.
127+
Maps a port from the host to the container.
109128

110129
```php
111-
$container->withPortMapping(portOnHost: 9000, portOnContainer: 9000);
112130
$container->withPortMapping(portOnHost: 8080, portOnContainer: 80);
113131
```
114132

115133
### Setting volume mappings
116134

117-
Maps a volume from the host to the container.
135+
Mounts a directory from the host into the container.
118136

119137
```php
120-
$container->withVolumeMapping(pathOnHost: '/path/on/host', pathOnContainer: '/path/in/container');
138+
$container->withVolumeMapping(pathOnHost: '/host/data', pathOnContainer: '/container/data');
121139
```
122140

123141
### Setting environment variables
124142

125-
Sets environment variables inside the container.
143+
Adds an environment variable to the container.
126144

127145
```php
128146
$container->withEnvironmentVariable(key: 'APP_ENV', value: 'testing');
129147
```
130148

131149
### Disabling auto-remove
132150

133-
Prevents the container from being automatically removed when stopped.
151+
By default, containers are removed when stopped. This disables that behavior.
134152

135153
```php
136154
$container->withoutAutoRemove();
@@ -160,6 +178,23 @@ With a custom timeout:
160178
$result = $started->stop(timeoutInWholeSeconds: 60);
161179
```
162180

181+
### Stopping on shutdown
182+
183+
Registers the container to be forcefully removed when the PHP process exits. On shutdown, the following cleanup
184+
is performed automatically:
185+
186+
- The container is killed and removed (`docker rm --force --volumes`).
187+
- Anonymous volumes created by the container (e.g., MySQL's `/var/lib/mysql`) are removed.
188+
- Unused networks created by the library are pruned.
189+
190+
Only resources labeled with `tiny-blocks.docker-container=true` are affected. Containers, volumes, and networks
191+
from other environments are never touched.
192+
193+
```php
194+
$started = $container->run();
195+
$started->stopOnShutdown();
196+
```
197+
163198
### Executing commands after startup
164199

165200
Runs commands inside an already-started container.
@@ -192,11 +227,11 @@ Blocks until a readiness condition is satisfied, with a configurable timeout. Th
192227
depends on another being fully ready.
193228

194229
```php
195-
$mySQLStarted = MySQLDockerContainer::from(image: 'mysql:8.1')
230+
$mySQLStarted = MySQLDockerContainer::from(image: 'mysql:8.4')
196231
->withRootPassword(rootPassword: 'root')
197232
->run();
198233

199-
$flywayContainer = GenericDockerContainer::from(image: 'flyway/flyway:11.1.0')
234+
$container = GenericDockerContainer::from(image: 'my-app:latest')
200235
->withWaitBeforeRun(
201236
wait: ContainerWaitForDependency::untilReady(
202237
condition: MySQLReady::from(container: $mySQLStarted),
@@ -223,7 +258,7 @@ MySQL-specific configuration and automatic readiness detection.
223258
| `withGrantedHosts` | `$hosts` | Sets hosts granted root privileges (default: `['%', '172.%']`). |
224259

225260
```php
226-
$mySQLContainer = MySQLDockerContainer::from(image: 'mysql:8.1', name: 'my-database')
261+
$mySQLContainer = MySQLDockerContainer::from(image: 'mysql:8.4', name: 'my-database')
227262
->withTimezone(timezone: 'America/Sao_Paulo')
228263
->withUsername(user: 'app_user')
229264
->withPassword(password: 'secret')
@@ -240,7 +275,7 @@ Configures how long the MySQL container waits for the database to become ready b
240275
`ContainerWaitTimeout` exception. The default timeout is 30 seconds.
241276

242277
```php
243-
$mySQLContainer = MySQLDockerContainer::from(image: 'mysql:8.1', name: 'my-database')
278+
$mySQLContainer = MySQLDockerContainer::from(image: 'mysql:8.4', name: 'my-database')
244279
->withRootPassword(rootPassword: 'root')
245280
->withReadinessTimeout(timeoutInSeconds: 60)
246281
->run();
@@ -264,6 +299,65 @@ $password = $environmentVariables->getValueBy(key: 'MYSQL_PASSWORD');
264299
$jdbcUrl = $mySQLContainer->getJdbcUrl();
265300
```
266301

302+
## Flyway container
303+
304+
`FlywayDockerContainer` provides a specialized container for running Flyway database migrations. It encapsulates
305+
Flyway configuration, database source detection, and migration file management.
306+
307+
### Setting the database source
308+
309+
Configures the Flyway container to connect to a running MySQL container. Automatically detects the JDBC URL and
310+
target schema from `MYSQL_DATABASE`, and sets the history table to `schema_history`.
311+
312+
```php
313+
$flywayContainer = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine')
314+
->withNetwork(name: 'my-network')
315+
->withMigrations(pathOnHost: '/path/to/migrations')
316+
->withSource(container: $mySQLStarted, username: 'root', password: 'root');
317+
```
318+
319+
The schema and table can be overridden after calling `withSource()`:
320+
321+
```php
322+
$flywayContainer
323+
->withSource(container: $mySQLStarted, username: 'root', password: 'root')
324+
->withSchema(schema: 'custom_schema')
325+
->withTable(table: 'custom_history');
326+
```
327+
328+
### Configuring migrations
329+
330+
Sets the host directory containing Flyway migration SQL files. The files are copied into the container at
331+
`/flyway/migrations`.
332+
333+
```php
334+
$flywayContainer->withMigrations(pathOnHost: '/path/to/migrations');
335+
```
336+
337+
### Configuring Flyway options
338+
339+
| Method | Parameter | Description |
340+
|-------------------------------|-------------|------------------------------------------------------------------|
341+
| `withTable` | `$table` | Overrides the history table name (default: `schema_history`). |
342+
| `withSchema` | `$schema` | Overrides the target schema (default: auto-detected from MySQL). |
343+
| `withCleanDisabled` | `$disabled` | Enables or disables Flyway's clean command. |
344+
| `withConnectRetries` | `$retries` | Sets the number of database connection retries. |
345+
| `withValidateMigrationNaming` | `$enabled` | Enables or disables migration naming validation. |
346+
347+
### Running Flyway commands
348+
349+
| Method | Flyway command | Description |
350+
|---------------------|-----------------|----------------------------------------------|
351+
| `migrate()` | `migrate` | Applies pending migrations. |
352+
| `validate()` | `validate` | Validates applied migrations against local. |
353+
| `repair()` | `repair` | Repairs the schema history table. |
354+
| `cleanAndMigrate()` | `clean migrate` | Drops all objects and re-applies migrations. |
355+
356+
```php
357+
$flywayContainer->migrate();
358+
$flywayContainer->cleanAndMigrate();
359+
```
360+
267361
## Usage examples
268362

269363
- When running the containers from the library on a host (your local machine), map the volume
@@ -273,59 +367,32 @@ $jdbcUrl = $mySQLContainer->getJdbcUrl();
273367

274368
### MySQL with Flyway migrations
275369

276-
The MySQL container is configured and started:
370+
Configure both containers and start image pulls in parallel before running either one:
277371

278372
```php
279-
$mySQLContainer = MySQLDockerContainer::from(image: 'mysql:8.1', name: 'test-database')
280-
->withNetwork(name: 'tiny-blocks')
373+
$mySQLContainer = MySQLDockerContainer::from(image: 'mysql:8.4', name: 'test-database')
374+
->pullImage()
375+
->withNetwork(name: 'my-network')
281376
->withTimezone(timezone: 'America/Sao_Paulo')
282-
->withUsername(user: 'xpto')
283-
->withPassword(password: '123')
377+
->withPassword(password: 'secret')
284378
->withDatabase(database: 'test_adm')
285-
->withPortMapping(portOnHost: 3306, portOnContainer: 3306)
286379
->withRootPassword(rootPassword: 'root')
287-
->withGrantedHosts()
288-
->withReadinessTimeout(timeoutInSeconds: 60)
289-
->withoutAutoRemove()
290-
->runIfNotExists();
291-
```
292-
293-
With the MySQL container started, retrieve the connection data:
294-
295-
```php
296-
$environmentVariables = $mySQLContainer->getEnvironmentVariables();
297-
$jdbcUrl = $mySQLContainer->getJdbcUrl();
298-
$database = $environmentVariables->getValueBy(key: 'MYSQL_DATABASE');
299-
$username = $environmentVariables->getValueBy(key: 'MYSQL_USER');
300-
$password = $environmentVariables->getValueBy(key: 'MYSQL_PASSWORD');
301-
```
302-
303-
The Flyway container is configured and only starts after the MySQL container is **ready**:
304-
305-
```php
306-
$flywayContainer = GenericDockerContainer::from(image: 'flyway/flyway:11.1.0')
307-
->withNetwork(name: 'tiny-blocks')
308-
->copyToContainer(pathOnHost: '/test-adm-migrations', pathOnContainer: '/flyway/sql')
309-
->withVolumeMapping(pathOnHost: '/test-adm-migrations', pathOnContainer: '/flyway/sql')
310-
->withWaitBeforeRun(
311-
wait: ContainerWaitForDependency::untilReady(
312-
condition: MySQLReady::from(container: $mySQLContainer),
313-
timeoutInSeconds: 30
314-
)
315-
)
316-
->withEnvironmentVariable(key: 'FLYWAY_URL', value: $jdbcUrl)
317-
->withEnvironmentVariable(key: 'FLYWAY_USER', value: $username)
318-
->withEnvironmentVariable(key: 'FLYWAY_TABLE', value: 'schema_history')
319-
->withEnvironmentVariable(key: 'FLYWAY_SCHEMAS', value: $database)
320-
->withEnvironmentVariable(key: 'FLYWAY_EDITION', value: 'community')
321-
->withEnvironmentVariable(key: 'FLYWAY_PASSWORD', value: $password)
322-
->withEnvironmentVariable(key: 'FLYWAY_LOCATIONS', value: 'filesystem:/flyway/sql')
323-
->withEnvironmentVariable(key: 'FLYWAY_CLEAN_DISABLED', value: 'false')
324-
->withEnvironmentVariable(key: 'FLYWAY_VALIDATE_MIGRATION_NAMING', value: 'true')
325-
->run(
326-
commands: ['-connectRetries=15', 'clean', 'migrate'],
327-
waitAfterStarted: ContainerWaitForTime::forSeconds(seconds: 5)
328-
);
380+
->withGrantedHosts();
381+
382+
$flywayContainer = FlywayDockerContainer::from(image: 'flyway/flyway:12-alpine')
383+
->pullImage()
384+
->withNetwork(name: 'my-network')
385+
->withMigrations(pathOnHost: '/path/to/migrations')
386+
->withCleanDisabled(disabled: false)
387+
->withConnectRetries(retries: 5)
388+
->withValidateMigrationNaming(enabled: true);
389+
390+
$mySQLStarted = $mySQLContainer->runIfNotExists();
391+
$mySQLStarted->stopOnShutdown();
392+
393+
$flywayContainer
394+
->withSource(container: $mySQLStarted, username: 'root', password: 'root')
395+
->cleanAndMigrate();
329396
```
330397

331398
## License

src/Contracts/ContainerStarted.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,25 @@ public function getAddress(): Address;
4545
public function getEnvironmentVariables(): EnvironmentVariables;
4646

4747
/**
48-
* Stops the running container.
48+
* Stops the running container gracefully.
4949
*
5050
* @param int $timeoutInWholeSeconds The maximum time in seconds to wait for the container to stop.
5151
* @return ExecutionCompleted The result of the stop command execution.
5252
* @throws DockerCommandExecutionFailed If the stop command fails.
5353
*/
5454
public function stop(int $timeoutInWholeSeconds = self::DEFAULT_TIMEOUT_IN_WHOLE_SECONDS): ExecutionCompleted;
5555

56+
/**
57+
* Forcefully removes the container and its anonymous volumes, then prunes
58+
* unused networks created by the library.
59+
*/
60+
public function remove(): void;
61+
62+
/**
63+
* Registers the container to be removed when the PHP process exits.
64+
*/
65+
public function stopOnShutdown(): void;
66+
5667
/**
5768
* Executes commands inside the running container.
5869
*

0 commit comments

Comments
 (0)