Friday, 17 January 2025

Developing a PHP package, 2025 edition

Since my blog post about the anatomy of a dope PHP package repository a lot has changed in the PHP ecosystem and it's surrounding tools. Time to reflect these changes and provide, yet another, guide to develop a PHP package in 2025.

Idea to implementation

The hardest part to get started is finding an idea worth implementing. Sources of inspiration can be visiting conferences, visiting local user groups, or the good old blog post reading. Also, daily business might generate an idea worth investing time. The idea might be very niche at first but that shouldn't stop you from going for it; the learnings might advance your self-confidence and career.

Getting started

Template repositories are a GitHub feature released in June 2019, which allows you and others to generate new Git repositories with the same directory structure and files based on the given template repository. Some templates to look into are spatie/package-skeleton-php and ergebnis/php-package-template. If you're developing a dedicated Laravel package spatie/package-skeleton-laravel is a good starting point. For checking if a package is PDS conform, there are for one the command-line tools of the pds/skeleton project or the package analyser which is a very opinionated validator/analyser.

Must have development tools

Some of the above-mentioned package templates come with preconfigured development tools like PHPUnit or the PHP Coding Standards Fixer. Other tools you might have to add or switch yourself. For example when you're developing a Laravel package you might want to replace PHPUnit with the traction gaining Pest testing framework and the PHP Coding Standards Fixer with Pint. For aiding you in development, tools like PHPStan and Rector have become quite mandatory. Also, worth checking out is their thriving extension ecosystem e.g. Larastan and rector-laravel. For the CI part, GitHub Actions have replaced former CI environments like Travis CI. So it might be a good invest to look into the usage and definition of GitHub Actions.

The automation of dependency updates for dependant Composer packages can currently be handled via Dependabot but might soon be replaced by Conductor.

Identifying wording or grammar mistakes in repositories might become easier once we can add Peck, a wrapper around GNU Aspell, into the mix.

To practice Datensparsamkeit you can add the lean-package-validator, which ensures that no unnecessary package artifacts end up in the distributed dist archives. For some additional development tools, it's also worth checking out Tomas Votruba's tools selection.

Registering your package

After having finished the implementation of the package you need to Git tag it, ideally following semantic versioning, and register it at Packagist, the main PHP package repository.

Package promotion

To promote your package start with a care- and heartful crafted README.md file. If you want to rise the visibility of your package invest some time to create a catching logo or delegate its creation to your bubble. Other channels to promote your work are X, the awesome PHP list, and giving a presentation about it at your local user group.

Thursday, 23 November 2023

Automating the backslash prefixing for native PHP function calls

After reading the blog post Why does a backslash prefix improve PHP function call performance by Jeroen Deviaene I was looking for a way to automate it for the codebase of the Lean Package Validator, to shave off some miliseconds for it's CLI. The PHP Coding Standards Fixer has a rule named native_function_invocation which does the very exact task.

Configuring the PHP Coding Standards Fixer

.php-cs-fixer.php
<?php

use PhpCsFixer\Config;
use PhpCsFixer\Finder;

$finder = Finder::create()
    ->in([__DIR__, __DIR__ . DIRECTORY_SEPARATOR . 'tests']);

$rules = [
    'psr_autoloading' => false,
    '@PSR2' => true,
    'phpdoc_order' => true,
    'ordered_imports' => true,
    'native_function_invocation' => [
        'include' => ['@internal'],
        'exclude' => ['file_put_contents']
    ]
];

$cacheDir = \getenv('HOME') ? \getenv('HOME') : __DIR__;

$config = new Config();

return $config->setRules($rules)
    ->setFinder($finder)
    ->setCacheFile($cacheDir . '/.php-cs-fixer.cache');
To make this rule executeable I needed to add the --allow-risky=yes option to the PHP Coding Standards Fixer calls in the two dedicated Composer scripts shown next.

composer.json
"scripts": {
    "lpv:test": "phpunit",
    "lpv:test-with-coverage": "export XDEBUG_MODE=coverage && phpunit --coverage-html coverage-reports",
    "lpv:cs-fix": "php-cs-fixer --allow-risky=yes fix . -vv || true",
    "lpv:cs-lint": "php-cs-fixer fix --diff --stop-on-violation --verbose --dry-run --allow-risky=yes",
    "lpv:configure-commit-template": "git config --add commit.template .gitmessage",
    "lpv:application-version-guard": "php bin/application-version --verify-tag-match=bin",
    "lpv:application-phar-version-guard": "php bin/application-version --verify-tag-match=phar",
    "lpv:static-analyse": "phpstan analyse --configuration phpstan.neon.dist", 
    "lpv:validate-gitattributes": "bin/lean-package-validator validate"
},
After running the lpv:cs-fix Composer script the first time the tests of the system under test started failing due to file_put_contents being prefixed with a backslash when using phpmock\MockBuilder's setName method, so I had to exclude it as shown in the PHP Coding Standards Fixer configuration above.

