
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Managing CI CD for monorepo with multiple PHP apps introduces complexity that standard single-repo pipelines cannot handle efficiently. When your repository contains a Laravel API, a Symfony worker, and shared packages, rebuilding everything on every commit wastes resources and slows feedback loops. You need a pipeline that understands dependency graphs, detects actual changes, and deploys only what is necessary while maintaining atomic versioning.
How do you detect changes selectively in CI CD for monorepo with multiple PHP apps?
The foundation of any efficient monorepo pipeline is accurate change detection. Without it, you rebuild every PHP application on every commit, which becomes untenable as your repository grows. In practice, I have seen teams waste 45+ minutes per push building six Laravel apps when only one had changes. The solution is a dedicated detection step that outputs a structured list of affected projects before any build job starts.
Implementing path filters with Git diff
Most CI systems provide native path filtering, but these are often too coarse for PHP monorepos where shared packages create implicit dependencies. A more reliable approach uses git diff against the merge base to identify changed directories, then maps those paths to your application structure. For GitHub Actions, this means a preliminary job that sets output variables consumed by downstream matrix jobs.
# .github/workflows/detect-changes.yml
name: Detect Affected Apps
on: [push, pull_request]
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed files
id: changed
run: |
BASE=${{ github.event.pull_request.base.sha || github.event.before }}
CHANGED=$(git diff --name-only $BASE HEAD | \
grep -E '^apps/' | \
cut -d'/' -f2 | \
sort -u | \
jq -R -s -c 'split("\n")[:-1]')
echo "apps=$CHANGED" >> $GITHUB_OUTPUT
- name: Set matrix
id: set-matrix
run: echo "matrix={\"app\":${{ steps.changed.outputs.apps }}}" >> $GITHUB_OUTPUT This script compares against the PR base or previous commit, extracts unique app directories from changed files, and formats them as JSON for matrix consumption. If you use GitLab CI, the equivalent uses rules:changes combined with a custom script for dependency-aware detection. Understanding these Git branching strategies helps determine the correct comparison base for accurate detection.
Handling shared package dependencies
PHP monorepos frequently contain shared libraries under packages/ that multiple apps depend on via Composer path repositories. A change to packages/auth should trigger builds for every app requiring it. Maintain a dependency map in a configuration file or parse composer.json files dynamically during detection. Tools like turborepo or nx automate this graph analysis, but for pure PHP setups without Node tooling, a simple shell script parsing composer requirements works reliably.
How do you structure matrix builds for multiple PHP applications?
Once you have identified affected applications, matrix builds allow parallel execution across all changed services. This is where CI CD for monorepo with multiple PHP apps diverges significantly from single-app pipelines. Each matrix entry must be self-contained yet able to leverage shared caching and common setup steps.
Configuring dynamic matrices in GitHub Actions
Static matrices list every app regardless of changes. Dynamic matrices consume the output from your detection job, spawning jobs only for affected applications. This requires the needs keyword and proper output passing between jobs.
# .github/workflows/build.yml
jobs:
build:
needs: detect
if: ${{ needs.detect.outputs.matrix != '{"app":[]}' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.detect.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: pdo_mysql, redis, bcmath
coverage: none
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: apps/${{ matrix.app }}/vendor
key: php-${{ matrix.app }}-${{ hashFiles(format('apps/{0}/composer.lock', matrix.app)) }}
restore-keys: php-${{ matrix.app }}-
- name: Install & Test
working-directory: apps/${{ matrix.app }}
run: |
composer install --prefer-dist --no-progress
php artisan test --parallel The fail-fast: false setting is critical for monorepos. One failing app should not cancel builds for unrelated services. Each job maintains its own cache scope using the app name in the cache key, preventing cross-contamination between applications with different dependency versions. For teams evaluating their automation platform, comparing GitHub Actions vs GitLab CI reveals meaningful differences in matrix flexibility and cache performance.
Isolating application contexts
A common mistake in PHP monorepo pipelines is running commands from the repository root. Always set working-directory explicitly or use cd before executing Composer and Artisan commands. Environment variables like APP_NAME and database credentials must be scoped per application, not globally. Store these as environment-specific secrets or use a centralized secrets manager referenced during the build.
How do you optimize caching and dependencies across PHP services?
Dependency installation dominates PHP build times. In a monorepo with five Laravel applications, naive composer install calls can consume 3–5 minutes per app even with warm caches. Optimizing this layer yields the largest time savings in CI CD for monorepo with multiple PHP apps.
- Hash-based cache keys: Key caches on
composer.lockhashes, not branch names. Lock files are deterministic; branches are not. A feature branch sharing identical dependencies with main should reuse the same cache entry. - Vendor directory caching: Cache the entire
vendor/directory rather than Composer's global cache. Restoring pre-installed dependencies skips extraction and autoloader generation entirely. - Platform optimization: Run
composer config platform.php 8.4.0in each app to prevent CI from resolving dependencies against the runner's exact PHP patch version, improving cache hit rates across minor updates. - Parallel installation: Use
--prefer-distto download archives instead of cloning repositories, and considercomposer install --no-scriptsfollowed by explicit script execution to separate network-bound and CPU-bound phases.
For Docker-based builds, multi-stage images with COPY instructions targeting specific composer.lock files enable layer caching independent of source code changes. This means dependency layers persist across application code commits, reducing image build times to seconds when only PHP source changes. Proper Docker containerization for Laravel establishes patterns that translate directly to monorepo optimization.
What deployment strategy works best for monorepo PHP applications?
Deployment in a monorepo context means selective promotion. You never deploy "the monorepo"; you deploy individual applications that happened to change. This requires artifact tagging that encodes both the application identity and the triggering commit, enabling traceability back to the exact source state.
| Strategy | Best For | Complexity | Rollback Speed | Cache Efficiency |
|---|---|---|---|---|
| Per-app Docker tags | Kubernetes / ECS deployments | Medium | Fast (tag revert) | High (layer reuse) |
| Unified version stamp | Atomic releases, compliance | Low | Medium (full rollback) | Medium |
| Git SHA tagging | Traceability, debugging | Low | Fast | High |
| Semantic per-app | Public APIs, client libs | High | Fast | Variable |
In my experience with Nepal-based fintech teams and global SaaS platforms, per-app Docker tagging with Git SHA suffixes provides the best balance. Images are tagged as customer-api:a1b2c3d rather than monorepo:v2.4.0. This allows independent rollbacks, clear audit trails, and efficient registry storage since unchanged apps retain their existing tags. For teams adopting GitOps, tools like ArgoCD or Flux can watch specific image repositories per application, triggering deployments only when that app's tag changes.
How do you handle testing isolation and quality gates?
Testing in a PHP monorepo requires balancing thoroughness with speed. Unit tests should always run for changed apps. Integration tests that touch shared databases or message queues need careful orchestration to avoid flaky cross-service interference. I recommend a tiered approach: fast unit tests in the matrix build, integration tests in a dedicated post-build stage that spins up ephemeral infrastructure, and end-to-end tests reserved for pre-production promotion.
Quality gates must be enforced per application, not globally. A failing test suite in the billing worker should block that app's deployment without preventing the customer API from shipping. Configure your CI system to report status checks per matrix entry. In GitHub Actions, this happens automatically with dynamic matrices. For SonarQube or similar tools, pass the app name as the project key to maintain separate quality profiles and historical trends.
Implementing Selective CI CD for Monorepo with Multiple PHP Apps
Building effective CI CD for monorepo with multiple PHP apps demands intentional design around change detection, parallel execution, and selective deployment. Start with accurate path filtering and dependency-aware triggering before optimizing cache layers and matrix configurations. Measure your pipeline's feedback cycle weekly; if developers wait more than ten minutes for PR checks, your change detection or caching needs refinement. The goal is a system where adding a new PHP application to the monorepo increases capability without proportionally increasing build time. If your current pipeline rebuilds everything on every push, implement the detection job described above as your first improvement. For teams ready to modernize their deployment workflow, explore zero-downtime deployment with Deployer to complement your optimized CI pipeline. Need help architecting a monorepo pipeline that fits your specific PHP stack and team size? Get in touch to discuss your infrastructure challenges.