Wrapping it up

Writing is a social activity: although often performed in solitude, all writers write to be read by an audience. If you have difficulties finding readers, you have failed - whether you write for print or web, fiction or non-fiction, or computer programs.

Line Length

Typographers of print and web agree that the hard limit for line length should be somewhere between 50 and 75 characters (including spaces). When lines are too short, readers will be too busy traveling back and forth between the beginning and end of the line. When lines are too long, readers will have difficulties finding the following line.

Computer programmers usually have a different understanding.

In 1920, IBM introduced rectangular punched cards with 80 columns per line. Little did they know that these 80 columns would determine the width of computer terminals for years to come.

Punched cards from IBM Deutschland

Punched cards (Ziffernkarten) from IBM Deutschland, with 80 columns each. Feel like you need a punched card? Send me an email with your mailing address and I will send you a punched card by snail mail.

In 2012, the PHP Framework Interop Group (PHP-FIG) accepted PSR-2, which recommended the following:

There MUST NOT be a hard limit on line length.

The soft limit on line length MUST be 120 characters; automated style checkers MUST warn but MUST NOT error at the soft limit.

Lines SHOULD NOT be longer than 80 characters; lines longer than that SHOULD be split into multiple subsequent lines of no more than 80 characters each.

There MUST NOT be trailing whitespace at the end of non-blank lines.

Blank lines MAY be added to improve readability and to indicate related blocks of code.

There MUST NOT be more than one statement per line.

In 2019, the PHP Framework Interop Group accepted PSR-12 as a replacement for PSR-2, which still recommends the same.

The popular development platform GitHub allows

  • 139 characters per line in the compare branches view before starting to wrap lines
  • 150 characters per line in the pull request view before starting horizontal scrolling
  • 154 characters per line in the file view before starting to wrap lines

But hey, these are only recommendations and external constraints, and you can buy ultra-wide monitors for very little money!

Ultra-wide monitor

An example of an ultra-wide monitor. I have never had one, and if I did, I am not sure I would be comfortable using its entire width for code. Instead, I am a big fan of splitting editor windows.

So why bother with line lengths?

Maintenance

While the compiler, the most frequent reader of the code, typically does not care whether a computer program is written in a single line or not, maintainers of the computer program will.

Like skimming an article, I tend to recognize patterns when reading code. If you have ever played Minesweeper for long enough, you know the feeling: you do not have to think hard where mines hide but recognize patterns on the board and flag them.

In my experience, complexity in code - the equivalent of mines in Minesweeper - tends to hide in long lines. So why not make it easier for maintainers and expose that complexity by unwrapping it into multiple lines?

Candidates

There are several candidates where authors of computer programs typically exceed line lengths:

Annotations

Annotations allow developers to configure meta-data for classes, methods, fields, and functions. However, they are not a native feature of PHP and require an annotation parser, such as the widely used doctrine/annotations package, to read the meta-data from the DocBlocks.

For example, when using the popular doctrine/orm package, a developer can add annotations to a class to declare it as an entity.

<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM;

/**
 * @ORM\Mapping\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User
{
    // ...
}

The annotation Doctrine\ORM\Mapping\Entity refers to a class with the same name. The class declares properties, and by adding the annotation here, the annotation reader creates an instance of that class where the $repositoryClass property has the value 'App\Repository\UserRepository'.

Apart from doctrine/orm, there are a lot of popular packages out there that allow developers to add annotations, and a lot of these annotations can have a lot of properties. So when developers add more and more annotations with more and more properties, these annotations can become hard to read, understand, and maintain.

When an annotation has more than one property, wrap properties so that every property is on a new line.

 <?php

 declare(strict_types=1);

 namespace App\Entity;

 use Doctrine\ORM;

 /**
  * @ORM\Mapping\Entity(repositoryClass="App\Repository\UserRepository")
- * @ORM\Mapping\Table(name="user", indexes={@ORM\Mapping\Index(name="email_idx", columns={"email"})})
+ * @ORM\Mapping\Table(
+ *     name="user",
+ *     indexes={
+ *         @ORM\Mapping\Index(
+ *             name="email_idx",
+ *             columns={
+ *                 "email",
+ *             },
+ *         ),
+ *     },
+ * )
  */
  class User
  {
      // ...

      /**
-      * @ORM\Mapping\Column(name="email", type="string")
+      * @ORM\Mapping\Column(
+      *     name="email",
+      *     type="string",
+      * )
       */
      private string $email;

      // ...
  }

