=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- title: Eloquent Interactions Redux date: 2024-02-20 06:24:00 =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- Eight years ago, I published [0] an open source implementation of the command pattern [1] for Laravel called Eloquent Interactions [2]. 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`): ```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 [3], and then runs the unit tests (via PHPUnit [4]) 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`): ```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): ```yml 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: ```json { "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): ```json { "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: ```yml 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: ```php 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 [5] (officially `v1.0.0` because this is a major breaking change) and deployed it to Packagist [6] for my new pen pal's usage. By all accounts, it's working swimmingly out in the wild. --- [0]: http://flower.codes/2018/03/09/introducing-eloquent-interactions.html [1]: https://refactoring.guru/design-patterns/command [2]: https://github.com/zachflower/eloquent-interactions [3]: https://getcomposer.org/ [4]: https://phpunit.de/ [5]: https://github.com/zachflower/eloquent-interactions/releases/tag/v1.0.0 [6]: https://packagist.org/ --- >> This is post 032 of #100DaysToOffload EOF