Monday, 15 January 2018

Documenting Composer scripts

For open source projects I'm involved with, I developed the habit to define, and document the steady growing amount of repository and build utilities via Composer scripts. Having Composer scripts available makes it trivial to define aliases or shortcuts for complex and hard to remember CLI calls. It also lowers the barrier for contributors to start using these tools while helping out with fixing bugs or providing new features. Finally they're also simplifying build scripts by stashing away complexity.

Defining Composer scripts

If you've already defined or worked with Composer scripts or even their npm equivalents you can skip this section, otherwise the next code snippet allows you to study how to define these. The here defined Composer scripts range from simple CLI commands with set options (e.g. the test-with-coverage script) to more complex build utility tools (i.e. the application-version-guard script) which are extracted into specific CLI commands to avoid cluttering up the composer.json or even the .travis.yml.

composer.json
{
  "scripts": {
    "test": "phpunit",
    "test-with-coverage": "phpunit --coverage-html coverage-reports",
    "cs-fix": "php-cs-fixer fix . -vv || true",
    "cs-lint": "php-cs-fixer fix --diff --stop-on-violation --verbose --dry-run",
    "configure-commit-template": "git config --add commit.template .gitmessage",
    "application-version-guard": "php bin/application-version --verify-tag-match"
  }
}

Describing Composer scripts

Since Composer 1.6.0 it's possible to set custom script descriptions via the scripts-descriptions element like shown next. It's to point out here that the name of a description has to match the name of a defined custom Composer script to be recognised at runtime. On another note it's to mention that the description should be worded in simple present to align with the other Composer command descriptions.

composer.json
{
  "scripts-descriptions": {
    "test": "Runs all tests.",
    "test-with-coverage": "Runs all tests and measures code coverage.",
    "cs-fix": "Fixes coding standard violations.",
    "cs-lint": "Checks for coding standard violations.",
    "configure-commit-template": "Configures a local commit message template.",
    "application-version-guard": "Checks that the application version matches the given Git tag."
  },
  "scripts": {
    "test": "phpunit",
    "test-with-coverage": "phpunit --coverage-html coverage-reports",
    "cs-fix": "php-cs-fixer fix . -vv || true",
    "cs-lint": "php-cs-fixer fix --diff --stop-on-violation --verbose --dry-run",
    "configure-commit-template": "git config --add commit.template .gitmessage",
    "application-version-guard": "php bin/application-version --verify-tag-match"
  }
}
Now when running $ composer via the terminal the descriptions of defined custom scripts will show up sorted in into the list of available commands, which makes it very hard to spot the Composer scripts of the package at hand. Luckily Composer scripts can also be namespaced.

Namespacing Composer scripts

To namespace (i.e. some-namespace) the custom Composer scripts for any given package define the script names with a namespace prefix as shown next. As the chances are very high that you will be using the one or other Composer script several times, while working on the package, it's recommended to use a short namespace like in the range from two to four characters.

composer.json
{
  "scripts": {
    "some-namespace:test": "phpunit",
    "some-namespace:test-with-coverage": "phpunit --coverage-html coverage-reports",
    "some-namespace:cs-fix": "php-cs-fixer fix . -vv || true",
    "some-namespace:cs-lint": "php-cs-fixer fix --diff --stop-on-violation --verbose --dry-run",
    "some-namespace:configure-commit-template": "git config --add commit.template .gitmessage",
    "some-namespace:application-version-guard": "php bin/application-version --verify-tag-match"
  }
}
Now this time when running $ composer via the terminal the defined custom scripts will show up in the list of available commands in a namespaced manner giving an immediate overview of the available Composer script of the package at hand.

$ composer
 ... ommitted content
Available commands:
  ... ommitted content
 some-namespace
  some-namespace:application-version-guard  Checks that the application version matches the given Git tag.
  some-namespace:configure-commit-template  Configures a local commit message template.
  some-namespace:cs-fix                     Fixes coding standard violations.
  some-namespace:cs-lint                    Checks for coding standard violations.
  some-namespace:test                       Runs all tests.
  some-namespace:test-with-coverage         Runs all tests and measures code coverage.
To use any namespaced Composer script, e.g. to fix coding standard violations after a substantial refactoring, it has to be called with its namespace e.g.$ composer some-namespace:cs-fix, which is the one disadavantage of Composer script namespacing.

