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