Avoiding one-liners in PHP

PHP developers frequently share how they transform multiple lines of PHP code into one-liners. Through the eye of a maintainer, how do these one-liners compare to their multi-line counterparts? After all, writing PHP code is not a problem, but maintaining it is even more so.

Maintenance of PHP projects

With a safety net of automated tests, the maintenance of a PHP project is easy.

If you have that safety net, the readability of the production does not matter: you can always refactor it into something more or less readable without worrying about breaking it.

If you have that safety net, the syntactic sugar you use in the production code does not matter: you can always be more progressive or conservative, depending on the minimum version of PHP you support and the features you are comfortable using.

If you have that safety net, you could probably let ChatGPT generate your production code based on yiur automated tests and consider the production code a build artifact that you do not even bother checking into version control. Well, maybe we are not quite there yet!

But, if you have seen your fair share of PHP projects, you know that most of these, particularly closed-source PHP projects, suffer from a lack of tests.

Without a safety net of automated tests, the maintenance of a PHP project can quickly become a nightmare, and there is little reason to make it even more difficult for you.

But how good is your safety net of automated tests?

Effects of one-liners on code coverage

As a maintainer of a PHP project with less than 100% code coverage, you understand how deleting, adding, and formatting code can affect code coverage metrics.

Code coverage will decrease when

  • you delete tests
  • you reformat tested production code from more to fewer executable lines (one-liners, anyone?)
  • you add untested code
  • you reformat untested production code from fewer to more executable lines

Code coverage will increase when

  • you add tests
  • you reformat tested production code from fewer to more executable lines
  • you deleted untested production code
  • you reformat untested production code from more to fewer executable lines (one-liners, anyone?)

By using clever one-liners in your production code, you can gain misleadingly high confidence in your production code and your safety net of automated tests.

Collecting line, path, and branch code coverage

When you use PHPUnit to collect code coverage, PHPUnit will only collect line coverage by default. You could additionally collect line, branch, and path coverage, but as you can see in Collecting line, branch, and path coverage with PHPUnit, the collection of branch and path coverage comes at a cost.

Are the conditional one-liners in your PHP project worth these costs?

Instead of spending time, resources, and money to collect branch and path coverage when line coverage would suffice, why not avoid conditional one-liners in the first place?

Running mutation tests

When you use infection/infection to run mutation tests, you can combine your code coverage metrics with mutation score indicators to get a better idea of the quality of your test suites.

infection/infection will mutate your production code and run the tests against the mutations. If the tests pass after applying mutants, your tests are not good enough, and infection/infection will log the uncovered mutations.

But you do not maintain your PHP project in a log file, do you? You maintain your production code in an IDE.

Visualizing code coverage in PhpStorm

When you use PhpStorm and run tests with code coverage, PhpStorm will mark tested and untested lines of code as green and red, so you can immediately identify which lines of code require tests.

PhpStorm does not yet support branch coverage, so if you use conditional one-liners in your PHP project, PhpStorm does not show whether your tests cover both branches of a condition.

By using conditional one-liners in your PHP project, you are potentially hiding untested branches in your production code in plain sight.

Instead of hiding these untested parts of your production code in your IDE, why not avoid conditional one-liners in the first place?

Effects of one-liners on debugging

When you use PhpStorm and Xdebug to debug PHP code, you can set breakpoints to halt the execution, inspect variables in the current scope, and step through the program.

If you set breakpoints in your one-liners, where exactly is the execution going to halt? How will you understand where you are in the current execution, step into, over, and out of the current expression?

The widespread use of clever one-liners will make it rather difficult for you to use a step debugger.

Candidates for clever one-liners

Let's look at a few PHP language features that allow you to use one-liners instead of their multi-line counterparts (all of these are from the PHP website).

Ternary operators

The following example demonstrates the use of the ternary operator. Unfortunately, I do not know which PHP version introduced it, but 3v4l.org shows that the ternary operator works on PHP 4.3.0.

<?php

$username = isset($_GET['user']) ? $_GET['user'] : 'nobody';

The example above is equivalent to the following PHP code:

<?php

if (isset($_GET['user'])) {
    $username = $_GET['user'];
} else {
    $username = 'nobody';
}

The example above is also equivalent to the following PHP code:

<?php

$username = 'nobody';

if (isset($_GET['user'])) {
    $username = $_GET['user'];
}

