Cost and value of DocBlocks

Over the years I have added, updated, and removed a lot of DocBlocks. I have also suggested to add, update, or remove DocBlocks many times, as well as explained why I believe a DocBlock makes sense in one case, and doesn't in another. In order to have a document I can refer to if I need to explain my reasoning again, I'm writing this article - maybe it's of use for you as well.

As a user of products that run software, the products I like the most are those that can be used and explored without needing a lot of documentation. As a user of open-source software I have been surprised by excellent and up-to-date documentation, and disappointed by outdated or even entirely missing documentation. As a contributor to open-source software I have witnessed the struggles and challenges of projects to keep documentation up-to-date. As a software engineer working on closed-source software I understand that there are different documentation needs for open- and closed-source software.

Regardless of whether software is open- or closed-source software, its maintenance, and the maintenance of its documentation, incurs costs. Since resources are limited, it's in the interest of everyone involved to focus on activities that provide value.

DocBlocks can provide value as documentation that lives inline with the code: it can easily be understood by developers, and interpreted by tools. The information present in DocBlocks can guide developers on the what, how and why of a piece of software. However, the value of DocBlocks diminishes quickly when they only repeat information that is already present on the structural elements they intend to document. Worse, when not kept up-to-date, they can become misleading.

Personally, when it comes to DocBlocks, I follow two rules:

  1. Add a DocBlock when it adds value.
  2. Remove a DocBlock when it does not add value.

DocBlocks can be applied to a number of structural elements:

File

Frequently found in open-source software, file-level DocBlocks document copyright information, refer to a license document, and link to the source repository:

<?php

declare(strict_types=1);

/**
 * Copyright (c) 2017 Andreas Möller.
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 *
 * @see https://github.com/ergebnis/test-util
 */

 namespace Ergebnis\Test\Util;

So far I have not found a use for file-level DocBlocks in closed-source software.

However, I have worked on a closed-source project where dependencies had actually been checked in into version control. When introducing composer and replacing the checked-in dependencies with requirements, file-level DocBlocks proved to be extremely useful to identify some of these dependencies.

When using friendsofphp/php-cs-fixer, file-level DocBlocks similar to above can be easily added or replaced with a configuration similar to the following:

<?php

declare(strict_types=1);

$header = <<<EOF
Copyright (c) 2017 Andreas Möller

For the full copyright and license information, please view
the LICENSE file that was distributed with this source code.

@see https://github.com/ergebnis/test-util
EOF;

$config = PhpCsFixer\Config::create()->setRules([
    'header_comment' => [
        'commentType' => 'PHPDoc',
        'header' => $header,
        'location' => 'after_declare_strict',
        'separate' => 'both',
    ],
]);

File-level DocBlocks can easily be removed with a configuration similar to the following:

<?php

declare(strict_types=1);

$config = PhpCsFixer\Config::create()->setRules([
    'header_comment' => [
        'header' => '',
    ],
]);

Class

I have rarely found a need for class-level DocBlocks, and in particular, class-level DocBlocks such as

 <?php

 declare(strict_types=1);

-/**
- * Represents a user in the system.
- */
 final class User
 {
 }

do not add any value - so I remove them.

Oftentimes, a need for a description of a class is a smell. Sometimes the only thing that is required to enhance clarity is a name that reveals intent.

Property

Depending on how properties are initialized (for example, via constructor injection), some IDEs are capable of inferring their type - either from method-level DocBlocks or type declarations on constructors, or from initializations of properties within constructors. Adding DocBlocks for properties with @var tags assists with auto-completion when using an IDE, but also helps when reading the code elsewhere, so I always add DocBlocks to properties.

 <?php

 declare(strict_types=1);

 final class User
 {
+    /**
+     * @var UserId
+     */
     private $id;

+    /**
+     * @var Login
+     */
     private $login;
 }

It is worth mentioning that there have been a few RFCs related to typed properties

an on September 27, 2018, the PHP RFC: Typed Properties 2.0 was accepted!

That is, starting with PHP 7.4, a lot of DocBlocks on properties can be removed.

 <?php

 declare(strict_types=1);

 final class User
 {
-    /**
-     * @var UserId
-     */
-     private $id;
+     private UserId $id;

-    /**
-     * @var Login
-     */
-     private $login;
+     private Login $login;
 }

Descriptions on property-level DocBlocks such as

 <?php

 declare(strict_types=1);

 final class User
 {
     /**
-     * The identifier of the user.
-     *
      * @var UserId
      */
     private $id;

     /**
-     * The login of the user.
-     *
      * @var Login
      */
     private $login;
 }

do not add any value - so I remove them.

Again, a need for a description of a property is a smell.

Method

Method-level DocBlocks which only repeat information that is entirely present in type and return type declarations, such as

 <?php

 declare(strict_types=1);

 class User
 {
     /**
      * @var UserId
      */
     private $id;

     /**
      * @var Login
      */
     private $login;

-    /**
-     * @param UserId $id
-     * @param Login  $login
-     */
     public function __construct(
         UserId $id,
         Login $login
     ) {
         $this->id = $id;
         $this->login = $login;
     }

-    /**
-     * @return UserId
-     */
     public function id(): UserId
     {
         return $this->id;
     }

-    /**
-     * @return Login
-     */
     public function login(): Login
     {
         return $this->login;
     }
 }

