PHP Setup Guide

Introduction

This guide will help you integrate OtterWise with your PHP project to track code coverage. OtterWise supports all major PHP testing frameworks and works seamlessly with popular frameworks like Laravel, Symfony, and WordPress.

Whether you're building web applications, APIs, command-line tools, or packages, this guide covers the setup for your specific use case.

Prerequisites

Before setting up OtterWise for your PHP project, you'll need:

  • A GitHub repository connected to OtterWise
  • PHP installed (version 8.1 or higher recommended)
  • A testing framework configured in your project
  • Xdebug or PCOV extension for code coverage collection
  • Your OtterWise repository token (found in your repository settings after enabling it in OtterWise)

Supported Coverage Formats

OtterWise supports the following coverage formats for PHP projects:

  • Clover XML - The most common format for PHP, supported by PHPUnit and Pest
  • Cobertura XML - Alternative XML format

We recommend using Clover XML format as it's the standard for PHP projects.

Coverage Drivers

PHP requires a coverage driver to collect code coverage data. The two main options are:

Xdebug

Xdebug is the most popular PHP debugging and profiling extension. It's feature-rich but can be slower for large test suites.

Install via PECL:

pecl install xdebug

Or on Ubuntu/Debian:

sudo apt-get install php-xdebug

PCOV

PCOV is a lightweight code coverage driver designed specifically for code coverage. It's much faster than Xdebug for coverage collection.

Install via PECL:

pecl install pcov

Recommendation

Use PCOV for CI environments as it's significantly faster. Use Xdebug for local development if you need debugging features.

Setup with PHPUnit

PHPUnit is the de facto standard testing framework for PHP.

Installation

Install PHPUnit via Composer:

composer require --dev phpunit/phpunit

Configuration

Create or update your phpunit.xml or phpunit.xml.dist file:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory suffix=".php">src</directory>
        </include>
    </source>

    <coverage>
        <report>
            <clover outputFile="build/logs/clover.xml"/>
        </report>
    </coverage>
</phpunit>

Running Tests with Coverage

Run PHPUnit with coverage:

# Using PCOV (recommended for CI)
php -d pcov.enabled=1 vendor/bin/phpunit --coverage-clover=build/logs/clover.xml

# Using Xdebug
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover=build/logs/clover.xml

# If configured in phpunit.xml
vendor/bin/phpunit

Uploading to OtterWise

After generating the coverage report, upload it to OtterWise using the bash uploader:

# Using environment variable
export OTTERWISE_TOKEN=your-repo-token
bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh)

# Or pass token directly
bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) --repo-token your-repo-token

Setup with Pest

Pest is an elegant PHP testing framework with a focus on simplicity. It's built on top of PHPUnit.

Installation

composer require --dev pestphp/pest --with-all-dependencies

Configuration

Pest uses the same phpunit.xml configuration as PHPUnit. Alternatively, you can specify coverage options directly:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory suffix=".php">app</directory>
            <directory suffix=".php">src</directory>
        </include>
        <exclude>
            <directory>app/Providers</directory>
        </exclude>
    </source>

    <coverage>
        <report>
            <clover outputFile="build/logs/clover.xml"/>
        </report>
    </coverage>
</phpunit>

Running Tests with Coverage

# Generate coverage report
vendor/bin/pest --coverage --coverage-clover=build/logs/clover.xml

# With minimum coverage enforcement
vendor/bin/pest --coverage --min=80

Framework-Specific Setup

Laravel

Laravel projects can use either PHPUnit or Pest. Laravel includes PHPUnit by default.

Using PHPUnit (default)

Laravel includes a phpunit.xml file by default. Update it to include coverage reporting:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory>app</directory>
        </include>
    </source>

    <coverage>
        <report>
            <clover outputFile="build/logs/clover.xml"/>
        </report>
    </coverage>

    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
    </php>
</phpunit>

Run tests:

# Using artisan command
php artisan test --coverage-clover=build/logs/clover.xml

# Or directly with PHPUnit
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover=build/logs/clover.xml
Using Pest

Install Pest for Laravel:

composer require --dev pestphp/pest --with-all-dependencies
composer require --dev pestphp/pest-plugin-laravel
php artisan pest:install

Run tests:

vendor/bin/pest --coverage --coverage-clover=build/logs/clover.xml

# With artisan (Laravel 10.48+)
php artisan test --coverage --coverage-clover=build/logs/clover.xml

Symfony

