GitLab CI for Generic PHP Microservices: a Pipeline Without Magento Baggage
AI generated
CI/CD
.yml
GitLab · CI/CD · PHP Microservices
GitLab CI for generic PHP microservices
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.

17 min read Symfony Laravel PHPUnit PHPStan

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.

11. FAQ: GitLab CI for PHP Microservices: The Essentials at a Glance

1Why is a microservice pipeline simpler than a Magento pipeline?
Because a microservice has no shared filesystem structure, no static content compilation, and no complex module landscape. Build, test, and a single deployment step are usually enough.
2How do I avoid every job running composer install individually?
The build job installs dependencies once and provides vendor/ as artifacts: for all downstream jobs, so they do not need to repeat composer install.
3How do I show test coverage in the merge request widget?
Via a coverage: regex on the PHPUnit job for the percentage display, and a Cobertura XML report under artifacts: reports: coverage_report for line-level highlighting in the diff.
4Should PHPStan block the pipeline on errors?
Yes, unlike optional scans, static analysis should actively fail the pipeline with a non-zero exit code, since type errors are not an acceptable state for the main branch.
5What extra checks are needed for Symfony pipelines?
doctrine:schema:validate ensures entity mappings match the database schema, and doctrine:migrations:migrate --dry-run checks pending migrations without actually applying them.
6What extra checks are needed for Laravel pipelines?
artisan test as a PHPUnit wrapper, a generated APP_KEY before the test run, and a trial artisan config:cache run to catch configuration caching errors early.
7Why use PCOV instead of Xdebug for coverage in CI?
PCOV was built exclusively for coverage collection and is significantly faster than the full Xdebug debugger, which carries additional overhead for step debugging that CI jobs do not need.
8How is a microservice deployment technically implemented?
Usually by building a Docker image in the pipeline, pushing it to the GitLab Container Registry, and rolling it out via kubectl or a simple restart on the target system.
9How does a failed microservice deployment differ from Magento?
A microservice container can usually be fixed simply by rolling back to the previous image tag, while Magento needs more elaborate zero-downtime mechanisms with multiple release directories.
10Is a multi-stage Dockerfile worth it for PHP microservices?
Yes, it keeps Composer dev dependencies and test tools out of the final production image, reducing image size and shrinking the attack surface in production.