Saturday, 25 March 2017

Keeping your CLI integration tests green on Windows

Lately on a Windows system, some failing integration tests for CLI commands utilising the Symfony Console component caused me some blip headaches by PHPUnit insisting that two strings are not identical due to different line endings. The following post documents the small steps I took to overcome these headaches.

First the assertion message produced by the failing test, see the console output below, got me thinking it might be caused by different encodings and line endings; though the project was utilising an .editorconfig from the early start and the related files were all encoded correctly and had the configured line endings. The Git configuration e.g. core.autocrlf=input also was as it should be.

1) Stolt\LeanPackage\Tests\Commands\InitCommandTest::createsExpectedDefaultLpvFile
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
 #Warning: Strings contain different line endings!
-Created default 'C:\Users\stolt\AppData\Local\Temp\lpv\.lpv' file.
+Created default 'C:\Users\stolt\AppData\Local\Temp\lpv\.lpv' file.
Another deeper look at the CommandTester class yielded that it’s possible to disable the command output decoration and also to normalise the command output. So a change of the SUT preparation and a normalisation of the console output, visualised via a git diff -U10, brought the solution for this particular test.

diff --git a/tests/Commands/InitCommandTest.php b/tests/Commands/InitCommandTest.php
index 58e7114..fb406f3 100644
--- a/tests/Commands/InitCommandTest.php
+++ b/tests/Commands/InitCommandTest.php
@@ -48,21 +48,21 @@ class InitCommandTest extends TestCase
     /**
      * @test
      */
     public function createsExpectedDefaultLpvFile()
     {
         $command = $this->application->find('init');
         $commandTester = new CommandTester($command);
         $commandTester->execute([
             'command' => $command->getName(),
             'directory' => WORKING_DIRECTORY,
-        ]);
+        ], ['decorated' => false]);

// ommitted  code

-        $this->assertSame($expectedDisplay, $commandTester->getDisplay());
+        $this->assertSame($expectedDisplay, $commandTester->getDisplay(true));
         $this->assertTrue($commandTester->getStatusCode() == 0);
         $this->assertFileExists($expectedDefaultLpvFile);
Since the SUT had a lot of integration test for its CLI commands, the lazy me took the shortcut to extend the CommandTester and using it, with desired defaults set, instead of changing all of the related command instantiations.

<?php

namespace SUT\Tests;

use Symfony\Component\Console\Tester\CommandTester as ConsoleCommandTester;

class CommandTester extends ConsoleCommandTester
{
    /**
     * Gets the display returned by the last execution of the command.
     *
     * @param bool $normalize Whether to normalize end of lines to \n or not
     *
     * @return string The display
     */
    public function getDisplay($normalize = true)
    {
        return parent::getDisplay($normalize);
    }

    /**
     * Executes the command.
     *
     * Available execution options:
     *
     *  * interactive: Sets the input interactive flag
     *  * decorated:   Sets the output decorated flag
     *  * verbosity:   Sets the output verbosity flag
     *
     * @param array $input   An array of command arguments and options
     * @param array $options An array of execution options
     *
     * @return int The command exit code
     */
    public function execute(
        array $input,
        array $options = ['decorated' => false]
    ) {
        return parent::execute($input, $options);
    }
}
So it's a yay for green CLI command integration tests on Windows from here on. Another measure for the SUT would be to enable Continuous Integration on a Windows system via AppVeyor, but that’s a task for another commit 85bdf22.

Monday, 10 October 2016

Eight knobs to adjust and improve your Travis CI builds

After having refactored several Travis CI configuration files over the last weeks, this post will provide eight adjustments or patterns immediately applicable for faster, changeable, and economic builds.

1. Reduce git clone depth

The first one is a simple configuration addition with a positive impact on the time and disk space consumption, which should be quite noticeable on larger code bases. Having this configured will enable shallow clones of the Git repository and reduce the clone depth from 50 to 2.

.travis.yml
git:
  depth: 2

2. Enable caching

The second one is also a simple configuration addition for caching the Composer dependencies of the system under build (SUB) or its result of static code analysis. Generally have a look if your used tools allow caching and if so cache away. This one deserves a shout out to @localheinz for teaching me about this one.

The next shown configuration excerpt assumes that you lint coding standard compliance with the PHP Coding Standards Fixer in version 2.0.0-alpha and have enable caching in its .php_cs configuration.

.travis.yml
cache:
  directories:
    - $HOME/.composer/cache
    - $HOME/.php-cs-fixer

3. Enforce contribution standards

This one might be a tad controversial, but after having had the joys of merging GitHub pull requests from a master branch I started to fail builds not coming from feature or topic branch with the next shown bash script. It's residing in an external bash script to avoid the risk of terminating the build process.

