A lean pipeline without Magento overhead
Not every PHP project is a monolith like Magento. For standalone microservices built with Symfony or Laravel, a much leaner .gitlab-ci.yml is enough, one that still cleanly covers the same core principles of build, test, and deployment.
Table of Contents
- 1. Why a microservice pipeline looks different from a Magento pipeline
- 2. Basic skeleton: stage structure for a generic PHP pipeline
- 3. Composer install and caching as the base for all downstream jobs
- 4. PHPUnit with coverage evaluation in the MR widget
- 5. Static code analysis with PHPStan and PHP-CS-Fixer
- 6. Symfony-specific steps: Doctrine migrations and environment
- 7. Laravel-specific steps: artisan test and config cache
- 8. Deployment as a Docker image build and push
- 9. Conclusion: stay lean instead of copying Magento patterns
- 10. Summary
- 11. FAQ
1. Why a microservice pipeline looks different from a Magento pipeline
A Magento pipeline has to deal with particularities like reindexing, static content deployment, zero-downtime switches between release directories, and a complex module structure. A standalone PHP microservice, say a Symfony-based invoicing service or a Laravel API for order status lookups, has none of that: it consists of a single Composer project, a manageable number of dependencies, and usually one clear, containerized deployment target.
That reduction is an advantage, not a shortcoming. A generic pipeline for a microservice can focus on four clear steps: install dependencies, run automated tests, run static code analysis, and build and roll out a deployable artifact, usually a Docker image. Anyone trying to transplant Magento pipeline patterns one to one onto such a project ends up building unnecessary complexity that nobody needs or wants to maintain.
2. Basic skeleton: stage structure for a generic PHP pipeline
The basic structure follows a simple linear pattern: build installs dependencies and makes them available as an artifact for subsequent stages, test runs PHPUnit and static analysis in parallel, and deploy builds and distributes the final artifact. Unlike Magento, a dedicated stage for static content deployment or reindexing is entirely absent, because those concepts simply do not exist in a generic PHP service.
The stages: block at the top of the file matters because it sets the global order. Jobs within the same stage run in parallel, stages themselves run sequentially, unless needs: defines explicit cross-stage dependencies, which is often unnecessary for smaller microservices since the total runtime stays low anyway.
stages:
- build
- test
- deploy
variables:
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
default:
image: php:8.3-cli
before_script:
- apt-get update -qq && apt-get install -y -qq unzip git
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
3. Composer install and caching as the base for all downstream jobs
The build job installs dependencies once and makes vendor/ available as an artifact for all subsequent jobs. That avoids every test and analysis job running composer install individually, which costs unnecessary time across several parallel jobs and puts unnecessary load on the runners. Combined with a CI cache for the Composer download cache, even the first composer install speeds up noticeably, since package zips do not need to be re-fetched from the network on every run.
For production-grade builds, composer install --no-dev --optimize-autoloader is the right choice for the final deployment artifact, while test and analysis jobs need the dev dependencies such as PHPUnit or PHPStan. A clean approach is therefore to install once with dev dependencies in the build job and let test jobs reuse that artifact, while a separate, later step in the deploy context produces the lean production variant.
build:
stage: build
script:
- composer install --no-interaction --prefer-dist
cache:
key: composer-cache
paths:
- $COMPOSER_CACHE_DIR
artifacts:
paths:
- vendor/
expire_in: 1 hour
4. PHPUnit with coverage evaluation in the MR widget
GitLab reads test coverage directly from the job output when a coverage: regex is configured on the job, and shows the result as a percentage in the pipeline widget. For more detailed evaluation, such as line-level highlighting in a merge request diff, a Cobertura XML report is additionally needed as artifacts: reports: coverage_report, which PHPUnit produces via --coverage-cobertura.
It matters that Xdebug or PCOV is active in the PHP image used, since PHPUnit cannot produce coverage data without a coverage driver and will silently skip the report step. For faster test runs, PCOV is preferable to Xdebug, since it was built exclusively for coverage collection and therefore causes far less overhead compared to the full Xdebug debugger.
phpunit:
stage: test
needs: ["build"]
script:
- vendor/bin/phpunit --coverage-text --coverage-cobertura=coverage.xml
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
5. Static code analysis with PHPStan and PHP-CS-Fixer
Static analysis runs in its own stage in parallel with PHPUnit, since both tools work independently and do not need to block each other. PHPStan checks type correctness and potential runtime errors before execution even happens, while PHP-CS-Fixer in --dry-run mode ensures the code style matches the project standard without actually modifying files.
Both tools should actively fail the pipeline on violations, unlike optional security scans, because style violations and type errors are not an acceptable state for the main branch in a well-maintained microservice project. A non-zero exit code from PHPStan or PHP-CS-Fixer --dry-run is already enough for that, without any additional allow_failure: configuration being needed.
phpstan:
stage: test
needs: ["build"]
script:
- vendor/bin/phpstan analyse src --level=8 --no-progress
php-cs-fixer:
stage: test
needs: ["build"]
script:
- vendor/bin/php-cs-fixer fix --dry-run --diff
6. Symfony-specific steps: Doctrine migrations and environment
A Symfony service adds two extra checks that a generic framework-less PHP project does not have: doctrine:schema:validate ensures the entity mappings match the actual database schema, and doctrine:migrations:migrate --dry-run in the test stage verifies that pending migrations can be applied cleanly, without actually running them against a real database.
Because Symfony strictly separates APP_ENV=test, dev, and prod, the CI environment variable APP_ENV must be explicitly set to test, otherwise the pipeline accidentally reaches for a production configuration that does not even exist in CI. A dedicated .env.test.local used only in the pipeline keeps real credentials cleanly separated from the CI configuration.
symfony-doctrine-check:
stage: test
needs: ["build"]
variables:
APP_ENV: test
services:
- postgres:16
script:
- php bin/console doctrine:schema:validate --skip-sync
- php bin/console doctrine:migrations:migrate --dry-run --no-interaction
7. Laravel-specific steps: artisan test and config cache
In Laravel, artisan test takes on the role of PHPUnit as a convenient wrapper with readable output and additional Laravel-specific assertions. Before the test run, the .env file must be copied from .env.testing and an APP_KEY generated, since Laravel aborts with an exception during bootstrapping without a valid encryption key.
It is also worth running artisan config:cache and artisan route:cache as a trial run inside the pipeline, even if that cache is not permanently used in the test context. It surfaces errors early that only become visible once configuration gets cached, such as unresolvable environment variables in config files that would otherwise silently pass through as null in normal, uncached operation.
laravel-test:
stage: test
needs: ["build"]
script:
- cp .env.testing .env
- php artisan key:generate
- php artisan config:cache
- php artisan test --parallel
8. Deployment as a Docker image build and push
The usual deployment path for a containerized microservice is building a Docker image inside the pipeline, pushing it to the GitLab Container Registry, and rolling it out on the target platform afterward, for example via kubectl or a simple SSH restart on a Docker Compose host. The production-ready image should use a multi-stage Dockerfile that never brings Composer dev dependencies or test tools into the final image in the first place.
Unlike a Magento instance with a shared filesystem and multiple parallel PHP-FPM processes, deploying a microservice is usually a single, disposable container. A failed deployment can therefore often be fixed simply by rolling back to the previous image tag, without the more elaborate zero-downtime mechanisms that monolithic Magento deployments require.
build-image:
stage: deploy
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
rules:
- if: $CI_COMMIT_BRANCH == "main"
9. Conclusion: stay lean instead of copying Magento patterns
A generic PHP microservice benefits most from a pipeline that contains exactly the steps the project actually needs, not from a shrunken copy of a Magento pipeline with adopted but unused concepts. Build, test, static analysis, and a simple image build fully cover most Symfony or Laravel services.
Framework-specific additions like Doctrine migration checks for Symfony or artisan test for Laravel should be added deliberately and minimally, rather than integrating every conceivable check just in case. A short, readable .gitlab-ci.yml is almost always more valuable for a small microservice than a complete but cluttered pipeline full of unused stages.
| Aspect | Magento pipeline | Generic PHP microservice pipeline |
|---|---|---|
| Typical stages | build, static-content, test, deploy, reindex | build, test, deploy |
| Deployment target | Shared filesystem, zero-downtime releases | Single, disposable Docker image |
| Framework specifics | Module compilation, DI compile, static content | Doctrine migrations or artisan commands |
| Typical runtime | 10-30 minutes | 2-6 minutes |
Mironsoft
CI/CD pipelines, zero-downtime deployments and release automation
Deployments that run without downtime and without the nail-biting?
We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.
Pipeline Review
Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.
Zero-Downtime Deployment
Building symlink releases, health checks and rollback strategies for Magento stores.
CI/CD Automation
Connecting tests, security scans and deployments into one reliable pipeline.
10. Summary
GitLab CI for PHP Microservices: The Essentials at a Glance
Four basic stages
build, test, static analysis, and deploy cover most standalone PHP services.
Artifact reuse
Install vendor/ once in the build job and pass it to all downstream jobs via artifacts:.
Targeted framework extras
Doctrine checks for Symfony, artisan test for Laravel, nothing more than actually needed.
Containers over zero-downtime tricks
A disposable Docker container replaces complex release-directory mechanisms.