Comments #
Comments are one of the easiest things to write in code — and also one of the easiest to write badly. There are two equally bad extremes: code with no comments at all, which leaves your teammates (or yourself three months from now) struggling to understand the intent, and code full of useless comments that merely repeat what’s already obvious from the code itself. PHP provides three comment syntaxes — //, #, and /* */ — plus the PHPDoc documentation format recognized by IDEs and automated documentation tools. This article covers how to use all three properly, when comments are genuinely needed, and how PHPDoc can make your PHP code far more professional and maintainable.
Types of PHP Comments #
PHP supports three comment syntaxes, each with its own appropriate use context.
flowchart TD
A[PHP Comments] --> B["Single-Line\n// or #"]
A --> C["Multi-Line\n/* ... */"]
A --> D["Documentation\n/** ... */\nPHPDoc"]
B --> E["Short explanation\nin the middle of code"]
C --> F["Long explanation\nblock or\ncommenting out code"]
D --> G["Documenting functions,\nclasses, properties\nfor IDEs & tools"]Single-Line Comments #
PHP recognizes two ways to write single-line comments: // and #. Both are functionally identical — the interpreter ignores everything from the marker to the end of the line.
<?php
// This is a single-line comment with a double slash
echo "Hello!";
# This is a single-line comment with a hash sign (shell style)
echo "World!";
$price = 150000; // Rp 150,000 — a comment can go at the end of a line of code
$discount = 0.1; # 10% discount
In modern PHP practice, // is far more common. The # sign comes from shell scripting conventions and is still valid in PHP, but it’s uncommon in contemporary PHP code. Use // as your consistent standard.
Multi-Line Comments #
Multi-line comments use the opening /* and closing */. Everything between them is ignored by the interpreter, no matter how many lines it spans.
<?php
/*
* This function calculates progressive discounts:
* - Purchases < 100,000 : no discount
* - Purchases 100k-500k : 5% discount
* - Purchases > 500,000 : 10% discount
*
* Note: the price received already includes VAT
*/
function calculateDiscount(float $price): float
{
if ($price >= 500000) {
return $price * 0.10;
}
if ($price >= 100000) {
return $price * 0.05;
}
return 0;
}
A common convention is to add a * at the start of every line inside a comment block (as in the example above). This isn’t a PHP requirement, but it makes comment blocks easier to read and has become a de facto standard in the PHP community.
Heredoc as a “Comment” for Long Strings #
One pattern sometimes used to insert long text without executing it is an unused heredoc, but this isn’t a real comment and should be avoided:
<?php
// ANTI-PATTERN: an unassigned heredoc is not a comment
<<<EOT
This is not a comment.
PHP will parse this text as a string expression,
then discard it because it's not assigned to a variable.
Wasteful and confusing.
EOT;
// CORRECT: use /* */ for long blocks of text you want to comment out
/*
This is a real comment.
The interpreter truly ignores this.
*/
When to Write Comments — and When Not To #
This is a more important question than “how to write comments”. Good comments explain why, not what. Good code should already explain what it does through clear variable names, functions, and structure.
Comments That Aren’t Needed #
<?php
// ANTI-PATTERN: comments that merely repeat the code
// Initialize the $total variable to 0
$total = 0;
// Loop through the $items array
foreach ($items as $item) {
// Add the item's price to the total
$total += $item['price']; // add price to total
}
// Return the total
return $total;
None of the comments above add any information. The code itself is clear enough: $total = 0 obviously initializes the total, foreach obviously iterates. Comments like these only add noise and must be maintained every time the code changes — if you forget to update them, the comments become misleading.
<?php
// CORRECT: self-explanatory code, without excessive comments
$total = 0;
foreach ($items as $item) {
$total += $item['price'];
}
return $total;
Comments That Are Needed #
Comments are necessary when the code does something non-intuitive — when business logic, technical constraints, or design decisions can’t be captured from the code itself.
<?php
// ✓ Comments that explain WHY, not WHAT
// PHP's strtotime() can't parse the Indonesian date format "15 Januari 2024"
// because it uses English month names. Manual conversion is required.
$date = convertIndonesianDate($inputDate);
// ✓ Comments explaining business context that isn't visible in the code
// The price is multiplied by 1.11 because it includes 11% VAT per PMK 2022
$finalPrice = $basePrice * 1.11;
// ✓ Comments explaining a workaround or a deliberately tolerated bug
// BUG: library XYZ v2.x returns null instead of false
// when the resource isn't found. This is fixed in v3.x.
// TODO: remove this null check after upgrading to v3.x (#TICKET-4521)
$result = libraryXyz->findData($id);
if ($result === null || $result === false) {
return null;
}
// ✓ Comments flagging critical sections that must not be changed carelessly
// WARNING: the order of these operations matters for hash consistency.
// Changing the order will invalidate all existing tokens.
$token = hash('sha256', $userId . $timestamp . $secret);
Temporarily Disabling Code #
One practical use of comments is temporarily disabling code during debugging:
<?php
function processOrder(array $data): array
{
$order = createOrder($data);
// Temporarily disable email sending during debugging
// sendConfirmationEmail($order['email'], $order['id']);
/*
// The SMS notification feature is still in development
$sms = new SmsGateway();
$sms->send($order['phone'], "Order #{$order['id']} successful");
*/
reduceStock($order['items']);
return $order;
}
Comments for disabling code are a temporary debugging tool — don’t leave commented-out code in a production codebase permanently. Unused code should be deleted, not commented out. Version control (Git) keeps the change history if you ever need to go back to old code.
Effective Inline Comments #
Inline comments — comments at the end of a line of code — are most effective for brief explanations directly tied to a single expression:
<?php
// ✓ Inline comments that add value
define('MAX_LOGIN_ATTEMPTS', 5); // Per internal security policy
$timeout = 30 * 60; // 30 minutes in seconds
$pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'; // RFC 5322 simplified
// ✗ Unnecessary inline comments
$i = 0; // set i to 0
$name = "Budi"; // name is Budi
$active = true; // active is true
For magic-number constants (numbers without context), inline comments are very helpful:
<?php
// ANTI-PATTERN: magic numbers without context
if ($responseCode === 429) {
sleep(60);
}
// CORRECT: give them names and comment if needed
define('HTTP_TOO_MANY_REQUESTS', 429);
define('RATE_LIMIT_BACKOFF_SECONDS', 60); // Cooldown per the vendor API documentation
if ($responseCode === HTTP_TOO_MANY_REQUESTS) {
sleep(RATE_LIMIT_BACKOFF_SECONDS);
}
PHPDoc #
PHPDoc is the standard for writing documentation comments in PHP, following the /** ... */ format (two asterisks in the opening). PHPDoc comments are read by IDEs like PhpStorm and VS Code to provide autocomplete, type hints, and inline documentation — even without running the code.
sequenceDiagram
participant Dev as Developer
participant IDE as IDE / Editor
participant Tools as phpDocumentor / PHPStan
Dev->>IDE: Write PHPDoc on functions/classes
IDE-->>Dev: Autocomplete parameters & return type
IDE-->>Dev: Warning if arguments have the wrong type
Dev->>Tools: Run documentation generator
Tools-->>Dev: HTML docs / type analysis reportPHPDoc Structure #
A PHPDoc block consists of three optional parts:
<?php
/**
* [Summary — one short sentence, ending with a period.]
*
* [Description — a longer explanatory paragraph, optional.
* Can span more than one line. Use this to explain
* context, constraints, or behavior not visible from the signature.]
*
* @tag value [Tag annotations]
* @tag value
*/
The Most Important PHPDoc Tags #
| Tag | Used On | Purpose |
|---|---|---|
@param | Function / Method | Document a parameter: type and description |
@return | Function / Method | Document the return value |
@var | Property / Variable | Document a variable’s type |
@throws | Function / Method | Exceptions that may be thrown |
@deprecated | Everything | Mark as obsolete, include an alternative |
@see | Everything | Reference another element or URL |
@since | Everything | The first version this element appeared in |
@author | File / Class | The code’s author |
@link | Everything | External reference URL |
@todo | Everything | Work that still needs to be done |
Documenting Functions #
<?php
/**
* Calculates the total price after discount and tax.
*
* The discount is applied first, before tax.
* A discount value exceeding the base price is clamped to 0.
*
* @param float $price Base price in rupiah (excluding tax).
* @param float $discount Discount percentage as a decimal (0.0 - 1.0).
* @param float $vatRate VAT percentage as a decimal, default 0.11 (11%).
* @return float Total price after discount and VAT.
*/
function calculateTotal(float $price, float $discount, float $vatRate = 0.11): float
{
$afterDiscount = $price * (1 - $discount);
$afterDiscount = max(0, $afterDiscount); // clamp, must not be negative
return $afterDiscount * (1 + $vatRate);
}
// The IDE now knows:
// - The first parameter must be a float
// - The second parameter must be a float between 0.0-1.0 (from the description)
// - The function returns a float
// - The third parameter is optional with a default of 0.11
Documenting Classes and Properties #
<?php
/**
* Represents one item in a shopping cart.
*
* @since 1.0.0
*/
class CartItem
{
/**
* @var string Unique product ID from the catalog.
*/
private string $productId;
/**
* @var int Number of units ordered. Always >= 1.
*/
private int $quantity;
/**
* @var float Price per unit when the item was added to the cart.
* This price is snapshotted and doesn't change even if the product price changes.
*/
private float $unitPrice;
/**
* @param string $productId Product ID from the catalog.
* @param int $quantity Number of units, minimum 1.
* @param float $unitPrice Price per unit in rupiah.
* @throws \InvalidArgumentException If quantity is less than 1.
*/
public function __construct(string $productId, int $quantity, float $unitPrice)
{
if ($quantity < 1) {
throw new \InvalidArgumentException(
"Minimum quantity is 1, received: $quantity"
);
}
$this->productId = $productId;
$this->quantity = $quantity;
$this->unitPrice = $unitPrice;
}
/**
* Calculates the item subtotal (unit price × quantity).
*
* @return float Subtotal in rupiah.
*/
public function subtotal(): float
{
return $this->unitPrice * $this->quantity;
}
/**
* Increases the item's unit quantity.
*
* @param int $amount Number of units to add. Must be > 0.
* @return static Returns this instance for method chaining.
* @throws \InvalidArgumentException If the amount isn't positive.
*/
public function addQuantity(int $amount): static
{
if ($amount <= 0) {
throw new \InvalidArgumentException("Amount must be positive");
}
$this->quantity += $amount;
return $this;
}
}
Complex Types in PHPDoc #
PHPDoc supports richer type notation than plain PHP type hints, very useful for arrays and union types:
<?php
/**
* Searches for products based on the given filter.
*
* @param array{
* category?: string,
* min_price?: float,
* max_price?: float,
* in_stock?: bool
* } $filter Search criteria. All keys are optional.
*
* @return array<int, array{
* id: int,
* name: string,
* price: float,
* stock: int
* }> List of matching products, numerically indexed.
*/
function findProducts(array $filter): array
{
// implementation...
return [];
}
/**
* Processes one item and returns the result, or null on failure.
*
* @param int|string $id Item ID — can be an integer from the database or
* a string UUID from an external system.
* @return Product|null A Product instance if found, null if not.
*/
function findProductById(int|string $id): ?Product
{
// implementation...
return null;
}
The @deprecated Tag
#
When a function or class is no longer recommended, use @deprecated to let developers know — the IDE will show the function calls with a strikethrough:
<?php
/**
* Encrypts passwords using MD5.
*
* @deprecated 2.0.0 MD5 is not safe for passwords. Use password_hash() instead.
* @see password_hash()
*
* @param string $password Password in plaintext.
* @return string MD5 hash — DO NOT use this for new systems.
*/
function encryptLegacyPassword(string $password): string
{
return md5($password); // insecure, only for backward compatibility
}
// The right way to hash passwords in modern PHP:
function storePassword(string $password): string
{
return password_hash($password, PASSWORD_BCRYPT);
}
function verifyPassword(string $password, string $hash): bool
{
return password_verify($password, $hash);
}
Integration with Static Analysis #
PHPDoc isn’t just for human documentation — static analysis tools like PHPStan and Psalm use the information in PHPDoc to find bugs before the code ever runs:
<?php
/**
* @param array<string, int> $scores Mapping of names to scores.
* @return string Name with the highest score.
*/
function getHighestScore(array $scores): string
{
// PHPStan knows $scores contains string keys and int values
// If you write code that treats the values as strings,
// PHPStan will warn you without needing to run the code
arsort($scores);
return array_key_first($scores);
}
// PHPStan will catch these errors:
// getHighestScore(["Budi" => "ninety"]); // ✗ value must be int, not string
// getHighestScore([1 => 90, 2 => 85]); // ✗ key must be string, not int
Comments for Files and Namespaces #
At the top of a PHP file, especially for libraries or frameworks, there’s usually a PHPDoc block documenting the whole file:
<?php
/**
* User authentication module.
*
* Handles the login, logout, token refresh, and session validation
* processes for web applications and REST APIs.
*
* @package App\Auth
* @author Backend Team <[email protected]>
* @since 1.0.0
* @link https://docs.example.com/auth
*/
declare(strict_types=1);
namespace App\Auth;
use App\Models\User;
use App\Exceptions\AuthException;
/**
* Service for managing user authentication and sessions.
*/
class AuthService
{
// ...
}
Notice declare(strict_types=1) placed after the opening PHP tag and before the namespace. This enables strict type checking in that file — any function call that doesn’t match its parameter types will throw a TypeError instead of performing an implicit conversion.
Common Comment Anti-Patterns #
Several comment patterns frequently show up in codebases but actually lower code quality:
<?php
// ✗ Anti-pattern 1: Lying comments (worse than no comments at all)
// Calculate the price with a 10% discount
$total = $price * 1.15; // This actually adds 15%, not a discount!
// ✗ Anti-pattern 2: TODOs that are never done
// TODO: fix this later
$result = unsafeMethod($data); // "later" never comes
// ✗ Anti-pattern 3: Irrelevant credit comments
// Created by Budi on March 15, 2021
// Modified by Siti on April 20, 2021
// Modified again by Dani on January 5, 2022
// (use git blame for this, not comments)
$price = 150000;
// ✗ Anti-pattern 4: Comments explaining basic language syntax
// Create a new array
$data = [];
// Assign a value to the variable
$name = "Budi";
// ✗ Anti-pattern 5: Old code commented out without explanation
// $result = oldMethod($input);
// $result = oldMethodV2($input, $options);
$result = newMethodV3($input, $options, $config); // what's the difference? why was it replaced?
<?php
// ✓ Better versions of the anti-patterns above:
// 1. Clear code with self-explanatory variables
$totalWithVat = $price * (1 + VAT_RATE); // VAT_RATE = 0.11 defined as a constant
// 2. Actionable, trackable TODOs
// TODO(TICKET-891): Replace unsafeMethod() with a bcrypt implementation
// before the v2.0 release — deadline June 30, 2025
$result = unsafeMethod($data);
// 3. Comments explaining why a change happened
// Switched from oldMethodV2 to newMethodV3 because V2 isn't thread-safe
// when processing concurrent requests. See incident #2024-03-15.
$result = newMethodV3($input, $options, $config);
Summary #
- Three PHP comment syntaxes:
//for single-line (most common),/* */for multi-line, and/** */for PHPDoc. The#sign is valid but not idiomatic in modern PHP.- Comments explain WHY, not WHAT — good code already explains what it does; comments are there to explain context, design decisions, and limitations that aren’t visible in the code.
- Comments that repeat the code are noise — they’re expensive to maintain (must be updated every time the code changes) and if you forget to update them, they become misleading.
- PHPDoc (
/** */) is recognized by IDEs and static analysis tools — use it for all public functions, methods, classes, and properties so autocomplete and type checking work optimally.- Important PHPDoc tags:
@param(parameters),@return(return values),@throws(exceptions),@var(property types),@deprecated(mark as obsolete).- Commented-out code is not an archive — delete unused code and rely on Git for history. Dead commented code clutters the codebase and confuses people.
- TODOs must be actionable — include a ticket number or deadline so they don’t become “eternal TODOs” that never get done.
- PHPStan / Psalm read PHPDoc — the types you document in
@paramand@returnare used for static analysis, so type bugs can be caught without running the code.