Want to get more articles like this one? Join my newsletter

In DevOps

GrumPHP: Your local continuous integration solution

Continuous integration is an essential part of any modern development workflow. You use it to build your projects and prepare them for deployment. But you also use it to check the quality of your code and run tests.

These steps ensure you (and your team if you work with one!) keep writing high-quality code. They also help catch potential issues before that code gets deployed. (No one likes deploying something and breaking production!)

But it’s one thing to understand the benefits of a continuous integration workflow. It’s another to set one up. If you’ve never done it, the entire thing can feel quite intimidating. (And even if you have, it can feel daunting to do it again!)

You have to configure the continuous integration platform. You need to get your tools to work on it. Odds are you’ll also want a test environment for the workflow to run. That’s a lot of potential pieces you might need to get working.

In many situations, this is often more work than it’s worth. You’d like to have the benefits of continuous integration. It’s just not worth the hassle of setting all that up on a continuous integration platform.

Well, that’s the reason I fell in love with GrumPHP! It gives you most of the benefits of continuous integration. But it just runs everything on your local machine, so there’s no need to configure a continuous integration workflow.

What is GrumPHP?

GrumPHP is a code quality enforcement tool. It leverages the git pre-commit hook to run specific tasks that you’ve configured. If any of the tasks fail, your commit will also fail.

As you can see above, GrumPHP is pretty explicit when you’ve failed any task! You also get the error output from the failed tasks. This will point you to the issues you need to fix in order for GrumPHP to let you commit your changes.

So I went ahead and fixed the composer error that GrumPHP pointed out. It’s now happy and lets the commit changes go through. Yay!

This feedback loop is the essence of what GrumPHP does for you and your development team. It basically yells at you (CARL!!!) whenever you or anyone doesn’t follow defined coding practices. You’re then forced to fix them before being able to work on something else.

What can GrumPHP check?

So I mentioned “defined coding practices” just now. That’s because, out of the box, GrumPHP doesn’t do anything. You have to configure each code quality check that you want it to perform. And GrumPHP has dozens of those!

It can run tests whether it’s with PHPUnit or some other testing framework. (It supports most of them.) It can run static code analyzers to detect problems in the code you’ve written. It can check for code style violations with PHP_CodeSniffer. You can even create your own!

This over abundance of choice might feel a bit overwhelming. But the beauty of GrumPHP is that you can start with something as small as just checking git commit messages. And then you slowly add more checks as you need them.

What are some useful checks to use with GrumPHP?

Ok, so what checks should you add when you’re ready to add more? There are just too many of them to go over them all. So we’ll focus on broader categories, and you can pick the checks that make sense for your project.

Testing

The obvious thing to use GrumPHP for is to run your test suite whenever you commit changes. The advantage of doing this is that it lets you catch potential problems right away. It could be a regression or it could just be a test you forgot to update.

Either way, it’s ok. None of us are perfect. But it’s better to catch mistakes like these as early as possible before they affect others.

Now, running a test suite whenever you commit code might not be realistic. The most obvious reason is if it takes a long time to run it. No one wants to wait 15 minutes whenever we commit a change. And, after all, that’s why we created these continuous integration workflows in the first place!

In scenarios like that, you can run a subset of your test suite as a sanity check. Maybe you could just run the unit tests and leave the rest for your CI workflow. But whatever you choose, some testing is better than none at all.

grumphp:
  tasks:
    phpunit:
      testsuite: grumphp

Above is a configuration example with PHPUnit. We have GrumPHP run a special test suite called grumphp. (A very original name!) And the test suite could be configured like this:

<testsuite name="grumphp">
    <directory suffix="Test.php">./tests</directory>
    <exclude>./tests/Feature</exclude>
    <exclude>./tests/Integration</exclude>
</testsuite>

It’ll basically run all the tests in the tests folders except the ones in the directories we excluded. Here the excluded folders are for the feature and integration tests. But you should try to keep as many tests as you’re willing to handle with each commit.

Code quality tools

The next useful group of checks centers on code quality tools. Code quality tools are a different beast than testing. There isn’t one tool that dominates the entire PHP ecosystem.

Instead, there are a wide variety of tools that perform a wide range of code quality checks. Some are check for very specific things while others do a much broader analysis of your code. So you can really pick and choose what you want to have GrumPHP check.

