Test PHP projects in CI with PHP CLI, Composer, PHPUnit, pcov code coverage, and common extensions.
22
Test PHP projects in CI with PHP CLI, Composer, PHPUnit, pcov code coverage, and common extensions.
This image runs the tests of a PHP project on a computer or in a CI job. It contains:
php:latest (Debian).composer:latest image.phpunit command. The build
checks the GPG signature of the PHAR with the PHPUnit release key.bcmath, intl, pdo_mysql, pdo_pgsql, and zip, and the built-in
mbstring, pdo_sqlite, curl, dom, openssl, sodium, and xml.git and unzip, which Composer uses to download packages.The image uses php.ini-development, so PHP shows all errors and runs assert(). The memory
limit is off (memory_limit = -1), because the coverage of a large project uses much memory.
Composer keeps its settings and its cache in /tmp/composer (COMPOSER_HOME), so it also
works for a user without a home directory. COMPOSER_ALLOW_SUPERUSER=1 stops the warning
when a CI job runs Composer as root. The working directory is /workspace. The default
command starts Bash.
| Tag | Meaning | Platform |
|---|---|---|
latest | Latest successful build from main with the newest stable tools. | linux/amd64 |
View all tags on Docker Hub.
The latest tag changes when a new build passes its tests.
docker pull stden/php:latest
docker run --rm stden/php:latest bash -c 'php --version && composer --version && phpunit --version'
Run the commands from the root of your project, where composer.json is. The commands
mount the project directory at /workspace. --user "$(id -u):$(id -g)" runs the container
as your user, so vendor/, composer.lock, and the coverage reports belong to you.
mkdir -p "$HOME/.cache/composer"
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/workspace \
-v "$HOME/.cache/composer":/tmp/composer/cache \
stden/php:latest composer install --no-interaction --prefer-dist
The second volume keeps the Composer download cache between runs. Create the cache
directory before the first run. If Docker creates it, it belongs to root.
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/workspace \
stden/php:latest vendor/bin/phpunit
vendor/bin/phpunit is the PHPUnit version that composer.lock sets. If phpunit/phpunit
is not a dependency of the project, use the phpunit command of the image. To get a shell
in the project, add -it and do not give a command. The default command is bash.
PHPUnit must have a <source> element in phpunit.xml. This element tells PHPUnit which
files to report:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" cacheDirectory=".phpunit.cache"
failOnWarning="true" failOnRisky="true">
<testsuites>
<testsuite name="unit">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/workspace \
stden/php:latest vendor/bin/phpunit --coverage-text --coverage-html coverage
Open coverage/index.html for the report. The Runtime: line of the PHPUnit output must
show with PCOV. pcov collects data only for files in the directory pcov.directory. By
default, this is src, lib, or app in the current directory. If none of them exists, it
is the current directory. If your code is in a different directory, set it:
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/workspace stden/php:latest \
php -d pcov.directory=/workspace/modules vendor/bin/phpunit --coverage-text
Integration tests must have a database server. Start PostgreSQL in a Docker network. Give
the connection settings to the tests in environment variables. The test code reads them,
for example new PDO(getenv('DB_DSN'), getenv('DB_USER'), getenv('DB_PASSWORD')).
docker network create php-tests
docker run -d --name php-tests-db --network php-tests \
-e POSTGRES_PASSWORD=test -e POSTGRES_DB=app_test postgres:latest
until docker exec php-tests-db pg_isready -h localhost -U postgres; do sleep 1; done
docker run --rm --user "$(id -u):$(id -g)" --network php-tests -v "$PWD":/workspace \
-e DB_DSN="pgsql:host=php-tests-db;dbname=app_test" -e DB_USER=postgres -e DB_PASSWORD=test \
stden/php:latest vendor/bin/phpunit
docker rm -f php-tests-db
docker network rm php-tests
For MySQL, use mysql:latest with MYSQL_ROOT_PASSWORD and MYSQL_DATABASE, and the DSN
mysql:host=php-tests-db;dbname=app_test. The password test is a placeholder for a
disposable test database.
The image has the docker-php-ext-install, docker-php-ext-enable, and pecl tools of
the official PHP image. Add the extensions that are necessary for your project:
FROM stden/php:latest
RUN apt-get update \
&& apt-get install -y --no-install-recommends libpng-dev \
&& docker-php-ext-install gd \
&& rm -rf /var/lib/apt/lists/*
The job runs in the image. The PostgreSQL service has the host name postgres.
name: PHP tests
on: [push, pull_request]
permissions:
contents: read
jobs:
phpunit:
runs-on: ubuntu-latest
container: stden/php:latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: app_test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DB_DSN: pgsql:host=postgres;dbname=app_test
DB_USER: postgres
DB_PASSWORD: test
steps:
- uses: actions/checkout@v7
- uses: actions/cache@v6
with:
path: /tmp/composer/cache
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: composer-
- run: composer validate --strict
- run: composer install --no-interaction --prefer-dist --no-progress
- run: vendor/bin/phpunit --coverage-text --coverage-clover coverage.xml
The job runs in the image. The MySQL service has the host name mysql. GitLab reads the
coverage percentage from the Lines: line and shows the test results in the merge request.
phpunit:
image: stden/php:latest
services:
- name: mysql:latest
alias: mysql
variables:
MYSQL_DATABASE: app_test
MYSQL_ROOT_PASSWORD: test
DB_DSN: "mysql:host=mysql;dbname=app_test"
DB_USER: root
DB_PASSWORD: test
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
cache:
key:
files: [composer.lock]
paths: [.composer-cache/]
script:
- composer validate --strict
- composer install --no-interaction --prefer-dist --no-progress
- vendor/bin/phpunit --colors=never --coverage-text --log-junit junit.xml --coverage-cobertura coverage.xml
coverage: '/^\s*Lines:\s*\d+\.\d+%/'
artifacts:
when: always
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
GitLab caches only files in the project directory. For this reason, COMPOSER_CACHE_DIR
points into the project.
composer.lock. Then CI installs the same package versions as your computer.
Run composer install in CI. Run composer update only when you want new versions.composer install --no-interaction --prefer-dist.
--prefer-dist downloads release archives and does not clone git repositories. This is
faster and uses less disk space. Add --no-progress to keep the CI log short.vendor/. Use composer.lock in the cache key. With a
warm cache, composer install does not download the packages again.phpunit/phpunit to require-dev. Run
vendor/bin/phpunit. Then all developers and CI use the same version.failOnWarning and failOnRisky in phpunit.xml. Put the
PHPUnit cache in .phpunit.cache. Add this directory to .gitignore.composer validate --strict on each change. Run
composer audit to find packages with known security problems. It must have network access.--user "$(id -u):$(id -g)", so that the
container does not create files that belong to root in your project.The smoke test writes a small project with a PSR-4 autoloader and no external
dependencies. It validates composer.json, generates the autoloader with Composer, runs
PHPUnit with pcov, and checks the exact coverage numbers. If a test fails, PHPUnit must
exit with status 1. The test also checks that each extension loads without a warning and
works. It runs without network access.
Content type
Image
Digest
sha256:1ec1293ca…
Size
215.6 MB
Last updated
about 2 hours ago
docker pull stden/php