Symfony projects use PHPUnit with the Symfony PHPUnit Bridge.

Install dependencies:

composer require --dev symfony/test-pack
composer require --dev symfony/phpunit-bridge

Create or update phpunit.xml.dist:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="tests/bootstrap.php"
         colors="true">
    <php>
        <ini name="display_errors" value="1"/>
        <ini name="error_reporting" value="-1"/>
        <server name="APP_ENV" value="test" force="true"/>
        <server name="SHELL_VERBOSITY" value="-1"/>
        <server name="SYMFONY_PHPUNIT_REMOVE" value=""/>
        <server name="SYMFONY_PHPUNIT_VERSION" value="9.5"/>
    </php>

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory suffix=".php">src</directory>
        </include>
    </source>

    <coverage>
        <report>
            <clover outputFile="build/logs/clover.xml"/>
        </report>
    </coverage>
</phpunit>

Run tests:

# Using Symfony PHPUnit Bridge
XDEBUG_MODE=coverage php bin/phpunit --coverage-clover=build/logs/clover.xml

# Or with PCOV
php -d pcov.enabled=1 bin/phpunit --coverage-clover=build/logs/clover.xml

WordPress

WordPress plugins and themes can use PHPUnit with the WordPress test library.

Install dependencies:

composer require --dev phpunit/phpunit
composer require --dev yoast/phpunit-polyfills

Create phpunit.xml.dist:

<?xml version="1.0"?>
<phpunit
    bootstrap="tests/bootstrap.php"
    colors="true">
    <testsuites>
        <testsuite name="Plugin Tests">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory suffix=".php">includes</directory>
        </include>
    </source>

    <coverage>
        <report>
            <clover outputFile="build/logs/clover.xml"/>
        </report>
    </coverage>
</phpunit>

Run tests:

XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover=build/logs/clover.xml

CI Integration Examples

GitHub Actions

Here's a complete example for a PHP project using GitHub Actions with the official OtterWise action:

name: Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 2

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: 8.3
        coverage: pcov
        extensions: mbstring, pdo, pdo_mysql

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

    - name: Cache Composer packages
      uses: actions/cache@v3
      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 --no-progress

    - name: Run tests with coverage
      run: vendor/bin/phpunit --coverage-clover=build/logs/clover.xml

    - name: Upload Coverage to OtterWise
      uses: getOtterWise/github-action@v1
      with:
        token: ${{ secrets.OTTERWISE_TOKEN }}

Important

Remember to add your OTTERWISE_TOKEN to GitHub Secrets (Settings > Secrets and variables > Actions).

GitHub Actions with Laravel

name: Laravel Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 2

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.3'
        coverage: pcov
        extensions: mbstring, pdo, pdo_mysql

    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"

    - name: Install Dependencies
      run: composer install --prefer-dist --no-progress

    - name: Generate key
      run: php artisan key:generate

    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache

    - name: Run tests with coverage
      env:
        DB_CONNECTION: mysql
        DB_HOST: 127.0.0.1
        DB_PORT: 3306
        DB_DATABASE: test_db
        DB_USERNAME: root
        DB_PASSWORD: password
      run: php artisan test --coverage-clover=build/logs/clover.xml

    - name: Upload Coverage to OtterWise
      uses: getOtterWise/github-action@v1
      with:
        token: ${{ secrets.OTTERWISE_TOKEN }}

GitLab CI

test:
  image: php:8.3
  stage: test
  before_script:
    - apt-get update -yqq
    - apt-get install -yqq git libzip-dev
    - pecl install pcov
    - docker-php-ext-enable pcov
    - curl -sS https://getcomposer.org/installer | php
    - php composer.phar install
  script:
    - php -d pcov.enabled=1 vendor/bin/phpunit --coverage-clover=build/logs/clover.xml
    - bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) --repo-token $OTTERWISE_TOKEN
  coverage: '/^\s*Lines:\s*\d+\.\d+\%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: build/logs/clover.xml

CircleCI

version: 2.1

jobs:
  test:
    docker:
      - image: cimg/php:8.3
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "composer.lock" }}
      - run:
          name: Install Dependencies
          command: composer install --prefer-dist --no-progress
      - save_cache:
          paths:
            - ./vendor
          key: v1-dependencies-{{ checksum "composer.lock" }}
      - run:
          name: Install PCOV
          command: |
            sudo pecl install pcov
            sudo docker-php-ext-enable pcov
      - run:
          name: Run Tests with Coverage
          command: php -d pcov.enabled=1 vendor/bin/phpunit --coverage-clover=build/logs/clover.xml
      - run:
          name: Upload Coverage to OtterWise
          command: bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) --repo-token $OTTERWISE_TOKEN