Applying the rule has the following advantages:

  • You can immediately see the complexity of the annotations - no horizontal scrolling required.
  • When adding a new property to an existing annotation, it will be more obvious where to add it.
  • When adding a new value to an existing annotation, it will be more obvious where to add it.

💡 You might find it easier to wrap and indent annotations when enabling the doctrine_annotation_indentation fixer for friendsofphp/php-cs-fixer.

💡 You can quickly sort properties by name, for example, by using the Lines Sorter plugin for PhpStorm.

💡 You can quickly move the line under the cursor (or a block of selected lines) up or down by pressing Option + Shift + or Option + Shift + on macOS. Also see Move Line in the IntelliJ IDEA Guide.

Attributes

Like annotations, attributes allow configuring meta-data on classes, properties, functions, methods, parameters, and constants. They are a native feature of PHP since their introduction with PHP 8.0.

When an attribute has more than one property, wrap properties so that every property is on a new line.

<?php

 declare(strict_types=1);

 namespace App\Http;

 use Symfony\Component\HttpFoundation;
 use Symfony\Component\Routing;

 final class HealthAction
 {
-    #[Routing\Annotation\Route(path: '/health/', name: 'health', methods: [HttpFoundation\Request::METHOD_GET])]
+    #[Routing\Annotation\Route(
+        path: '/health/',
+        name: 'health',
+        methods: [
+            HttpFoundation\Request::METHOD_GET,
+        ],
+    )]
     public function __invoke(): HttpFoundation\JsonResponse
     {
         // ...
     }

Applying the rule has the following advantages:

  • You can immediately see the complexity of the attributes - no horizontal scrolling required.
  • When adding a new property to an existing attribute, it will be more obvious where to add it.
  • When adding a new value to an existing attribute, it will be more obvious where to add it.

💡 You can quickly sort properties by name, for example, by using the Lines Sorter plugin for PhpStorm.

💡 You can quickly move the line under the cursor (or a block of selected lines) up or down by pressing Option + Shift + or Option + Shift + on macOS. Also see Move Line in the IntelliJ IDEA Guide.

Array Initializations

When initializing a non-empty array, wrap elements so that each element is on a new line.

<?php

 declare(strict_types=1);

 $foo = [];

-$bar = ['baz'];
+$bar = [
+    'baz',
+];

-$baz = ['qux' => ['quux', 'quuz'], 'corge' => 'grault'];
+$baz = [
+    'qux' => [
+        'quux',
+        'quuz',
+    ],
+    'corge' => 'grault',
+];

Applying the rule has the following advantages:

  • You can immediately see the structure of the array - no horizontal scrolling required.
  • When an array is a list, and the order does not matter, you can easily sort elements by value.
  • When an array is a map, and the order does not matter, you can easily sort elements by key.
  • When adding a new element to a list or a map, it will be more obvious where to add it.

💡 You can quickly sort elements of a map by name and elements of a list by value, for example, by using the Lines Sorter plugin for PhpStorm.

💡 You can quickly move the line under the cursor (or a block of selected lines) up or down by pressing Option + Shift + or Option + Shift + on macOS. Also see Move Line in the IntelliJ IDEA Guide.

Conditions

When a condition has more than one predicate, wrap predicates so that each predicate is on a new line.

 <?php

 declare(strict_types=1);