Here are some code quality tools that I use for almost all my projects:

  • PHP Copy/Paste Detector does exactly as its name implies. It checks for duplicate code you might have been copying around. This helps keep my code DRY.
  • PHP Coding Standards Fixer is my favourite tool for enforcing coding standards. Another popular tool like that one is PHP_CodeSniffer.
  • PHP Parallel Lint is a small linter that catches small embarrassing mistakes like a missing semi-colon.
  • PHP Mess Detector is another tool that looks for a range of code quality issues. It’s one of the few tools that does complexity analysis. This is the primary reason I use it in every project.
  • PHP Insights is another great code quality tool that can detect a wide range of issues. It’s quite new and it’s not supported by GrumPHP officially. But I like it so much that I wrote a small integration for it.

Static code analysis

Another subset of code quality tools are static code analysis tools. These are tools that will analyze your code without actually running it. They are very useful as an early bug detection mechanism. For example, they can detect type safety issues.

There are two popular PHP static analysis tools: PHPStan and Psalm. As far as I know, there’s no need to use both. I use PHPStan, but you can use whichever you prefer.

The important thing is to use one. And that’s even with WordPress, where static code analysis is a challenge. There are extensions for both PHPStan and Psalm to help do static code analysis with WordPress. So there’s really no reason to not try to get it to work, even if it’s only some subset of your code.

Commit messages

I mentioned to it earlier. GrumPHP can also validate commit messages. This is especially handy when working on a team. In that context, it can be useful that everyone follows a specific commit message format. GrumPHP can help enforce that.

This isn’t something I’d really thought about until I came across the conventional commits specification. But I’m a big fan of it now. GrumPHP makes supporting commit message standards like that one a breeze.

You can also just use it to prevent someone from writing long commit messages. By default, it’ll restrict the subject of a commit message to 60 characters. This can be handy to make sure that your git logs are easy to read.

You can also use regular expressions to ensure that your commit messages contain specific things like a bug ticket #. Or you might just want to enforce a specific way of writing it. (e.g. JIRA-) This is definitely handy in large teams where you might want to enforce standards like those.

Going over a sample config file

Now, this should give you an excellent overview of the tools that you can use with GrumPHP. Next, let’s look at a sample grumphp.yml configuration file. For this article, we’ll look at the one I use for the Ymir Laravel application.

grumphp:
  process_timeout: 120
  tasks:
    composer: ~
    git_commit_message:
      enforce_capitalized_subject: false
      max_subject_width: 72
      type_scope_conventions:
        - types:
          - build
          - ci
          - chore
          - docs
          - feat
          - fix
          - perf
          - refactor
          - revert
          - style
          - test
    phpcpd:
      directory: ['./app']
    phpcsfixer2:
      allow_risky: true
      config: '.php_cs'
    phpinsights: ~
    phplint: ~
    phpmd:
      ruleset: ['phpmd.xml']
      whitelist_patterns: ['/^app\//']
    phpstan:
      ignore_patterns: ['/^(?!app)/']
      level: max
      memory_limit: "-1"
    phpunit:
      always_execute: true
      testsuite: grumphp

services:
  Ymir\App\Tests\GrumPHP\PhpInsightsTask:
    arguments:
      - '@process_builder'
      - '@formatter.raw_process'
    tags:
      - {name: grumphp.task, task: phpinsights}

Process timeout

This a pretty large file with quite a few tasks. The first thing to point out is the process_timeout option. This option used to be more important before GrumPHP started running tasks in parallel.

But it’s still handy if you have a task that takes a long time like tests. With this Laravel application, the unit tests are fast. But I also run integration tests with a SQLite database and various external API calls.

These tests can take over 60 seconds. So, for that reason, I bumped the process_timeout option to 120 seconds. So far, no task has hit that new 2 minute limit.

Tasks

Next, let’s look at all the tasks that GrumPHP runs on each commit. Most of the tasks are for tools we discussed earlier in the article. The only one I haven’t mentioned is the composer task which just checks your composer.json file for errors and other issues. For example, it won’t let you commit if there’s a mismatch between the dependencies in your composer.json and composer.lock files.

The other noteworthy task configuration is the one for git_commit_message. This is the configuration that I use for following conventional commits specification. The type_scope_conventions option contains all the possible commit types. I also extended the max_subject_width so it matches the one for GitHub.

Outside of git_commit_message, most of the tasks have minimal configuration options in the grumphp.yml file. Instead, my preference is to use each tool’s dedicated configuration file to store their configuration options.

