How to Set Up Jenkins for PHP Projects

How to Set Up Jenkins for PHP Projects

PHP CI/CD gets less blog coverage than its Java or Node counterparts, which is odd given how much of the web still runs on it. When I set up Jenkins for a legacy Laravel monolith at a previous job, most of the pain wasn’t Jenkins itself – it was getting PHP’s dependency and testing tooling to behave predictably inside CI. Once that was sorted, the pipeline itself was refreshingly simple. Here’s the complete setup, from a bare Jenkins install to a full Composer-Test-Deploy pipeline.

Why Use Jenkins for PHP Projects

PHP projects benefit from CI/CD the same way any codebase does, but a few things matter more here specifically:

Step 1: Prerequisites on the Jenkins Agent

Your Jenkins agent needs PHP, Composer, and your testing tools installed. The cleanest way to guarantee consistency is to run builds inside a PHP Docker image rather than installing PHP directly on the agent host.

Step 2: Install Required Jenkins Plugins

From Manage JenkinsPlugins, install:

java -jar jenkins-cli.jar -s http://localhost:8080/ install-plugin git docker-workflow junit htmlpublisher warnings-ng -restart

Step 3: Write the Jenkinsfile

Here’s a full pipeline for a Laravel-style PHP project, covering dependency installation, static analysis, unit tests, and coverage reporting:

pipeline {
    agent {
        docker {
            image 'php:8.3-cli'
            args '-v composer-cache:/tmp/composer-cache'
        }
    }

    environment {
        COMPOSER_CACHE_DIR = '/tmp/composer-cache'
        APP_ENV = 'testing'
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/myorg/php-app.git'
            }
        }

        stage('Install Dependencies') {
            steps {
                sh 'curl -sS https://getcomposer.org/installer | php'
                sh 'php composer.phar install --no-interaction --prefer-dist --optimize-autoloader'
            }
        }

        stage('Static Analysis') {
            steps {
                sh 'vendor/bin/phpstan analyse app --level=5 --error-format=checkstyle > phpstan-report.xml || true'
            }
        }

        stage('Code Style Check') {
            steps {
                sh 'vendor/bin/phpcs --standard=PSR12 app --report=checkstyle --report-file=phpcs-report.xml || true'
            }
        }

        stage('Run Tests') {
            steps {
                sh 'cp .env.testing .env'
                sh 'php artisan key:generate'
                sh 'php artisan migrate --force'
                sh 'vendor/bin/phpunit --coverage-html coverage --log-junit junit-report.xml'
            }
        }
    }

    post {
        always {
            junit 'junit-report.xml'
            recordIssues tools: [phpStan(pattern: 'phpstan-report.xml'), checkStyle(pattern: 'phpcs-report.xml')]
            publishHTML(target: [
                reportDir: 'coverage',
                reportFiles: 'index.html',
                reportName: 'PHPUnit Coverage Report'
            ])
        }
    }
}

Step 4: Testing Against Multiple PHP Versions

For libraries that need to support several PHP versions, use a matrix build:

pipeline {
    agent none
    stages {
        stage('Test Matrix') {
            matrix {
                axes {
                    axis {
                        name 'PHP_VERSION'
                        values '8.1', '8.2', '8.3'
                    }
                }
                stages {
                    stage('Test') {
                        agent {
                            docker { image "php:${PHP_VERSION}-cli" }
                        }
                        steps {
                            sh 'curl -sS https://getcomposer.org/installer | php'
                            sh 'php composer.phar install --no-interaction'
                            sh 'vendor/bin/phpunit'
                        }
                    }
                }
            }
        }
    }
}

Step 5: Handling a Database in Tests

Most real PHP applications need a database for integration tests. Use Docker Compose or Jenkins’ docker agent with a linked service container:

pipeline {
    agent {
        docker {
            image 'php:8.3-cli'
            args '--network=ci-net'
        }
    }
    stages {
        stage('Start MySQL') {
            steps {
                sh 'docker run -d --name mysql-test --network=ci-net -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=app_test mysql:8.0'
                sh 'sleep 15'
            }
        }
        stage('Run Tests') {
            steps {
                sh 'DB_HOST=mysql-test DB_DATABASE=app_test vendor/bin/phpunit'
            }
        }
    }
    post {
        always {
            sh 'docker rm -f mysql-test || true'
        }
    }
}

Step 6: Deployment

A common PHP deployment pattern is rsync-based deployment to a traditional LAMP server, or building a Docker image for containerized environments.

Traditional server deployment (rsync + SSH):

stage('Deploy') {
    when { branch 'main' }
    steps {
        sshagent(['prod-ssh-key']) {
            sh '''
                rsync -avz --delete \
                  --exclude=.env \
                  --exclude=storage \
                  ./ deploy@prod-server:/var/www/app/
                ssh deploy@prod-server "cd /var/www/app && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan config:cache"
            '''
        }
    }
}

Containerized deployment:

stage('Build and Push Image') {
    when { branch 'main' }
    steps {
        sh 'docker build -t myregistry.com/php-app:${BUILD_NUMBER} .'
        sh 'docker push myregistry.com/php-app:${BUILD_NUMBER}'
    }
}

Integrating with the Wider Toolchain

Monitoring and Troubleshooting

Security Best Practices

Best Practices

FAQs

Does Jenkins support PHPUnit output natively? Yes, PHPUnit can output JUnit-format XML (--log-junit), which the standard JUnit Jenkins plugin reads directly without any special PHP-specific plugin.

Can I run WordPress plugin/theme tests in this same setup? Yes, using wp-cli and the WordPress test suite bootstrap inside your Docker image, following largely the same pattern shown above.

How do I handle Composer private repositories/packages? Store the Composer auth token (e.g., for a private Packagist or GitHub token) as a Jenkins Credential and export it as COMPOSER_AUTH before running composer install.

Is Xdebug needed for coverage reports? Yes, or alternatively PCOV, which is faster and purpose-built for coverage collection; install whichever extension your PHP Docker image doesn’t already include.

Can this pipeline support zero-downtime deployments? Yes, using a symlink-based release strategy (deploy to a timestamped directory, then atomically switch a current symlink), which is a common pattern for traditional PHP hosting.

Summary

Setting up Jenkins for PHP is mostly about getting Composer, PHPUnit, and static analysis tools running consistently inside a containerized PHP environment, then wiring their output into Jenkins’ native reporting via JUnit XML and the Warnings Next Generation plugin. From there, deployment can go either the classic rsync/SSH route for traditional hosting or a full Docker image build for containerized environments. Once this pipeline is in place, PHP’s reputation for “hard to test in CI” mostly disappears – it becomes just another well-behaved pipeline in your Jenkins setup.

References

Exit mobile version