-if (($booking->getStatus() === Booking::STATUS_FIXED
-        || ($booking->getStatus() === Booking::STATUS_PROMISED && ($booking->getPromisedUntil() > $now || ($buildingIndex && $booking->getSynchronizeExpired()))))
-    && $allocation->getCapabilityBegin() < $allocation->getCapabilityEnd()) {
+if (
+    (
+        $booking->getStatus() === Booking::STATUS_FIXED
+        || (
+            $booking->getStatus() === Booking::STATUS_PROMISED
+            && (
+                $booking->getPromisedUntil() > $now
+                || (
+                    $buildingIndex
+                    && $booking->getSynchronizeExpired()
+                )
+            )
+        )
+    )
+    && $allocation->getCapabilityBegin() < $allocation->getCapabilityEnd()
+) {
     // ...
 }

Applying the rule has the following advantages:

  • You can immediately see the complexity of the condition - no horizontal scrolling required.
  • You can more easily identify opportunities for improvement and refactoring - all of which you could have easily missed if all predicates were on the same line.
  • Are there too many predicates? Why?
  • Are the predicates using || and && combined correctly? Why not?
  • Should all cases be handled the same? Why?
  • Are there automated tests that assert that correct behaviour for all cases? Why not?
  • Can we reduce the cognitive load by breaking, continueing, or returning early? Why not?

Constructor Declarations

When a constructor declares more than one parameter, wrap parameters so that each parameter is on a new line.

<?php

 declare(strict_types=1);

 class Request
 {
     // ...

-    public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
+    public function __construct(
+        array $query = [],
+        array $request = [],
+        array $attributes = [],
+        array $cookies = [],
+        array $files = [],
+        array $server = [],
+        $content = null,
     ) {
         // ...
     }
 }

Applying the rule has the following advantages:

  • You can immediately see the number of constructor parameters along with their visibilities, readonly modifiers, and default values - no horizontal scrolling required.
  • You can more easily identify opportunities for improvement and refactoring - all of which you could have easily missed if all parameters were on the same line.
  • Does the constructor have too many parameters? Perhaps the class is doing too much?
  • When using constructor property promotion on PHP 8.0, do all parameters have visibilities? Why not?
  • When using readonly properties on PHP 8.1, do all parameters have readonly modifiers? Why not?
  • Do all parameters have type declarations? Why not?
  • Does the constructor have parameters with default values? Why?
  • Does the constructor have unused parameters? Why?
  • Do you keep seeing the same parameters used together again and again? Perhaps they belong together in a service or a value object?

Constructor Invocations

When a constructor invocation uses more than one argument, wrap arguments so that each argument is on a new line.

 <?php

 declare(strict_types=1);

 abstract class AbstractBrowser
 {
     // ...

     public function request(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], string $content = null, bool $changeHistory = true): Crawler
     {
         // ...

-        $this->internalRequest = new Request($uri, $method, $parameters, $files, $this->cookieJar->allValues($uri), $server, $content);
+        $this->internalRequest = new Request(
+            $uri,
+            $method,
+            $parameters,
+            $files,
+            $this->cookieJar->allValues($uri),
+            $server,
+            $content,
+        );

         // ...
     }
 }

Looking at the constructor declaration from the call site, applying the rule has similar advantages:

  • You can immediately see the number of constructor arguments - no horizontal scrolling required.
  • You can more easily identify opportunities for improvement and refactoring - all of which you could have easily missed if all arguments were on the same line.
  • Are there too many arguments? Perhaps the object is doing too much?
  • Are there more arguments than the constructor accepts? Why?
  • Are there arguments that are identical to the corresponding parameter default value? Why?
  • Do you keep seeing the same arguments used together again and again? Perhaps they belong together in a service or a value object?

Function Declarations

With exceptions, when a function declares more than one parameter, wrap parameters so that each parameter is on a new line.

 <?php

 declare(strict_types=1);

-function blog_get_headers($courseid = null, $groupid = null, $userid = null, $tagid = null, $tag = null, $modid = null, $entryid=null, $search = null) {
+function blog_get_headers(
+    $courseid = null,
+    $groupid = null,
+    $userid = null,
+    $tagid = null,
+    $tag = null,
+    $modid = null,
+    $entryid = null,
+    $search = null
+) {
     // ...
 }

 // ...

 $filter = static function (string $value): bool {
     return '' !== trim($value);
 };

 // ...

 $reducer = static function (array $carry, array $item): array {
     // ...
 });

 // ...

 $sorter = static function (string $a, string $b): int {
       // ...
 };

Applying the rule has the following advantages:

  • You can immediately see the number of parameters - no horizontal scrolling required.
  • You can more easily identify opportunities for improvement and refactoring - all of which you could have easily missed if all parameters were on the same line.
  • Does the function have too many parameters? Perhaps the function is doing too much?
  • Do all parameters have type declarations? Why not?
  • Does the function have parameters with default values? Why?
  • Does the function have unused parameters? Why?
  • Do you keep seeing the same parameters used together again and again? Perhaps they belong together in a service or a value object?
  • Does the function have a return type declaration? Why not?
  • Is the return type declaration nullable? Why?

Function Invocations

With exceptions, when a function invocation uses more than one argument, wrap arguments so that each argument is on a new line.

 <?php

 declare(strict_types=1);

 // userland function, n-ary