do not add any value - so I remove them.

Again, oftentimes a need for a description of a method is a smell. Consider renaming instead to clarify the intent.

With the introduction of scalar and return type declarations in PHP 7.0, as well as with the introduction of nullable type and return declarations and void return type declarations in PHP 7.1, a lot of DocBlocks can easily be replaced by corresponding type declarations:

 <?php

 declare(strict_types=1);

 final class Login
 {
     /**
      * @var string
      */
     private $value;

-    /**
-     * @param string $value
-     */
-    public function __construct($value)
+    public function __construct(string $value)
     {
         $this->value = $value;
     }

-    /**
-     * @return string
-     */
-    public function __toString()
+    public function __toString(): string
     {
         return $this->value;
     }
 }

However, oftentimes not all of the required information is present in type and return type declarations. For example, when a method throws exceptions

 <?php

 declare(strict_types=1);

 final class Login
 {
     /**
      * @var string
      */
     private $value;

+    /**
+     * @param string $value
+     *
+     * @throws \InvalidArgumentException
+     */
     public function __construct(string $value)
     {
         if ('' === trim($login)) {
             throw new \InvalidArgumentException('Value cannot be an empty string');
         }

         $this->value = $value;
     }

     public function __toString(): string
     {
         return $this->value;
     }
 }

or when a method returns an array (of objects or scalars)

 <?php

 declare(strict_types=1);

 class UserRepository
 {
+    /**
+     * @return array<int, User>
+     */
     public function all(): array
     {
         // ...
     }
 }

then DocBlocks add value - so I add them.

When an interface is extracted (or an implementation of an interface added) and the method on the interface already has all of the information

+<?php
+
+declare(strict_types=1);
+
+interface UserRepository
+{
+    /**
+     * @return array<int, User>
+     */
+    public function all(): array
+}

then a DocBlock on the corresponding method in the implementations such as

 <?php

 declare(strict_types=1);

-class UserRepository
+final class DoctrineUserRepository implements UserRepository
 {
-    /**
-     * @return array<int, User>
-     */
     public function all(): array
     {
         // ...
     }
 }

doesn't add any value - so I remove them.

Similarly, when an abstract class is extracted (or extending a class) and implementing an abstract method (overriding a method) the method on the interface already has all of the information, then a DocBlock on the corresponding method in the child class doesn't add any value - so I remove them as well.

Variable

Sometimes return types of methods are not clear

 <?php

 declare(strict_types=1);

 use PHPUnit\Framework;

 final class DoctrineUserRepositoryTest extends Framework\TestCase
 {
     public function testByLoginReturnsUser(): void
     {
         // ...

+        /** @var User $user */
         $user = $this->fixtureFactory->get(User::class, [
             'login' => $login,
         ]);

         // ...
     }
 }

and a clarifying inline DocComment adds value - so I add them.

Generated Code

When creating files and classes in IDEs, they often add unnecessary DocBlocks. However, DocBlocks such as

 <?php

  declare(strict_types=1);

-/**
- * Created by PhpStorm.
- * User: localheinz
- * Date: 05.05.18
- * Time: 22:18.
- */

 final class User
 {
 }

or

 <?php

 declare(strict_types=1);

-/**
- * Class User
- */
 final class User
 {
-    /**
-     * User constructor.
-     *
-     * @param Uuid   $id
-     * @param string $login
-     */
     public function __construct(Uuid $id, string $login)
     {
         $this->id = $id;
         $this->login = $login;
     }
 }

do not add any value - so I remove them.

When using PhpStorm, the file and code templates used when creating files and classy constructs can be easily configured (and unnecessary comments removed).

When creating code with command-line tools, for example, when creating database migrations using doctrine/migrations, the generated code often contains helpful comments for getting started, such as

 <?php

 declare(strict_types=1);

 use Doctrine\DBAL\Migrations\AbstractMigration;
 use Doctrine\DBAL\Schema\Schema;

-/**
- * Auto-generated Migration: Please modify to your needs!
- */
 class Version20180426054008 extends AbstractMigration
 {
     public function up(Schema $schema)
     {
-        // this up() migration is auto-generated, please modify it to your needs
     }

     public function down(Schema $schema)
     {
-        // this down() migration is auto-generated, please modify it to your needs
     }
 }

but other than that, these DocBlocks (and additional comments) add no value - so I remove them.

When code is generated from external sources, or its structure is dictated by external sources which we do not control, for example, when generating Data Transfer Objects or similar from API definitions or documentation, then DocBlocks with descriptions may add value. Especially when there is control over the mechanisms that generate the code, then including descriptions adds little maintenance cost.

If you have different opinions, that is fine - just make sure you don't blindly add DocBlocks everywhere, but weigh costs against value.

Hope it helps!

Do you find this article helpful?

Do you need help with your PHP project?