While the use of isset() is debatable, the multi-line examples will immediately expose a lack of code coverage and allow you to set a breakpoint on a line with a single expression.

Short ternary operators

The following example demonstrates the use of the short ternary operator, introduced with PHP 5.3.0.

<?php

$result = $action ?: 'default';

The example above is equivalent to the following PHP code (see ternary operators):

<?php

$result = $action ? $action : 'default';

The example above is equivalent to the following PHP code:

<?php

if ($action) {
    $result = $action;
} else {
    $result = 'default';
}

The example above is equivalent to the following PHP code:

<?php

$result = 'default';

if ($action) {
    $result = $action;
}

While the use of non-boolean expressions in conditions is debatable, the multi-line examples will immediately expose a lack of code coverage and allow you to set a breakpoint on a line with a single expression.

Null-coalescing operators

The following example demonstrates the use of the null-coalescing operator, introduced with PHP 7.0.0.

<?php

$username = $_GET['user'] ?? 'nobody';

The example above is equivalent to the following PHP code (see ternary operators):

<?php

$username = isset($_GET['user']) ? $_GET['user'] : 'nobody';

The example above is equivalent to the following PHP code:

<?php

if (isset($_GET['user'])) {
    $username = $_GET['user'];
} else {
    $username = 'nobody';
}

The example above is also equivalent to the following PHP code:

<?php

$username = 'nobody';

if (isset($_GET['user'])) {
    $username = $_GET['user'];
}

Again, while the use of isset() is debatable, the multi-line examples will immediately expose a lack of code coverage and allow you to set a breakpoint on a line with a single expression.

Null-coalescing assignment operators

The following example demonstrates the use of the null-coalescing assignment operator, introduced with PHP 7.4.0.

<?php

$array['key'] ??= computeDefault();


The example above is equivalent to the following PHP code:

<?php

if (!isset($array['key'])) {
    $array['key'] = computeDefault();
}

Again, while the use of isset() is debatable, the multi-line example will immediately expose a lack of code coverage and allow you to set a breakpoint on a line with a single expression.

Arrow functions

The following example demonstrates the use of arrow functions, introduced with PHP 7.4.0.

<?php

$factor = 10;

$nums = array_map(fn($n) => $n * $factor, [1, 2, 3, 4]);

The example above is equivalent to the following PHP code:

<?php

$factor = 10;

$nums = array_map(function ($n) use ($factor) {
   return $n * $factor;
}, [1, 2, 3, 4]);

While the lack of type and return type declarations is debatable, the multi-line example will allow you to set a breakpoint on a line with a single expression. Also, the arrow function only implicitly captures the variable $factor. At the same time, the multi-line example requires you to explicitly declare which variables you want to import into the scope of the closure. Explicit is better than implicit.

Null-safe operator

The following example demonstrates the use of the null-safe operator, introduced with PHP 8.0.0.

<?php

$result = $repository?->getUser(5)?->name;

The example above is equivalent to the following PHP code:

<?php

if (is_null($repository)) {
    $result = null;
} else {
    $user = $repository->getUser(5);

    if (is_null($user)) {
        $result = null;
    } else {
        $result = $user->name;
    }
}

The example above is equivalent to the following PHP code:

<?php

$result = null;

if ($repository !== null) {
    $user = $repository->getUser(5);

    if ($user !== null) {
        $result = $user->name;
    }
}

While the use of is_null() is debatable, the multi-line examples will immediately expose a lack of code coverage and allow you to set a breakpoint on a line with a single expression.

Throw expressions

The following example demonstrates the use of the throw expression, introduced with PHP 8.0.0.

<?php

function test() {
    do_something_risky() or throw new Exception('It did not work');
}

The example above is equivalent to the following PHP code:

<?php

function test() {
    if (!do_something_risky()) {
        throw new Exception('It did not work');
    }
}

The example above is equivalent to the following PHP code:

<?php

function test() {
    $result = do_something_risky();

    if (!$result) {
        throw new Exception('It did not work');
    }
}

While the use of non-boolean expressions in conditions is debatable, the multi-line examples will immediately expose a lack of code coverage and allow you to set a breakpoint on a line with a single expression.

Recommendation

Avoid clever one-liners or use them with care. Focus on writing code that solves real problems, is well-tested, and is easy to maintain. The developers who follow your path will thank you!

Do you find this article helpful?

Do you have feedback?

Do you need help with your PHP project?