The Rule
Every comment must earn its place. If the information is already in the code (method name, type signature, class name), the comment is noise — delete it. If the comment explains WHY something exists, documents a non-obvious side effect, or declares a thrown exception — keep it.
Type Declarations
Use PHP 8.2+ native types on ALL function signatures. No exceptions.
// Correct — native types everywhere
public function find(string $id): ?Contact { }
public function save(Contact $contact): Contact { }
public function delete(Contact $contact): void { }
public function match(Criteria $criteria): LengthAwarePaginator { }
readonly properties do NOT need @var annotations — the type is visible in the constructor signature:
// Correct — no @var needed
final readonly class CreateContact
{
public function __construct(
private ContactRepository $repository,
private EventBus $eventBus,
) {}
}
When PHPDoc IS Required
1. @throws — Always Declare Thrown Exceptions
/**
* @throws ContactNotFound
*/
public function byId(string $id): Contact
{
if (! $contact = $this->repository->find($id)) {
throw new ContactNotFound(strtr('Contact {id} not found.', ['{id}' => $id]));
}
return $contact;
}
This is non-negotiable. PHP does not enforce checked exceptions — @throws is the only way IDEs and consumers know what to expect. Omitting @throws is a bug.
2. @property on Eloquent Models
Eloquent model attributes cannot be declared as native PHP properties. Use @property and @method docblocks on the class:
/**
* @property string $id
* @property string $first_name
* @property string $last_name
* @property string $email
* @property Carbon $created_at
* @method static ContactFactory factory()
*/
class Contact extends Model { }
3. @var for Typed Array Contents
When an array holds a specific type that native array cannot express:
/**
* @var Event[]
*/
private array $domainEvents = [];
4. Scalar ID Payload Clarification
When an event carries a scalar ID instead of an entity, document what it represents:
/**
* @param string $contact The deleted contact uuid
*/
public function __construct(public string $contact) {}
When PHPDoc is FORBIDDEN
Never Add @param or @return When Native Types Exist
// WRONG — redundant, delete the entire docblock
/**
* @param string $id
* @return Contact|null
*/
public function find(string $id): ?Contact { }
// CORRECT — signature says everything
public function find(string $id): ?Contact { }
Never Add Docstrings That Repeat the Method Name
// WRONG — "Create a contact" is just the method name in prose
/**
* Create a contact.
*/
public function create(array $input): Contact { }
// CORRECT — no docblock at all
public function create(array $input): Contact { }
Never Add Class-Level Docstrings That Repeat the Class Name
// WRONG — "Handler for sending user email verification" is just the class name
/**
* Handler for sending user email verification.
*/
final readonly class SendUserEmailVerification implements ShouldQueue { }
// CORRECT — no docblock
final readonly class SendUserEmailVerification implements ShouldQueue { }
Never Add @var to Constructor-Promoted Properties
// WRONG — type is already in the constructor
/**
* @var ContactRepository
*/
private ContactRepository $repository;
// CORRECT — type is in the signature, nothing else needed
public function __construct(private ContactRepository $repository) {}
Inline Comments
Keep: WHY Comments
// Events must be published AFTER persistence to ensure committed state
$contact->publishDomainEvents($this->eventBus);
// Deletion is idempotent — missing entity is not an error
try {
$contact = $this->finder->byId($id);
} catch (ContactNotFound) {
return;
}
// Transitions call save() internally — spatie framework requirement
$this->status->transitionTo(Granted::class);
Keep: Side-Effect Warnings
// Also clears the domain events queue after dispatching
$eventBus->publish(...$this->domainEvents);
$this->domainEvents = [];
Keep: Non-Obvious Business Rule
// Only register event when email actually changes (avoid duplicate events)
if ($email !== $this->email) {
$this->forceFill(['email' => $email]);
$this->registerDomainEvent(new UserEmailUpdated($this));
}
Delete: WHAT Comments
// WRONG — all of these restate the code
$this->name = $name; // Set the name
$contact->save(); // Save the contact
return redirect('/dashboard'); // Redirect to dashboard
$contact->delete(); // Delete the contact
Delete: Visual Separators
// WRONG — never use separator comments
// ========================================
// Static Constructors
// ========================================
// --- Relationships ---
// =====================
// Domain Methods
// =====================
Comments in Exception Classes
Exception classes are always empty-bodied. Never add docstrings to them:
// CORRECT — empty body, no docstring
class ContactNotFound extends ContactException {}
// WRONG — docstring adds nothing
/**
* Exception thrown when a contact is not found.
*/
class ContactNotFound extends ContactException {}
Comments in Tests
- Test method names are self-documenting — do not add docstrings to test methods
- Comment only non-obvious test setup or surprising assertions
- Do not add
// Arrange,// Act,// Assertsection labels unless the test is unusually complex
// WRONG — test name already says this
/** @test */
public function it_throws_not_found_when_contact_does_not_exist(): void
{
// Act & Assert ← REMOVE these labels
$this->expectException(ContactNotFound::class);
$finder->byId('nonexistent-id');
}
// CORRECT — no docstring, no section labels
/** @test */
public function it_throws_not_found_when_contact_does_not_exist(): void
{
$this->expectException(ContactNotFound::class);
$finder->byId('nonexistent-id');
}
Contract Interfaces
Interface-level docblocks document the contract purpose, not individual methods:
// CORRECT — documents the contract, not the method
/**
* Persistence contract for Contact aggregate root.
*/
interface ContactRepository
{
public function find(string $id): ?Contact;
public function match(Criteria $criteria): LengthAwarePaginator;
public function save(Contact $contact): Contact;
public function delete(Contact $contact): void;
}
Individual interface methods do NOT get docblocks — the method signature is the documentation. Add @throws only on the implementing class if it throws.
Quick Decision Table
| Situation | Action |
|---|---|
| Method has native type signature | No @param / @return |
| Method throws an exception | Add @throws — always |
| Eloquent model attributes | Add @property docblock on class |
| Array holds typed items | Add @var Type[] |
| Event carries scalar ID | Add @param with description |
readonly property in constructor |
No @var |
| Comment explains WHY | Keep |
| Comment explains WHAT | Delete |
| Comment is a visual separator | Delete |
| Exception class body | Always empty — no docstring |
| Test method | No docstring — name is documentation |
| Interface-level purpose | Add class-level docblock |
| Interface method | No docblock — signature is enough |
Red Flags — STOP and Remove
If you catch yourself writing any of these, delete it immediately:
- A docblock that starts with the method/class name in different words
@paramor@returntags when native types are already in the signature// Set the...,// Get the...,// Create a...,// Delete the...- A section separator made of
=,-, or*characters // Arrange,// Act,// Assertin a test shorter than 15 lines- A docblock on an empty-bodied exception class
@varon a constructor-promotedreadonlyproperty
Common Rationalizations
| Excuse | Reality |
|---|---|
| "PHPDoc improves IDE support" | Native types give IDEs everything they need. Redundant PHPDoc is noise. |
| "It's good practice to document all public methods" | Self-documenting code + @throws is the practice. Restating signatures is not. |
| "I added it for completeness" | Completeness means every comment adds value. Redundant comments subtract value. |
| "The team might not understand the method" | If the method name + types aren't clear, rename the method — don't paper over it with a comment. |
| "I'll add section separators for readability" | If your class needs separators, it's too large. Extract a class. |
| "Tests should be commented for clarity" | Test names ARE the documentation. A well-named test needs zero comments. |
Pre-flight Checklist
- All function signatures have native PHP type declarations?
- No
@param/@returnduplicating native types? -
@throwsdeclared on every method that throws? -
@propertydocblock on Eloquent models? - No docstrings restating method or class names?
- No visual separator comments?
- No comments explaining WHAT (only WHY)?
- No docstrings on exception classes?
- No
@varon constructor-promoted properties? - Test methods have no docstrings?