A multi-stage target that exists only to run tests

A named stage installing dev dependencies and running the suite guarantees the test environment is the runtime environment plus tools, rather than a second description that drifts.

FROM php:8.1-fpm-alpine AS base
# extensions, configuration

FROM base AS vendor-dev
RUN --mount=type=cache,target=/root/.composer/cache 
    composer install --no-progress

FROM vendor-dev AS test
COPY . .
RUN vendor/bin/phpunit && vendor/bin/phpstan analyse

FROM base AS runtime
COPY --from=vendor-prod /app/vendor /app/vendor

# docker build --target test .

Stages not reached by the target are never built, which is what keeps this affordable — building runtime does not run the tests. The cost is that a failing test is a failing build with the output buried in build logs, which is worse to read than a test step; most pipelines keep both, with this stage as the guarantee that they cannot diverge.