workflows:
  version: 2
  test:
    jobs:
      - test

Quick Reference: Coverage Commands

Framework Command Output Location
PHPUnit vendor/bin/phpunit --coverage-clover=build/logs/clover.xml build/logs/clover.xml
Pest vendor/bin/pest --coverage-clover=build/logs/clover.xml build/logs/clover.xml
Laravel (Artisan) php artisan test --coverage-clover=build/logs/clover.xml build/logs/clover.xml
Laravel (Pest) vendor/bin/pest --coverage-clover=build/logs/clover.xml build/logs/clover.xml
Symfony php bin/phpunit --coverage-clover=build/logs/clover.xml build/logs/clover.xml

Using Composer Scripts

You can add convenient scripts to your composer.json for running tests with coverage:

{
    "scripts": {
        "test": "vendor/bin/phpunit",
        "test:coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover=build/logs/clover.xml",
        "test:coverage:html": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=build/coverage",
        "test:pcov": "php -d pcov.enabled=1 vendor/bin/phpunit --coverage-clover=build/logs/clover.xml"
    }
}

Then run with:

composer test:coverage

Troubleshooting

Common Issues and Solutions

Issue Solution
No coverage driver available
  • Install Xdebug: pecl install xdebug
  • Or install PCOV: pecl install pcov
  • Verify installation: php -m | grep -i pcov
  • Enable extension: php -d pcov.enabled=1
Coverage file not found
  • Verify the test command runs successfully
  • Check that build/logs/clover.xml exists after running tests
  • Use --file flag to specify custom location
  • Check directory permissions
Low or zero coverage
  • Ensure source files are included in phpunit.xml
  • Check that tests are actually running (not skipped)
  • Verify coverage driver is enabled
  • Check for namespace issues in test files
Tests are very slow
  • Switch from Xdebug to PCOV for coverage
  • Use --cache-result flag
  • Enable process isolation only when needed
  • Consider parallel test execution with ParaTest
Memory exhausted errors
  • Increase memory limit: php -d memory_limit=512M
  • Process isolation: --process-isolation
  • Use PCOV instead of Xdebug (lower memory usage)
  • Split large test suites into smaller groups

Debugging Coverage Generation

To verify your coverage is being generated correctly:

# Run coverage locally
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover=build/logs/clover.xml

# Check if the file exists
ls -la build/logs/clover.xml

# View the first few lines to verify format
head -20 build/logs/clover.xml

# Generate HTML report for visual inspection
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=build/coverage
open build/coverage/index.html

A valid Clover XML file should start like this:

<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="...">
  <project timestamp="...">
    <file name="/path/to/file.php">
      <line num="1" type="stmt" count="1"/>
      ...

Advanced Configuration

Parallel Test Execution

For faster test execution, use ParaTest:

composer require --dev brianium/paratest

# Run tests in parallel with coverage
vendor/bin/paratest --coverage-clover=build/logs/clover.xml -p4

Coverage Filtering

Exclude specific files or directories from coverage in phpunit.xml:

<source>
    <include>
        <directory suffix=".php">src</directory>
    </include>
    <exclude>
        <directory>src/Migrations</directory>
        <directory>src/Generated</directory>
        <file>src/bootstrap.php</file>
    </exclude>
</source>

Path Coverage

PHPUnit 10+ supports path coverage (more accurate than line coverage):

<coverage pathCoverage="true">
    <report>
        <clover outputFile="build/logs/clover.xml"/>
    </report>
</coverage>

Additional Upload Options

The OtterWise bash uploader supports several options for advanced use cases. See the Bash Uploader documentation for complete details.

Common options for PHP projects:

# Specify custom coverage file location
bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \
  --repo-token $OTTERWISE_TOKEN \
  --file ./build/logs/clover.xml

# Add a flag for test suite identification
bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \
  --repo-token $OTTERWISE_TOKEN \
  --flag unit-tests

# For monorepos - track different packages separately
bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \
  --repo-token $OTTERWISE_TOKEN \
  --file ./packages/core/build/logs/clover.xml \
  --flag core

Next Steps

Once you have coverage data uploading successfully, you can:

For additional help, check our example repository or contact OtterWise support.