./bin/travis/fail-non-feature-topic-branch-pull-request
#!/bin/bash
set -e
if [[ $TRAVIS_PULL_REQUEST_BRANCH = master ]]; then
  echo "Please open pull request from a feature / topic branch.";
  exit 1;
fi

.travis.yml
script:
  - ./bin/travis/fail-non-feature-topic-branch-pull-request
The bash script could be extended to fail pull requests not following a branch naming scheme, e.g. feature- for feature additions or fix- for bug fixes, by evaluating the branch name. If this is a requirement for your builds you should also look into the blocklisting branches feature of Travis CI.

4. Configure PHP versions in an include

With configuring the PHP versions to build against in a matrix include it's much easier to inject enviroment variables and therewith configure the version specific build steps. You can even set multiple enviroment variables like done on the 7.0 version.

.travis.yml
env:
  global:
    - OPCODE_CACHE=apc

matrix:
  include:
    - php: hhvm
    - php: nightly
    - php: 7.1
    - php: 7.0
      env: DISABLE_XDEBUG=true LINT=true
    - php: 5.6
      env: 
      - DISABLE_XDEBUG=true

before_script:
  - if [[ $DISABLE_XDEBUG = true ]]; then
      phpenv config-rm xdebug.ini;
    fi
I don't know if enviroment variable injection is also possible with the minimalistic way to define the PHP versions list, so you should take that adjustment with a grain of salt.

It also seems like I stumbled upon a Travis CI bug where the global enviroment variable OPCODE_CACHE is lost, so add another grain of salt. To work around that possible bug the relevant configuration has to look like this, which sadly adds some duplication and might be unsuitable when dealing with a large amount of environment variables.

.travis.yml
matrix:
  include:
    - php: hhvm
      env: 
      - OPCODE_CACHE=apc
    - php: nightly
      env: 
      - OPCODE_CACHE=apc
    - php: 7.1
      env: 
      - OPCODE_CACHE=apc
    - php: 7.0
      env: OPCODE_CACHE=apc DISABLE_XDEBUG=true LINT=true
    - php: 5.6
      env: OPCODE_CACHE=apc DISABLE_XDEBUG=true

before_script:
  - if [[ $DISABLE_XDEBUG = true ]]; then
      phpenv config-rm xdebug.ini;
    fi

5. Only do static code analysis or code coverage measurement once

This one is for reducing the build duration and load by avoiding unnecessary build step repetition. It's achived by linting against coding standard violations or generating the code coverage for just a single PHP version per build, in most cases it will be the same for 5.6 or 7.0.

.travis.yml
matrix:
  include:
    - php: hhvm
    - php: nightly
    - php: 7.1
    - php: 7.0
      env: DISABLE_XDEBUG=true LINT=true
    - php: 5.6
      env: 
      - DISABLE_XDEBUG=true

script:
  - if [[ $LINT=true ]]; then
      composer cs-lint;
      composer test-test-with-coverage;
    fi

6. Only do release releated analysis and checks on tagged builds

This one is also for reducing the build duration and load by targeting the analysis and checks on tagged release builds. For example if you want to ensure that your CLI binary version, the one produced via the --version option, matches the Git repository version tag run this check only on tagged builds.

.travis.yml
script:
  - if [[ ! -z "$TRAVIS_TAG" ]]; then
      composer application-version-guard;
    fi

7. Run integration tests on very xth build

For catching breaking changes in interfaces or API's beyond your control it makes sense do run integration tests against them once in a while, but not on every single build, like shown in the next Travis CI configuration excerpt which runs the integration tests on every 50th build.

.travis.yml
script:
  - if [[ $(( $TRAVIS_BUILD_NUMBER % 50 )) = 0 ]]; then
      composer test-all;
    else
      composer test;
    fi

8. Utilise Composer scripts

The last one is all about improving the readability of the Travis CI configuration by extracting command configurations i.e. options into dedicated Composer scripts. This way the commands are also available during your development activitives and not hidden away in the .travis.yml file.

composer.json
{
    "__comment": "omitted other configuration",
    "scripts": {
        "test": "phpunit",
        "test-with-coverage": "phpunit --coverage-html coverage-reports",
        "cs-fix": "php-cs-fixer fix . -vv || true",
        "cs-lint": "php-cs-fixer fix --diff --verbose --dry-run"
    }
}
To ensure you don't end up with an invalid Travis CI configuration, which might be accidently committed, you can use composer-travis-lint a simple Composer script linting the .travis.yml with the help of the Travis CI API.

Happy refactoring.