Skip to main content
Caching avoids repeating expensive setup work on every workflow run. There are two main things worth caching in a PHP workflow: compiled PHP extensions and Composer’s package download cache.

Extension caching

Some PHP extensions take a long time to compile and install. You can cache them using the shivammathur/cache-extensions action together with actions/cache. When the cache is warm, extensions are enabled directly from the cache instead of being compiled again, which significantly reduces setup time.
jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.5'
          extensions: imagick, redis, swoole
          key: extensions-cache-v1

      - name: Cache extensions
        uses: actions/cache@v4
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.5'
          extensions: imagick, redis, swoole
The shivammathur/cache-extensions action must run before setup-php so that the cache is restored before PHP setup begins. Refer to the cache-extensions documentation for full configuration options.

Composer dependency caching

Composer maintains an internal download cache separate from the vendor directory. Caching this directory means packages are loaded from disk instead of being downloaded from Packagist on every run.

Basic setup with composer.lock

When you commit composer.lock, use its hash as the cache key. This ensures the cache is invalidated whenever your locked dependencies change.
steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Setup PHP
    uses: shivammathur/setup-php@v2
    with:
      php-version: '8.5'

  - name: Get composer cache directory
    id: composer-cache
    run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

  - name: Cache dependencies
    uses: actions/cache@v4
    with:
      path: ${{ steps.composer-cache.outputs.dir }}
      key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
      restore-keys: ${{ runner.os }}-composer-

  - name: Install dependencies
    run: composer install --prefer-dist

When there is no composer.lock

If you do not commit composer.lock (common for libraries), use the hash of composer.json instead:
- name: Cache dependencies
  uses: actions/cache@v4
  with:
    path: ${{ steps.composer-cache.outputs.dir }}
    key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
    restore-keys: ${{ runner.os }}-composer-

Using prefer-lowest and prefer-stable in cache keys

If your matrix tests both minimum and stable dependency versions, include the preference in your cache key so the two variants do not share a cache:
strategy:
  matrix:
    php-versions: ['8.2', '8.3', '8.4']
    prefer: ['prefer-lowest', 'prefer-stable']

steps:
  - name: Get composer cache directory
    id: composer-cache
    run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

  - name: Cache dependencies
    uses: actions/cache@v4
    with:
      path: ${{ steps.composer-cache.outputs.dir }}
      key: ${{ runner.os }}-composer-${{ matrix.prefer }}-${{ hashFiles('**/composer.lock') }}
      restore-keys: ${{ runner.os }}-composer-${{ matrix.prefer }}-

  - name: Install dependencies
    run: composer update --${{ matrix.prefer }} --prefer-dist
Do not cache the vendor directory using actions/cache. Caching vendor can cause subtle issues because generated files, platform-specific binaries, and autoloader state inside vendor are not portable across runners. Always cache Composer’s internal download cache (the path returned by composer config cache-files-dir) and run composer install to reconstruct vendor.

Complete workflow example

The following workflow combines PHP setup, extension caching, and Composer dependency caching:
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.operating-system }}
    strategy:
      matrix:
        operating-system: ['ubuntu-latest', 'windows-latest', 'macos-latest']
        php-versions: ['8.3', '8.4', '8.5']

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: ${{ matrix.php-versions }}
          extensions: mbstring, intl, redis
          key: extensions-cache-v1

      - name: Cache extensions
        uses: actions/cache@v4
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-versions }}
          extensions: mbstring, intl, redis
          coverage: none

      - name: Get composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --prefer-dist

      - name: Run tests
        run: vendor/bin/phpunit

Build docs developers (and LLMs) love