Documentation Standards (Doc Blocks)

In Drupal, documentation isn't an afterthought tacked onto the code — it's treated as part of it. The API docs on api.drupal.org are generated straight from the doc blocks living in the codebase, which is exactly why the coding standards are so specific about how those blocks get written. This chapter covers doc blocks for files, functions, classes, and hooks, along with inline comments and how deprecation gets handled.

1. General Doc Block Rules

A doc block is a comment that opens with /** and sits directly above the thing it documents, with no blank line separating the two.

  • Every line inside it wraps at 80 characters.
  • The first line is always a one-sentence summary, ending in a period, kept to a single line that never wraps. It says what the thing does, not how it does it.
  • For functions and methods specifically, that summary is third person singular and opens with a verb — "Builds…", "Returns…", "Checks…" — never "Build the form" or "This function builds…".
  • A blank comment line separates the summary from any longer description that follows, and another separates that description from the tag section.

2. File Doc Blocks

Any .php or .module file declaring more than one trivial function opens with a file doc block, sitting above the namespace declaration — or above the first line of code, in a procedural file. Its job is to describe what the file holds, not what any individual function does.

php
/**
 * Contains hook implementations for the Product Import module.
 */

If a file declares just one class, it doesn't need a separate file doc block of its own — the class doc block already covers that ground.

3. Function and Method Doc Blocks

After that one-line summary comes a blank comment line, then an optional longer description if one's needed, another blank line, and then the tags — always in the order @param, @return, @throws.

php
/**
 * Imports a batch of products from the remote API.
 *
 * Existing records are updated in place; records that no longer
 * appear in the feed are left untouched, not deleted.
 *
 * @param array $items
 *   The raw product records, keyed by external ID.
 * @param bool $overwrite
 *   Whether to overwrite locally edited fields.
 *
 * @return int
 *   The number of products that were created or updated.
 *
 * @throws \Drupal\product_import\Exception\ImportException
 *   Thrown when the feed cannot be parsed.
 */
public function importProducts(array $items, bool $overwrite = FALSE): int {
}

Each @param line opens with the parameter's type, then its variable name on that same line, with the description indented two spaces underneath. And a method returning nothing just skips @return altogether — it's never written out as @return void.

When a method is just fulfilling a parent class or interface contract and doesn't add anything worth documenting on its own, a one-line {@inheritdoc} block covers it:

php
/**
 * {@inheritdoc}
 */
public function label(): string {
}

4. Class and Interface Doc Blocks

A class doc block explains what the class is responsible for — not a rewording of its name. Interfaces get the same treatment, and every public method on an interface carries its own full doc block, since implementing classes will typically just point back to it with {@inheritdoc} rather than repeat the explanation.

php
/**
 * Imports product records from the external catalog API.
 */
class ProductImporter implements ProductImporterInterface {
}

5. Hook Implementation Doc Blocks

Hook implementations get a short, fixed-format summary that just names the hook, rather than re-explaining something the hook system already documents elsewhere:

php
// ❌ Incorrect: re-explains what hook_cron() already documents.
/**
 * Runs on every cron run to check the remote feed for new
 * products and imports them one at a time.
 */
function product_import_cron() {
}

// ✅ Correct: short, fixed-format summary that just names the hook.
/**
 * Implements hook_cron().
 */
function product_import_cron() {
}

/**
 * Implements hook_form_FORM_ID_alter() for the node edit form.
 */
function product_import_form_node_form_alter(&$form, FormStateInterface $form_state) {
}

The actual behavior lives in the hook's own documentation in core — there's no need to repeat it in every single implementation.

6. Inline Comments

Inline comments exist to explain why, not what — the code itself already shows what it's doing. They open with //, a single space, and a capital letter, and close with a period. A short one can sit right above the line it's talking about; a longer explanation earns its own paragraph-style block instead.

php
// ❌ Incorrect: restates what the code already shows.
// Remove duplicate values from the array.
$product_ids = array_unique($raw_ids);

// ✅ Correct: explains why, which the code alone doesn't show.
// The API returns duplicate rows for products listed in more than one
// category, so we de-duplicate before saving.
$product_ids = array_unique($raw_ids);

7. Deprecating Code

When something gets replaced but can't be ripped out right away, it's marked with an @deprecated tag in a fixed format — the version it was deprecated in, the version it's slated for removal in, and what to reach for instead.

php
/**
 * Gets the product's internal name.
 *
 * @deprecated in product_import:2.3.0 and is removed from
 *   product_import:3.0.0. Use
 *   \Drupal\product_import\Entity\Product::getDisplayName() instead.
 *
 * @see https://www.drupal.org/node/1234567
 */
public function getName(): string {
  @trigger_error('getName() is deprecated in product_import:2.3.0 and is removed from product_import:3.0.0. Use getDisplayName() instead. See https://www.drupal.org/node/1234567', E_USER_DEPRECATED);
  return $this->getDisplayName();
}

That @see line pointing to the change record is what actually lets people migrate — the one-line summary alone won't tell them the full story.

Latest update: 17.09.2026 16:40