Why Your PHPDoc Blocks Are Recipe Cards Nobody Can Use
A recipe card with blank ingredient fields is worse than no recipe card at all. Part 3 of the kitchen series covers PHPDoc, comments, and the documentation debt nobody notices until the person who understood it walks out the door.
Somebody handed me a recipe card once, mid-service, from a station I'd never worked. I needed it fast, because the chef who usually ran that station had gone home sick an hour earlier and the ticket rail didn't care. I opened it expecting instructions. What I got looked like this:
/**
* This function does the following
*
* @Reference sdgdfgzdfg
* @Variable
* @Params
*/
function preparePlating(string $dish, int $covers): float
{
// ...
}
I'm translating a kitchen memory into PHP syntax because the code version of this exact card crossed my desk during a review last year, and it made the same sound in my head that the paper one did the night I needed it: nothing. A header that promises a description and delivers none. A reference tag pointing at sdgdfgzdfg, which isn't a ticket number and isn't a URL. It's the sound of someone mashing a keyboard to satisfy a linter that checks for the tag's presence without checking whether the tag says anything. Two more tags, @Variable and @Params, sitting there empty, and I want to flag something before we go any further: those aren't even real PHPDoc tags. The actual tags are @var and @param. Whoever wrote this didn't skip filling in the blanks. They didn't know what the blanks were called.
That's worse than a blank recipe card. A blank card at least tells you, with total honesty, that nothing's written down. This one performs documentation without containing any, and a cook who trusts the performance finds out it's empty at the exact moment they can least afford to.
A Docblock Is a Recipe Card, and a Recipe Card Has One Job
A recipe card exists so a qualified cook who has never made this specific dish before can execute it correctly, tonight, without the original chef standing over their shoulder. It's not for the chef who wrote it. That chef already knows the dish. It's for the commis covering the station, the new hire on their third shift, or the same chef eight months from now after they've run four hundred other dishes and can no longer hold every detail of this one in their head from memory alone.
A PHPDoc block exists for the identical reason, and I want to be specific about who it's for, because getting this wrong is where most bad documentation starts. It's not for you, right now, while the function is fresh in your head. It's for the developer joining the project in March who has never seen this function, and it's for you in eight months after you've written four hundred other functions and can no longer hold this one's assumptions in memory without help.
/**
* Estimate plating time for a dish based on item complexity.
*
* Complexity applies a 40% time penalty above 5 components, based on
* kitchen floor timing data collected across Q1 2026.
*
* @param string $dish Name of the dish being plated, for logging purposes.
* @param int $itemCount Number of distinct components on the plate.
* @param float $baseSecondsPerItem Base plating time per item, in seconds.
* @return float Estimated total plating time, in seconds.
*/
function estimatePlatingTime(string $dish, int $itemCount, float $baseSecondsPerItem): float
{
$complexityMultiplier = $itemCount > 5 ? 1.4 : 1.0;
return $itemCount * $baseSecondsPerItem * $complexityMultiplier;
}
Compare that against the broken example at the top of this piece, line by line. The short description states what the function does in one sentence, the way a ticket call states the dish in one phrase. The longer description underneath explains something the code itself cannot: why 5 components is the threshold and where that number came from. That's the part a docblock can carry that the code never will, because $itemCount > 5 tells a future reader that five is the number, not why five is the number.
The @param tags do more than restate the type hint, which PHP already enforces on its own since the type declarations sit right there in the function signature. Each one adds the thing the type hint can't say: what this parameter represents in domain terms, not only what shape of data it accepts. $dish carries more meaning than "a string." It's the name of the dish, kept for logging. That distinction matters to the next person who has to decide whether it's safe to pass in something else that happens to also be a string.
Types Do the Boring Work Now. Docblocks Do the Interesting Work.
If you learned PHP before version 7, you remember when docblocks had to carry type information the language itself had no way to enforce. @param int $covers used to be the only signal a caller had that a parameter should be an integer, and PHP would happily accept a string, an array, or null in its place without complaint. That era is over, and it changes what a docblock's job is.
// Pre-PHP 7 era: the docblock was the only type enforcement that existed
/**
* @param int $covers
* @return float
*/
function calculateTip($covers) {
return $covers * 3.50;
}
// PHP 8: the language enforces this. The docblock's job has changed.
/**
* Calculate expected tip pool contribution for a shift.
*
* @param int $covers Number of covers served this shift.
* @return float Contribution amount, in dollars, before pooling adjustments.
*/
function calculateTip(int $covers): float
{
return $covers * 3.50;
}
The second version's docblock isn't restating int and float. Anyone reading the function signature already knows the types; the language itself refuses to run the function with the wrong ones. What the docblock adds is the part types cannot express: this is dollars, before pooling adjustments, calculated per shift. That's domain knowledge, and it's the only kind of information worth putting in a comment once your type system is doing its job. A docblock that repeats the type hint in prose is a recipe card that lists "flour: a powder" as an ingredient description. Technically true. Adds nothing a competent reader didn't already have.
There's one place docblocks still carry real type information PHP's own type system can't express on its own: collections. array tells you nothing about what's inside it, and this is exactly where a proper docblock earns its keep.
/**
* @return SauceStation[] All active sauce stations for tonight's service.
*/
function getActiveStations(): array
{
// ...
}
array as a return type is honest but useless to a caller trying to know what they're iterating over. SauceStation[] in the docblock tells them, and tools like PHPStan and Psalm read that annotation and will flag it if you return something that doesn't match, the same way a health inspector checks the label against what's in the container.
Comments Explain Why. The Code Already Explains What.
Inline comments are not docblocks, and conflating the two is one of the more common ways documentation turns into noise. A docblock is the recipe card, the contract for the whole dish. An inline comment is the sticky note a specific cook left on the inside of a specific pan, and it should only exist to explain something the pan itself, or in code terms, the line right below it, cannot say on its own.
// Bad: restates what the code already shows
// Multiply covers by 3.50
$tipContribution = $covers * 3.50;
// Good: explains something the code can't show on its own
// $3.50 is the house's standard per-cover tip pool rate, set by
// management in the 2026 wage agreement. Do not change without HR sign-off.
$tipContribution = $covers * 3.50;
The first comment is a cook writing "cut the onion" on a sticky note directly above a step that already says, in plain language, cut the onion. It adds nothing, and it will drift out of sync with the code the first time someone changes the multiplier and forgets the comment sitting three lines away, describing a number that's no longer there. The second comment tells you something the code has no way to express on its own: where this number came from and why you shouldn't touch it casually. That's the entire test for whether an inline comment earns its place. If it explains why, keep it. If it narrates what, delete it, because the code narrates itself, and a comment that repeats the code is a second copy of the truth that will eventually stop matching the first one.
The deeper history of a decision, the actual discussion that led to the 2026 wage agreement, doesn't belong wedged into a one-line comment at all. That belongs in your commit history and your pull request discussion, the durable record a comment was never designed to hold. Git exists precisely because "why did we do it this way" is a question worth answering in more depth than a single line can carry, with the actual conversation and reasoning attached to the change itself rather than paraphrased into a comment that will age worse than the commit it should have pointed to.
We recommend reading PHP OOP Explained Through a Real Kitchen Brigade System if you haven't already, since most of what's worth documenting well in a real PHP application lives on classes and methods, not standalone functions. The visibility rules from that piece decide who's even allowed to call the method you're about to document, and that context changes what the docblock needs to say.
A Reference Tag Points to Something Real, or It Doesn't Belong
Go back to the broken example that opened this piece. @Reference sdgdfgzdfg is the part that bothers me most, because it isn't a simple omission. It's an active performance of containing information it doesn't hold. A real reference tag, whether you're using @see, @link, or a project-specific convention your team has agreed on, points to something a reader can go open: a ticket, an RFC, a related class, an external spec.
/**
* Apply the house tip pooling formula.
*
* @see https://internal.example.com/wiki/tip-pooling-2026-agreement
* @param int $covers Number of covers served this shift.
* @return float Contribution amount, in dollars.
*/
function calculateTip(int $covers): float
{
return $covers * 3.50;
}
Anyone hitting this function with a question the docblock's short description doesn't fully answer has somewhere to go next. That's what a reference tag is for. A reference tag pointing at six random characters isn't a smaller version of that. It's a different thing entirely, a placeholder someone meant to fill in later and never did, left in place because the linter checking for the tag's existence has no way to check for the tag's honesty. Don't write a reference tag until you have something real to put in it. A missing reference is a gap you can see and fix. A fake one is a gap disguised as information, and disguised gaps are the ones that survive code review, because they look like they're already handled.
Documentation Generators Amplify Whatever You Wrote
Tools like phpDocumentor read your docblocks and generate a browsable reference site from them, the equivalent of turning your kitchen's individual recipe cards into a bound reference book for the whole brigade. That's a real convenience, and it's also exactly why garbage docblocks are worse than none. Run phpDocumentor against a codebase full of blocks like the one that opened this piece, and you don't get a warning that says "documentation missing." You get a polished, professional-looking reference site confidently displaying broken links and empty parameter descriptions, which is a more convincing kind of wrong than a blank page ever was. A new hire trusts a generated reference site by default. They have no reason not to, until they click through to sdgdfgzdfg and learn the hard way that the whole book was assembled from recipe cards nobody filled out.
This is the same lesson Part 1 of this series opened with, applied one level up. An unlabeled container of clarified butter is a small, contained failure, one cook, one station, one dish. A codebase full of confidently generated documentation pointing at nothing is the same failure running at the scale of an entire team, and it's harder to catch, because it looks finished.
README Files Are the Kitchen's Standing Procedure Binder
Every well-run kitchen has a binder that isn't a recipe card. It's the opening checklist, the closing checklist, the allergen protocol, the answer to "what do we do if the walk-in alarm goes off at 2 AM." A new hire reads that binder on day one, before they've touched a single pan, because it answers questions no individual recipe card was ever meant to answer.
A README is that binder. It should tell a new developer how to get the project running locally, how to run the test suite, what the major architectural decisions were and why, and where to look next if something in this file doesn't answer their question. If you've laid out your application with the kind of intentional structure proper PHP MVC design covers, the README is where you explain that structure exists on purpose, not as an accident of however the project happened to grow. A codebase with excellent docblocks and no README is a kitchen where every station has an immaculate recipe card and nobody ever wrote down where the fire extinguisher is.
The Cost Shows Up Later, and It Shows Up Big
Bad documentation doesn't cost you anything the day you write it. That's exactly what makes it easy to skip. The cost arrives later, on a night the person who understood the code isn't in the building, the same way a blank recipe card costs nothing until the chef who knew the dish by memory calls in sick and someone else has to cover the station cold. The broader PHP ecosystem has spent a long time treating documentation as the thing you get to once the feature ships, and the feature always ships before the documentation does, and the documentation, more often than anyone wants to admit, never gets written at all.
Fill in the docblock before you move to the next function, not after the sprint, not when someone files a ticket asking what a function does. Write the description that explains why, not the one that restates what. Point your reference tags at something a reader can open. None of this is complicated, and none of it was complicated the night I opened that recipe card either. It has to be there, filled out, by the person who understood the dish, before the person covering their station needs it.