Also worth pointing out that the phpunit task runs a specific testsuite called grumphp. This goes back to what I mentioned about running tests on commits. The full Ymir test suite takes one hour to run. The grumphp is a smaller testsuite (It’s about 80% of all tests) that lets me get quick feedback if I broke something without taking forever.

Custom tasks

The next thing we want to go over in the grumphp.yml file is the services section. This is where you register the custom task that you want to use with GrumPHP. Here, I register the custom task that I created for PHP Insights.

Custom tasks require that you create a class for the task. The services section registers the class with the Symfony dependency injection container used by GrumPHP. Here’s what the class looks like:

namespace Tests\GrumPHP;

use GrumPHP\Runner\TaskResult;
use GrumPHP\Runner\TaskResultInterface;
use GrumPHP\Task\AbstractExternalTask;
use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PhpInsightsTask extends AbstractExternalTask
{
    /**
     * {@inheritdoc}
     */
    public static function getConfigurableOptions(): OptionsResolver
    {
        $resolver = new OptionsResolver();
        $resolver->setDefaults([
            'config-path' => null,
            'min-architecture' => 85,
            'min-complexity' => 70,
            'min-quality' => 90,
            'min-style' => 95,
        ]);

        $resolver->addAllowedTypes('config-path', ['null', 'string']);
        $resolver->addAllowedTypes('min-architecture', ['null', 'int']);
        $resolver->addAllowedTypes('min-complexity', ['null', 'int']);
        $resolver->addAllowedTypes('min-quality', ['null', 'int']);
        $resolver->addAllowedTypes('min-style', ['null', 'int']);

        return $resolver;
    }

    /**
     * {@inheritdoc}
     */
    public function canRunInContext(ContextInterface $context): bool
    {
        return $context instanceof GitPreCommitContext || $context instanceof RunContext;
    }

    /**
     * {@inheritdoc}
     */
    public function run(ContextInterface $context): TaskResultInterface
    {
        $config = $this->getConfig()->getOptions();

        $arguments = $this->processBuilder->createArgumentsForCommand('php');
        $arguments->addSeparatedArgumentArray('artisan', ['insights', '--quiet', '--no-interaction']);
        $arguments->addOptionalArgument('--min-architecture=%s', $config['min-architecture']);
        $arguments->addOptionalArgument('--min-complexity=%s', $config['min-complexity']);
        $arguments->addOptionalArgument('--min-quality=%s', $config['min-quality']);
        $arguments->addOptionalArgument('--min-style=%s', $config['min-style']);

        $process = $this->processBuilder->buildProcess($arguments);
        $process->run();

        if (!$process->isSuccessful()) {
            return TaskResult::createFailed($this, $context, $this->formatter->format($process));
        }

        return TaskResult::createPassed($this, $context);
    }
}

Combining GrumPHP with a continuous integration platform

As we’ve been seeing, GrumPHP is a great way to get started with continuous integration. But it’s normal that you might outgrow it and want to use a continuous integration platform. So to wrap this up, we’ll look at how you can transition to using one.

The good news is that you don’t have to throw out what you’ve built with GrumPHP to use a continuous integration platform. GrumPHP can also have custom test suites. These test suites let you run a subset of your GrumPHP tasks.

testsuites:
  code_quality:
    tasks:
      - phpcpd
      - phpcsfixer2
      - phpinsights
      - phplint
      - phpmd
      - phpstan

Above is the code_quality test suite I run with a continuous integration workflow. You’ll notice that there are no more testing tasks. It’s only tasks that were in the code quality and static code analysis categories that we discussed earlier.

To run the code_quality test suite, you just need to specify it when you manually run GrumPHP. If you’re running GrumPHP from the vendor directory, you’ll want to run vendor/bin/grumphp run --testsuite=code_quality. In a GitHub actions workflow step, it would look like this:

- name: Run code quality checks
  run: vendor/bin/grumphp run --testsuite=code_quality

What happens to the test suites that we removed? You could do another GrumPHP test suite for them. But I prefer to just run PHPUnit directly.

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

A fantastic PHP tool

When I discovered GrumPHP, it was pretty much love at first sight. Here’s a tool that lets me combine all my favourite code quality tools and enforce some consistency in my code. What isn’t there to love!?

But it grew in my eyes when I realized that it could be a bridge to the world of continuous integration. Building a continuous integration workflow can be daunting. GrumPHP is a lot more accessible because it just runs on your machine and the configuration is a lot simpler.

So if you’re looking to start using continuous integration, but find the whole thing intimidating. Take a look at GrumPHP. You won’t regret it.

Creative Commons License