..

Eloquent Interactions Redux

Eight years ago, I published an open source implementation of the command pattern for Laravel called Eloquent Interactions.

While my library found some use in my professional life, it has largely sat unused for the past six or so years... at least by me; but last week, that illusion was shattered when I received an email from a developer in Australia whose company was using the library pretty heavily, but my relative abandonment made it impossible for them to upgrade their system to the latest version of Laravel.

To be clear, this email was kindly and respectfully written, and the developer was asking if I had any recommendations for a replacement library he could use to unblock the version upgrade.

I told him I would do him one better and catch Eloquent Interactions up to support the latest version of Laravel. Realistically, it would be as straightforward as updating the dependencies, but because I haven't actually used Eloquent Interactions in years, I wasn't entirely sure if that would be enough (spoiler alert: it was).

Turns out, I wrote unit tests for my little library.

If you are a developer, and you aren't writing unit tests, you are either wasting time now or you are setting yourself up to waste time later.

In order to catch Eloquent Interactions up to support the latest versions of both Laravel and PHP, I decided to do a few things:

  • I migrated the continuous integration workflow to GitHub Actions from Travis-CI.
  • I updated support for PHP 7.2.5 through 8.
  • I updated support for Laravel to versions 7 through 10.
  • I built out a test matrix that runs the unit tests for every combination of PHP and Laravel versions.

Migrating from Travis-CI to GitHub Actions

This was a surprisingly simple move that really took the form of a simple config change. Here is the original Travis-CI config file (.travis.yml):

language: php

php:
  - '5.6'
  - '7.0'
  - '7.1'
  - '7.2'

sudo: false

before_install:
  - travis_retry composer self-update

install:
  - travis_retry composer install --no-interaction --prefer-dist

script: vendor/bin/phpunit

Essentially, what it does is installs all of the library's dependencies using Composer, and then runs the unit tests (via PHPUnit) for every version of PHP defined in the php array up above (in this case, versions 5.6 through 7.2). As you can see, PHPUnit and Composer do most of the heavy lifting here. No complex setups required.

But, like I said, I've been done with Travis-CI for quite a while, and the more external accounts I have to maintain to keep this library running, the more headache I'm inviting into my life. So the first thing I did was convert the Travis-CI config to a GitHub Actions config (placed in .github/workflows/main.yml):

name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        php-versions: ['5.6', '7.0', '7.1', '7.2']

    steps:
    - uses: actions/checkout@v2

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: $
        coverage: none

    - name: Validate composer.json and composer.lock
      run: composer validate

    - name: Install dependencies
      run: composer install --prefer-dist --no-progress --no-suggest

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

While this script is just a tiny bit longer, it essentially does the same thing. The only exception is that you have to explicitly checkout the repository code and setup the PHP support. Otherwise it will run the same unit tests on every branch push and pull request for PHP versions 5.6 through 7.2.

Upgrading PHP Version Support

PHP 5.6 has been in end-of-life for five years now. Nobody should be using it. Hell, all versions of PHP 7, and even PHP 8.0 are EOL as well. While I could have excluded all end-of-lifed versions of PHP, I decided to support and test the minimum versions of PHP supported by Laravel 7 (which I decided was to be the most backwards-compatible version Eloquent Interactions would support).

Doing a little research, this landed me on PHP 7.2.5. So the next thing I did was update my test matrix to use PHP versions 7.2.5 through 8.3 (the current version of PHP):

name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        php-versions: ['7.2.5', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']

    steps:
    - uses: actions/checkout@v2

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: $
        coverage: none

    - name: Validate composer.json and composer.lock
      run: composer validate

    - name: Install dependencies
      run: composer install --prefer-dist --no-progress --no-suggest

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

We're not quite done yet, though. We also have to update the project's composer.json file to indicate that those versions of PHP are supported:

{
    "name": "zachflower/eloquent-interactions",
    "description": "An implementation of the interactor pattern for Laravel.",
    "keywords": ["interactor", "laravel", "library", "eloquent"],
    "license": "MIT",
    "authors": [
        {
            "name": "Zachary Flower",
            "email": "zach@zacharyflower.com"
        }
    ],
    "require": {
        "php": "^7.2.5|^8.0",
        "illuminate/validation": "~5.3|~5.4|~5.5|~5.6",
        "illuminate/support": "~5.3|~5.4|~5.5|~5.6",
        "illuminate/console": "~5.3|~5.4|~5.5|~5.6"
    },
    "require-dev": {
        "mockery/mockery": "~1.0",
        "phpunit/phpunit": "~5.0|~6.0|~7.0",
        "orchestra/testbench": "~3.0"
    },
    "autoload": {
        "psr-4": {
            "ZachFlower\\EloquentInteractions\\": "src/EloquentInteractions"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "ZachFlower\\EloquentInteractions\\Tests\\": "tests/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "ZachFlower\\EloquentInteractions\\EloquentInteractionsServiceProvider"
            ]
        }
    }
}

This is pretty straightforward. What we needed to do to indicate support for the new PHP versions is update the php directive in the require object to ^7.2.5|^8.0. In other words, we told Composer that the minimum versions of PHP that are supported are everything above just before to the version with the next breaking change (which is what the ^ symbol indicates).

Upgrading Laravel Version Support

