In the fast-paced world of software development, Continuous Integration (CI) has become an essential practice for PHP developers. CI is a development approach that involves frequently integrating code changes into a shared repository, followed by automated testing and deployment. This process helps catch bugs early, improve code quality, and streamline the development workflow.

Understanding Continuous Integration

Continuous Integration is more than just a set of tools; it’s a development philosophy. The core idea is to integrate code changes frequently – ideally, several times a day. Each integration triggers automated builds and tests, providing rapid feedback to developers.

🔑 Key benefits of CI:

  • Early detection of bugs and integration issues
  • Improved code quality
  • Faster release cycles
  • Increased visibility and communication within the team

Setting Up a CI Pipeline for PHP Projects

Let’s dive into the practical aspects of setting up a CI pipeline for your PHP project. We’ll use GitHub Actions as our CI tool, but the concepts apply to other CI platforms as well.

Step 1: Creating a Simple PHP Project

First, let’s create a simple PHP project with a basic function and a corresponding test.

// src/Calculator.php
<?php

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

Now, let’s create a test for this function:

// tests/CalculatorTest.php
<?php

use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    public function testAdd()
    {
        $calculator = new Calculator();
        $this->assertEquals(4, $calculator->add(2, 2));
    }
}

Step 2: Setting Up GitHub Actions

Create a new file in your repository at .github/workflows/ci.yml:

name: PHP CI

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

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

    - name: Install Dependencies
      run: composer install

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

This workflow will run every time you push to the main branch or create a pull request. It sets up PHP, installs dependencies, and runs your PHPUnit tests.

Enhancing Your CI Pipeline

Now that we have a basic CI pipeline, let’s enhance it with more advanced features.

Code Style Checking

Maintaining consistent code style is crucial for project maintainability. Let’s add PHP CodeSniffer to our pipeline.

First, add PHP_CodeSniffer to your project:

composer require --dev squizlabs/php_codesniffer

Then, update your ci.yml file:

    - name: Check Coding Standards
      run: vendor/bin/phpcs --standard=PSR12 src tests

This step will check your code against the PSR-12 coding standard.

Static Analysis

Static analysis tools can help catch potential bugs and improve code quality. Let’s add PHPStan to our pipeline.

First, install PHPStan:

composer require --dev phpstan/phpstan

Then, add this step to your ci.yml:

    - name: Run Static Analysis
      run: vendor/bin/phpstan analyse src tests --level=5

This will run PHPStan at level 5 (out of 9) on your src and tests directories.

Automating Deployment

The final step in our CI pipeline is automating the deployment process. This typically involves deploying your application to a staging or production environment after all tests pass.

Here’s an example of how you might set up a deployment step using SSH:

    - name: Deploy to Staging
      if: github.ref == 'refs/heads/main' && github.event_name == 'push'
      env:
        PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
        HOST: ${{ secrets.HOST }}
        USER: ${{ secrets.USER }}
      run: |
        echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
        ssh -o StrictHostKeyChecking=no -i private_key ${USER}@${HOST} '
          cd /path/to/your/project &&
          git pull origin main &&
          composer install --no-dev --optimize-autoloader &&
          php artisan migrate --force
        '

This step will only run on pushes to the main branch. It uses SSH to connect to your server, pull the latest changes, install production dependencies, and run database migrations.

🔒 Note: Make sure to set up the necessary secrets (SERVER_SSH_KEY, HOST, USER) in your GitHub repository settings.

Best Practices for PHP CI

  1. Keep builds fast: Aim for CI builds that complete in under 10 minutes. This ensures quick feedback for developers.

  2. Test thoroughly: Include unit tests, integration tests, and end-to-end tests in your CI pipeline.

  3. Use parallel jobs: For larger projects, run different types of tests in parallel to speed up the overall build time.

  4. Monitor and optimize: Regularly review your CI pipeline for bottlenecks and optimize where possible.

  5. Secure your secrets: Never hardcode sensitive information in your CI configuration. Use environment variables and secrets management.

Real-World Example: Laravel CI Pipeline

Let’s look at a more comprehensive example for a Laravel project:

name: Laravel CI/CD

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

jobs:
  laravel-tests:
    runs-on: ubuntu-latest

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

    steps:
    - uses: actions/checkout@v2

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

    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist

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

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

    - name: Create Database
      run: |
        mkdir -p database
        touch database/database.sqlite

    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: sqlite
        DB_DATABASE: database/database.sqlite
      run: vendor/bin/phpunit

    - name: Execute static analysis via PHPStan
      run: vendor/bin/phpstan analyse

    - name: Check coding style
      run: vendor/bin/phpcs --standard=PSR12 app tests

    - name: Deploy to Production
      if: github.ref == 'refs/heads/main' && github.event_name == 'push'
      env:
        PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
        HOST: ${{ secrets.HOST }}
        USER: ${{ secrets.USER }}
      run: |
        echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
        ssh -o StrictHostKeyChecking=no -i private_key ${USER}@${HOST} '
          cd /path/to/your/laravel/project &&
          git pull origin main &&
          composer install --no-dev --optimize-autoloader &&
          php artisan migrate --force &&
          php artisan config:cache &&
          php artisan route:cache &&
          php artisan view:cache
        '

This pipeline sets up a MySQL service for testing, runs PHPUnit tests, performs static analysis with PHPStan, checks coding style with PHP_CodeSniffer, and deploys to production if all checks pass.

Conclusion

Implementing Continuous Integration in your PHP projects can significantly improve your development workflow. By automating testing and deployment, you can catch bugs earlier, maintain code quality, and deploy with confidence.

Remember, CI is not just about tools – it’s about fostering a culture of continuous improvement and collaboration in your development team. Start small, iterate, and gradually build up your CI pipeline to suit your project’s needs.

🚀 Happy coding, and may your builds always be green!