-$blogheaders = blog_get_headers($filters['courseid'], $filters['groupid'], $filters['userid'], $filters['tagid'], $filters['tag'], $filters['cmid'], $filters['entryid'], $filters['search']);
+$blogheaders = blog_get_headers(
+    $filters['courseid'],
+    $filters['groupid'],
+    $filters['userid'],
+    $filters['tagid'],
+    $filters['tag'],
+    $filters['cmid'],
+    $filters['entryid'],
+    $filters['search'],
+);

 // native function invocation, binary
 $parts = explode(', ', $list);

 // native function invocation, binary
 $list = implode(', [
     'Gin',
     'Tonic',
     'Lemons',
 ]);

 // native function invocation, binary, using anonymous function with one argument as first argument
 $mapped = array_map(static function (string $value): string {
     // ...
 }, $values);

 // native function invocation, binary, using anonymous function with one argument as second argument
 $filtered = array_filter($values, static function (string $value): bool {
     // ...
 });

 // native function invocation, binary, using anonymous function with two arguments as as second argument
 usort($values, static function (string $a, string $b): int {
     // ...
 });

 // native function invocation, ternary
 $reduced = array_reduce(
     $values,
     static function (array $carry, array $item): array {
         // ...
     },
     []
 );

 // native function invocation, variying arity
 $message = sprintf(
     'Value should be one of "%s", got "%s" instead.',
     implode('", "', $values),
     $value
 );

Looking at the function declaration from the call site, applying the rule has similar advantages:

  • You can immediately see the number of function arguments - no horizontal scrolling required.
  • You can more easily identify opportunities for improvement and refactoring - all of which you could have easily missed if all arguments were on the same line.
  • Are there too many arguments? Perhaps the function is doing too much?
  • Are there more arguments than the function accepts? Why?
  • Are there arguments that are identical to the corresponding parameter default value? Why?
  • Do you keep seeing the same arguments used together again and again? Perhaps they belong together in a service or a value object?

Method Declarations

When a method declares more than one parameter, wrap parameters so that each parameter is on a new line.

 <?php

 declare(strict_types=1);

 final class JWTCookieProvider
 {
     // ...

-    public function createCookie(string $jwt, ?string $name = null, $expiresAt = null, ?string $sameSite = null, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httpOnly = null, array $split = []): Cookie
-    {
+    public function createCookie(
+        string $jwt,
+        ?string $name = null,
+        $expiresAt = null,
+        ?string $sameSite = null,
+        ?string $path = null,
+        ?string $domain = null,
+        ?bool $secure = null,
+        ?bool $httpOnly = null,
+        array $split = [],
+    ): Cookie {
         // ...
     }

     // ...
 }

Similar to function declarations, applying this rule has the following advantages:

  • You can immediately see the number of parameters - no horizontal scrolling required.
  • You can more easily identify opportunities for improvement and refactoring - all of which you could have easily missed if all parameters were on the same line.
  • Does the method have too many parameters? Perhaps the method is doing too much?
  • Do all parameters have type declarations? Why not?
  • Does the method have parameters with default values? Why?
  • Does the method have unused parameters? Why?
  • Do you keep seeing the same parameters used together again and again? Perhaps they belong together in a service or a value object?
  • Does the method have a return type declaration? Why not?
  • Is the return type declaration nullable? Why?

Method Invocations

With exceptions, when a method invocation uses more than one argument, wrap arguments so that each argument is on a new line.

 <?php

 declare(strict_types=1);

 final class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface
 {
     // ...
     public function handleAuthenticationSuccess(UserInterface $user, ?string $jwt = null): Response
     {
         // ...

-        $jwtCookie = $this->cookieProvider->createCookie($jwt, null, $cookieLifetime, null);
+        $jwtCookie = $this->cookieProvider->createCookie(
+            $jwt,
+            null,
+            $cookieLifetime,
+            null,
+        );

         // ...
     }
 }

 final class ListAction
 {
     // ...

     public function __invoke(): HttpFoundation\Response
     {
         // ...

         $content = $this->twig->render('view/article/list.html.twig', [
             'articlesGroupedByYear' => $articlesGroupedByYear,
         ]);

         // ...
     }

     // ...
}

Looking at the method declaration from the call site, applying the rule has similar advantages:

  • You can immediately see the number of method arguments - no horizontal scrolling required.
  • You can more easily identify opportunities for improvement and refactoring - all of which you could have easily missed if all arguments were on the same line.
  • Are there too many arguments? Perhaps the method is doing too much?
  • Are there more arguments than the method accepts? Why?
  • Are there arguments that are identical to the corresponding parameter default value? Why?
  • Do you keep seeing the same arguments used together again and again? Perhaps they belong together in a service or a value object?

Do you find this article helpful?

Do you have feedback?

Do you need help with your PHP project?