Alright, well, we're halfway there. I didn't feel like testing whether or not Laravel 5 properly ran on PHP 8.4, so I decided to go ahead and upgrade both Laravel and its dependencies. Like I said above, I wanted to support as far back as Laravel 7, so I updated the illuminate/* dependencies (which are libraries provided by Laravel, and the only ones required for Eloquent Interactions to work):

{
    "name": "zachflower/eloquent-interactions",
    "description": "An implementation of the interactor pattern for Laravel.",
    "keywords": [
        "interactor",
        "laravel",
        "library",
        "eloquent"
    ],
    "license": "MIT",
    "authors": [
        {
            "name": "Zachary Flower",
            "email": "zach@zacharyflower.com"
        }
    ],
    "require": {
        "php": "^7.2.5|^8.0",
        "illuminate/validation": "^7.0|^8.0|^9.0|^10.0",
        "illuminate/support": "^7.0|^8.0|^9.0|^10.0",
        "illuminate/console": "^7.0|^8.0|^9.0|^10.0"
    },
    "require-dev": {
        "mockery/mockery": "^1.0",
        "phpunit/phpunit": "^8.0|^9.0",
        "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0",
        "dms/phpunit-arraysubset-asserts": "^0.5.0"
    },
    "autoload": {
        "psr-4": {
            "ZachFlower\\EloquentInteractions\\": "src/EloquentInteractions"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "ZachFlower\\EloquentInteractions\\Tests\\": "tests/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "ZachFlower\\EloquentInteractions\\EloquentInteractionsServiceProvider"
            ]
        }
    }
}

Now we're cooking with gas!

As you can see, I indicated support for all major versions of Laravel from 7 through 10. I also (not directly indicated here) updated the development dependencies (namely phpunit/phpunit and orchestra/testbench) to be compatible with the indicated Laravel versions.

As it stands, what this file now does is indicate which versions of which dependencies it can run on. If someone running Laravel 7 on PHP 7.3 installs Eloquent Interactions, I am signing off that it is supported.

No pressure, right?

Putting It All Together

Okay, so we have an updated Composer file, and a GitHub Actions runner. Now what?

Remember our PHP version matrix? Well, we're going to have to make that a bit more complicated. Because beyond just the PHP version, we need to also test the different Laravel versions supported by each version of PHP:

name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        include:
          - php-version: 7.2.5
            laravel-version: '7.*'
          - php-version: 7.3
            laravel-version: '7.*'
          - php-version: 7.3
            laravel-version: '8.*'
          - php-version: 7.4
            laravel-version: '7.*'
          - php-version: 7.4
            laravel-version: '8.*'
          - php-version: 8.0
            laravel-version: '8.*'
          - php-version: 8.0
            laravel-version: '9.*'
          - php-version: 8.1
            laravel-version: '8.*'
          - php-version: 8.1
            laravel-version: '9.*'
          - php-version: 8.1
            laravel-version: '10.*'
          - php-version: 8.2
            laravel-version: '8.*'
          - php-version: 8.2
            laravel-version: '9.*'
          - php-version: 8.2
            laravel-version: '10.*'
          - php-version: 8.3
            laravel-version: '8.*'
          - php-version: 8.3
            laravel-version: '9.*'
          - php-version: 8.3
            laravel-version: '10.*'

    steps:
    - uses: actions/checkout@v2

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: $
        coverage: none

    - name: Validate composer.json and composer.lock
      run: composer validate

    - name: Pin Laravel version
      run: composer require "illuminate/validation:$" --no-update

    - name: Install other dependencies
      run: composer install --prefer-dist --no-progress --no-suggest

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

Two things should stand out here:

  1. A number of PHP and Laravel version pairs have been created, allowing us to explicitly test different versions alongside one another.
  2. A new CI step, called Pin Laravel version explicitly installs a specific version of Laravel in order to install the other dependencies properly.

So, everything looks good (albeit a bit complex). Let's test it out:

There was 1 failure:

1) ZachFlower\EloquentInteractions\Tests\InteractionTest::testInvalidInput
Failed asserting that an array has the subset Array &0 (
    'meters' => Array &1 (
        0 => 'The meters field must be a number.'
    )
).
--- Expected
+++ Actual
@@ @@
 array (
   'meters' =>
   array (
-    0 => 'The meters field must be a number.',
+    0 => 'The meters must be a number.',
   ),
 )

Shit.

Looks like Laravel changed the validation messages at some point (which turned out to be for Laravel 10). Thankfully this is just in the tests (no actual library code is broken), so in order to get things working, all we need to do is update our tests to detect which Laravel version is being used and assert accordingly.

Here is an example of that change:

if (version_compare($this->app->version(), '10.0.0', '>=')) {
  $this->assertArraySubset(['meters' => ['The meters field must be a number.']], $outcome->errors->toArray());
} else {
  $this->assertArraySubset(['meters' => ['The meters must be a number.']], $outcome->errors->toArray());
}

Laravel (or, more specifically, the Laravel Orchestra test bench) very helpfully provides us with a way to check the underlying version, so all we have to do is check it and update our assertions accordingly.

And that's it! After that final tweak, everything built properly and all of the tests successfully ran, so I tagged the new version (officially v1.0.0 because this is a major breaking change) and deployed it to Packagist for my new pen pal's usage.

By all accounts, it's working swimmingly out in the wild.

--

If you like this post or one of my projects, you can buy me a coffee, or send me a note. I'd love to hear from you!

--

This is post 032 of #100DaysToOffload