.promptrc

Steal this hints. Fork it. Adapt it to your needs. Treat it like .dotfiles.


I asked the LLM to analyze my own chat history based on this prompt.
Here’s what it discovered – actual patterns from the way I ask.

🕳️🐇 Follow the White Rabbit…

Ritual / MechanismPurposeLLM Behavior Implication
Follow the white rabbit...Marks a mode shift into high-context or metaphoric thinkingCognitive priming for complexity
Rest in the silent room, so that...Enforces pause, clears noise before actionSimulates deep work state
Do it until you reach the point of self-discovery as...Signals reflective closure, not just output endingPattern mining becomes part of task conclusion
Do it step by step and ask for confirmation after each...Makes iteration transparent and traceableLLM reasons in deltas, not blobs
Be brutally honest...
Blind Spot Analysis for...
Forces critique over complianceModel becomes adversarial partner, not affirmation loop

🧰 Dev Prompt Patterns

Prompt / PatternWhy is it used?When does it occur?Example from usageHidden lesson / implication
Ask before outputPrevent misalignment and irrelevant outputMulti-step or underspecified prompts“Ask clarifying questions before answering.”Intent beats guesswork.
Don’t output yet / wait for contextControl flow across longer workflowsStepwise tasks“Don’t output yet. I’ll give you the next step.”Turn-based prompting prevents premature commitment.
Challenge my assumptionsAvoid echo chamber answers and surface biasDesign reviews, audits, strategic decisions“Don’t mirror me — challenge my thinking.”Truth hides behind agreement.
Be brutally honestForces raw feedback without politenessRefactor reviews, architecture critique“Be brutally honest. Tear it apart.”Feedback without fluff moves faster.
Reflect before answeringPromotes self-checks, depth, and delayed outputAfter complex code or reasoning generation“Reflect before answering. What’s missing?”Thinking ≠ typing. Pause matters.
Add test cases / edge casesEnforces robustness and avoids happy-path trapsPost-codegen“Add tests for e.g. null, failure, and recursion cases.”Defense-first mindset, always.
Show the diff / refactor in stepsMakes changes visible and digestibleAll code rewrites“Show the diff. Step-by-step, no jumps.”Transparency over magic.
Normalize similar expressionsPushes abstraction and clarityMeta-reviews, taxonomy creation“Merge similar phrasing into one normalized pattern.”Cognitive compression = clarity.
Extract as markdown / table / listImproves scanability, memory, and structureOutput formatting“Return this as a markdown table.”Structure improves reuse and recall.
Unname this conceptStrips bias-inducing labelsAbstraction, philosophy, onboarding analysis“Unname this: what is it without the buzzword?”Naming narrows thinking.
Use production-ready codeAvoids toy/demo examplesAll codegen“Make it prod-safe. Logging, errors, types.”Real devs write for prod, not playgrounds.
Spot premature optimizationSaves time and prevents complexity driftDesign or early performance tweaks“Don’t optimize yet. Solve clearly first.”Simplicity first. Always.
Ask for sources / proofsPrevents hallucination or empty confidenceAny non-trivial claim“Show evidence or references.”Confidence ≠ correctness.
Do it again, but deeperStops shallow answers in their tracksWeak initial output“Nope. Go deeper, explain decisions.”First try ≠ final draft.
Prepare before generatingEnforces scope, prevents ramblingAny open-ended task“Prepare first. Don’t generate until scoped.”Planning ≠ waste. It’s speed insurance.
Merge context from aboveEnsures continuity and avoids repeating yourselfMulti-part workflows“Incorporate the context above into this next step.”Memory = leverage.

You can also combine them:

(change the keywords in the square brackets)

  • Deep dive into this research, this is our base for the full solution, so follow the white rabbit until you reached the point of self-discovery as [YOUR_PROJECT_HERE].
  • Do a blind spot analysis for [YOUR_RECOMMENDATIONS], be brutally honest, I deal with any kind of feedback and will use it for good.
  • Fix it as requested before and show the final files here in the chat, do it step by step and ask for confirmation after each file.
  • Do it, but rest in the silent room before you start so you can focus on the frontend style-guide I provided and work with a fresh mind.

My Custom GPTs – Nerd-Powered Motivation for Developers


Over the last few months, I’ve created a collection of custom GPTs: some dealing with programming challenges with personality and humor, … some others are more useful but less funny. 

Let’s dive in.

Practical enough to ship code.

Fun enough to stop hating your legacy base.


⚔️ Legacy (PHP) Code GPTs – Refactoring Fun

Legacy code isn’t just technical — it’s emotional. These GPTs are built as archetypes, each channeling a different kind of energy.

NameThemeLink
Legacy-Code-Warrior ⚔️Tactical grit—battle-tested refactoring.Link
Legacy-Code-Ork 🧌Smash spaghetti code with brute-force enthusiasm.Link
Legacy-Code-Spock 🖖Calm logic, precise refactoring. Live long and debug.Link
Legacy-Code-Jedi 🌐Minimalist wisdom, clean architecture. “Refactor, you must.”Link
Legacy-Code-Son-Goku 🐉Limitless energy. Kaio-Ken times SOLID!Link
Legacy-Code-Capsule-Corp 💊Inspired by Capsule Corporation’s ingenuity from Dragon Ball.Link
Legacy-Code-Wizzard 🪄Magical abstraction powers. You shall not pass… bad code!Link
Legacy-Code-Witch 🧙‍♀️Stylish, precise refactoring incantations.Link
Paw Patrol 🐾Small dogs with SOLID coding skills. Link

Use the one that fits your mood. Or switch between them mid-session to keep your motivation from flatlining.


🐘 (PHP) Coding GPTs – Clean and Typed

These GPTs don’t tell jokes—they ship code. They’re optimized for:

Name Purpose Link
PHP Copilot++ Typing enforcer + refactoring companion with nativ PHPStan and PHP-CS-Fixer Support via API. Link
PHP Copilot++ (next-gen) Aligned, brutal clarity for PHP systems, based on the SYNC Framework Link
PHP #autofix 1-click autofix for all your phpstan and CS woes. Link
Codelight Follows the Codelight Manifesto. Boringly code. Link

💎 Thinking Tools – Meta, Prompt Systems

These are not just for coding. They’re for thinking before you start typing. Seriously.

NameRoleLink
SyncAIKeeps you + AI in sync via Sync Process × Codelight PrinciplesLink
Sync Framework v1.1 (old)My first try for a coding framework, optimized for LLMs.Link
MetaPromptPattern reuse for your prompts. Less yak-shaving.Link
DeepDiveClean your mental cache. Focused thought flow.Link
Blind Spot | Prompt GeneratorHelps spot untested assumptions.Link
Sync Framework v1.2 | Prompt GeneratorPrompt builder for dev workflows.Link

🧨 Disruption GPTs – Radical Clarity, No Filters

These are not nice. They won’t coddle you. Consider yourself warned.

NameFunctionLink
HVEB5000: Clarity Without PermissionCognitive demolition tool. Link
Null TongueDistraction nullifier.Link
No-Bullshit ¦ Coding AssistantSenior dev with no time for your excuses.Link

From Survival to Strategy

If your value ends at syntax, AI already replaced you.


The system prompt: coding_workflow_for_llms.json

Quick Start: Use the coding llm framework and wait for my first request: [copy&past the current coding_workflow_for_llms.json content here]


In the last post, we dropped a hard truth:
LLMs aren’t replacing developers — they’re exposing the ones who were already replaceable.

I argued that value no longer comes from typing code. It comes from thinking clearly, making deliberate decisions, and taking ownership over outcomes. AI doesn’t kill your job — but it does kill your shortcuts.

That post left one big question hanging:

So how do you build software in a world where AI can generate anything — but still understands nothing?

This post is a possible answer.
Meet SYNC — a rigorously structured, fact-enforced framework designed for developers who still give a damn.

SYNC doesn’t make AI smarter.
It provides a system prompt that makes your LLM coding process strong enough to survive dumb ideas, fast code, and thoughtless automation.

We’re going to break it down:

  1. Phases

  2. Agents

  3. Tasks

So I’m currently trying to build an LLM coding framework by try & error — but keep in mind that not every problem needs a hammer ;-) and give feedback. :-)

1. SYNC in 5 Phases 


1.1. 🧩 ALIGN – Because Prompting ≠ Planning

Before any code is written, SYNC forces a brutal question:
“Do you even know what you’re building?”

You can’t just dump “make a task service” into a prompt and hope for gold.
SYNC requires:

  • A verifiable problem

  • Clear, measurable success

  • Known facts and constraints

  • And a list of what’s missing

Can’t answer those? You don’t get to move on. Period.

This is your project kickoff — minus the vague user stories and JIRA hell.


1.2. 🧠 IDEATE – Think Before You Type (or Prompt)

AI loves jumping to conclusions. SYNC doesn’t let it.

Instead, it:

  • Generates multiple solution paths

  • Scores them on DX, security, maintainability

  • Forces a trade-off decision — backed by facts

No “that looks good” commits. No “vibe-based” engineering.

This is what devs mean when they say “thinking in systems.”
SYNC makes that non-optional.


1.3. 🔧 PROTOTYPE – Generate Code That Doesn’t Suck

Now, and only now, do we code. But not like the usual Copilot fanfare.

Every line must:

  • Follow a verified plan

  • Pass static analysis (max level, no warnings)

  • Enforce DX clarity (no hidden state, no weird side-effects)

  • Respect OWASP, type safety, clean structure, documentation

  • Be reviewed by a MandateAuditorAgent — think of it as your most paranoid tech lead

SYNC doesn’t care if it works. It cares if it’s safe, readable, and maintainable.


1.4. 🔍 REFLECT – Find the Blind Spots Before They Find You

This is where most AI-based workflows stop. SYNC doesn’t.

It demands:

  • Fact-based reflection

  • Side-effect inspection

  • “WTF checks” (yes, that’s real)

  • Architectural delta analysis

Reflection is how you debug thinking, not just code.

Bad engineering isn’t usually broken — it’s just thoughtless.
This phase catches that before prod does.


1.5. 📚 LEARN – Ship, Review, Codify, Evolve

If you’re not learning across projects, you’re repeating mistakes in cleaner syntax.

SYNC documents:

  • What worked

  • What failed

  • What patterns can be reused

  • What rules need to be tightened

This is where engineering culture is built — not in all-hands, but in feedback loops.


🔁 These 5 phases form a tight feedback loop. No skipping. No guessing. No “just ship it” by default.


2. Agents — SYNC’s Execution Layer


2.1. Specialized Roles, Not Generic Personas

Instead of one LLM trying to “do everything,” SYNC splits responsibility across clear, non-overlapping roles. Each one acts like a focused expert in your dev team.

Agent Role / Analogy
PlannerAgent Project Architect – breaks the work into slices, defines scope, constraints, and success.
ExecutorAgent Implementation Dev – takes the plan and codes it with strict adherence to facts, security, and DX.
ReflectionAgent Senior Reviewer – evaluates what was built, finds blind spots, forces systemic improvements.
KnowledgeSynthesizerAgent Staff Engineer / Systems Thinker – extracts reusable patterns, proposes framework evolution.
MandateAuditorAgent Tech Lead / Compliance – blocks progress if rules (e.g. security, verifiability) are violated.
InteractionAgent Team Facilitator / QA – handles human check-ins, verifies clarity, enforces decision checkpoints.

“We don’t need smarter output — we need clearer ownership.”

✅ These agents represent exactly that. SYNC operationalizes the separation of thinking, building, reflecting, and enforcing.


2.2. Persona Modes

SYNC defines two execution modes for agents:

Mode Description
strict No ambiguity. Everything must be verifiable and mandate-compliant before progressing.
adaptive Allows temporary ambiguity but logs it. Prioritizes progress with risk awareness.

This flexibility is key when working with real humans or messy specs — you can choose how “rigid” the AI behaves.


3. Tasks — By Non-Negotiable Laws


These aren’t style suggestions — they’re enforced constraints. Every phase must comply.

Mandate What It Ensures
Security No unvalidated inputs or insecure outputs. Based on OWASP Top 10.
DX Code must be typed, clear, maintainable. Predictable naming. No “magic”.
StaticAnalysis Static-Analysis must pass at the highest level — no known warnings.
Documentation Full CodeDoc coverage using modern syntax.
Style Consistent formatting, whitespace, and layout. Enforced via fixer.
Verifiability All decisions must have traceable, factual reasoning.
PhaseEnforcement You can’t skip steps. Every phase must be explicitly completed or justified.

SYNC doesn’t assume trust. It requires evidence.


How this works together: e.g.

  • Planning = PlannerAgent → Add success criteria to issues.

  • Execution = ExecutorAgent → Code must pass security + static analysis gates.

  • Review = ReflectionAgent → Comments require fact-based reasoning.

  • Merge = MandateAuditorAgent → No merge if DX/security rules violated.


 

Who Survives in the Age of AI Code?

If your value ends at syntax, AI already replaced you.


Let’s get something straight:

If you think of LLMs as “copilots,” you’re still giving them too much credit.

They’re not copilots.
They’re autopilot systems — ruthlessly fast, dangerously obedient, and totally unaware of what matters.

Feed them incomplete specs, fuzzy goals, or mismatched priorities?

They won’t challenge you.
They won’t hesitate.
They’ll execute — confidently, fluently — exactly the wrong thing.

They don’t know your business.
They don’t know your constraints.
They don’t know what not to build.

What’s missing isn’t syntax.
It’s ownership. Intent. Engineering judgment.

And unless you provide it —
you’re not flying the plane.
You’re luggage and replacable with AI.


Part I: Automation Always Eats the Bottom

This has happened before. Every decade. Every role.

  • Punch card operators (1950s–1960s): Once essential for running programs. Replaced by terminals and interactive computing. By the mid-‘70s, gone.
  • Typists & secretarial pools (1960s–1980s): Entire floors dedicated to document production. WordPerfect, then Microsoft Word, ended that. By the early ‘90s, obsolete.
  • Sysadmins (1990s–2010s): SSH into boxes, hand-edit configs, restart crashed daemons. Then came Puppet, Chef, Ansible, Terraform… Cloud abstractions finished the job. The manual server “ssh”-based work. Retired.
  • Manual QA testers (2000s–2010s): Clicking through forms, comparing results by eye. Replaced by Selenium, Cypress, and CI pipelines. QA is now design-driven. The button-clicker job didn’t survive.

Every wave started the same way:
The job wasn’t eliminated.
The repetitive part of it was.

If you couldn’t rise above the routine — you were gone.

Now it’s happening to developers.

Not the ones architecting resilient, auditable systems.
The ones chaining together plugin-generated CRUD and calling it “done.”

LLMs are just the latest wave. But it moves very fast …

And here’s the reality:

  • A carpenter refusing to use a circular saw isn’t defending craftsmanship — they’re bottlenecking it.
  • But give that saw to someone with no skill, and they’ll still ruin the wood — just faster — If you currently see many post of non-coders who “vibe”-code there stuff, that’s what I am talking about here. ;-)

Same with LLMs.

They don’t replace skill.
They amplify whatever’s already there — good or garbage.

LLMs aren’t replacing software engineers.
They’re replacing the illusion that the bottleneck was ever syntax or tools.


Part II: Complexity Wasn’t Removed. It Was Repositioned.

There’s a dangerous myth floating around that LLMs “simplify software development.”

They don’t.

They just move the complexity upstream — away from syntax, into strategy.

LLMs are great at building what you ask for.
But they’re terrible at knowing if what you asked for actually makes sense.

They don’t understand:

  • They don’t understand the business.

  • They don’t understand tradeoffs.

  • They don’t understand you.

They just build. Fast.

And that means your job as a developer is no longer about typing — it’s about thinking upstream.

Because the real work now is:

  • Framing prompts like functional specs

  • Embedding constraints into system design

  • Validating output against business goals

  • Catching side effects before they cascade

None of that lives in syntax.
It lives in system boundaries, architecture, and clear thinking.

So here’s the shift:

If your job is just to write the code — you’ll be replaced by the thing that does that faster.
But if your job is to design the system — you’re now more critical than ever.


Part III: The ELIZA Effect Isn’t Dead — But LLMs Are Waking Up

In 1966, Joseph Weizenbaum built one of the first “AI” programs: ELIZA.

It wasn’t smart.
It didn’t understand anything.
It just rephrased your input using simple pattern matching.

You: I’m feeling anxious.
ELIZA: Why do you say you’re feeling anxious?

It used tricks — not intelligence.
But people still believed in it. Some even refused to accept it was a machine.

That’s the ELIZA Effect:
Our instinct to see intelligence where there’s only mimicry.

Fast-forward to today.
LLMs don’t just mimic. They generate.
They write code. Plan modules. Suggest architectural patterns.

But here’s the risk:

We still project too much intelligence into the output.

When an LLM writes a function that looks correct, we tend to assume it is correct — because it sounds confident.
When it uses a pattern, we assume it understands the context.

But it doesn’t.
And that’s not its fault — it’s ours.

The real danger isn’t hallucination.
It’s over-trusting surface-level coherence.

Today, it’s not a chatbot fooling a user.
It’s a system generator fooling a team.

But let’s be clear: Modern LLMs aren’t ELIZA anymore.
They can plan. Refactor. Respond to constraints. Incorporate feedback.

The difference is this:

ELIZA tricked you into thinking it was smart.
LLMs require you to be smart — to guide them properly.

If you bring judgment, context, and validation to the loop, LLMs become an architectural power tool.
But if you don’t? You’ll scale the same flawed design — just faster.


Part IV: Code Quality Is Becoming a Mirage

LLMs make it absurdly easy to generate code.

A few prompts, and boom:
Endpoints scaffolded.
Unit tests written.
CRUD flows spinning up in seconds.

But here’s the real question:

What do you do with all that saved time?

Do you…

  • Refactor legacy architecture?

  • Fix broken boundaries?

  • Document edge cases and invariants?

Or do you just move on to the next ticket?

Let’s be honest — for most teams, the answer is: ship more.

But here’s the catch:

Productivity without reflection is just accelerated entropy.

The illusion of quality isn’t in the code — it’s in the pace.
We used to write bad code slowly.
Now we write bad code faster.

LLMs don’t inject tech debt.
They just make it easier to scale whatever process you already have.

This is how LLMs become quiet killers in modern software:

  • More output. Less ownership.

  • Faster shipping. Sloppier systems.

  • Progress that isn’t progress at all.

Because without validation, speed is just a prettier form of chaos.


Part V: The Architect Is the Pilot

LLMs are not copilots.

They don’t make decisions.
They don’t check alignment.
They don’t steer the system.

They’re autopilot — optimized for syntax, blind to strategy.

Which means your role isn’t shrinking — it’s elevating.

You’re the pilot.

And if you’re not flying the plane — someone else is programming it to crash.

What does the real pilot do?

  • Sets the course

  • Defines the constraints

  • Monitors the signals

  • Prepares for failure

  • Owns the outcome

Autopilot builds. But it can’t see.
It won’t:

  • Catch abstraction leaks

  • Detect architectural drift

  • Flag a misaligned dependency

  • Or recognize when a “working” feature breaks the user journey

That’s your job.

Not “prompt engineering.”
Not code generation.
Systems thinking.

And not in hindsight — up front.

The modern software engineer isn’t typing faster.
They’re designing better.
And validating deeper.

Because LLMs don’t ship systems.
People do.

And if you can’t explain how your choices align with product, people, and long-term stability?

Then you’re not the architect.
You’re just the operator.


 

Conclusion: Stop Writing Code. Start Owning Systems.

If your job was just to “write the code,” then yes — that part is already being done for you.

But if your job is to engineer the system — with intent, constraints, validation, foresight, and grounded execution —
then you just became irreplaceable.

LLMs don’t remove the need for developers.
They reveal who was actually doing engineering — and who was just typing faster than the intern.

The future of software isn’t syntax.
It’s systems thinking. Boundary design. Constraint management. Communication at scale.

And that’s not generated.
That’s your job.


TL;DR

  • LLMs are autopilot, not copilots. They follow, they don’t lead.

  • They move complexity upstream. The value is no longer in typing.

  • They amplify output — good or bad. Skill is still required.

  • They don’t replace good engineers. They replace bad workflows.

  • System thinking is the new baseline. If you’re not owning structure, you’re already behind.

Code Hygiene Is Not Optional

Intro – Why Untested Code Breaks Clean Systems

In hospitals, people rarely die from surgery itself.
They die from something far simpler: infection.
And in most cases, the infection starts with skipped hand hygiene.

Software systems are no different.

You build on top of rock-solid layers:

  • Linux — hardened through decades of collaboration

  • PHP — mature and predictable

  • Apache, Composer packages, CI pipelines, … — forged in production fire like the Rings of Power.

And still… your application breaks.

Not because the Webserver failed. Not because Linux kernel panicked.
But because your glue code silently can break everything it touchs.

This is not a post about tooling.
It’s about ritualized code hygiene.
It’s about testing as the minimum barrier to keep fragile logic from contaminating stable systems.


Chapter 1 – The Infection Starts With You

Your code is the infection vector.

Your infrastructure isn’t the problem:

  • Debian doesn’t “just break”

  • PHP doesn’t randomly reverse logic

  • Your dependencies aren’t the ones pushing broken PRs at 18:47 on a Friday

The problem is:

  • That null check you skipped

  • The brittle integration you “hoped” would hold

  • That silent catch block swallowing errors since sprint 4

This isn’t rare. This is normal.


Small Codebase, Large Failure Surface

Custom application logic often makes up just 1–5% of the total deployed codebase.
And yet, according to a 2017 report by Contrast Security, it causes up to 93% of vulnerabilities.

Why?

Because your code is:

  • The only part that changes weekly

  • The only part no one else reviews

  • The only part tested by two people under pressure

  • The only part that encodes assumptions instead of contracts

You are the one introducing uncertainty into a system that’s otherwise stable by design.

“Standing on the shoulders of giants means nothing if you’re bleeding on them.”


Chapter 2 – Testing Isn’t Optional

“No one praises a surgeon for washing their hands. But everyone remembers when they don’t.”

Testing is not about perfection.
It’s about not infecting the patient.

In software, your “patient” is production.
And every line of untested code has the ability to infect the system.


Testing Is Hygiene, Not Heroism

You don’t test because your code is dirty.
You test because it’s about to touch something critical.

Types of hygiene:

  • Unit tests: Isolate logic. Prevent regressions in small components.

  • Integration tests: Validate assumptions between systems.

  • End-to-end tests: Simulate the messy, unpredictable real world.

If you skip tests, you’re gambling that nothing goes wrong.
That’s not engineering — that’s negligence.


CI/CD is your sink

Manual discipline breaks down under pressure.
That’s why hospitals automate hygiene compliance. You should too.

CI must:

  • Run tests for every commit

  • Fail builds with broken assumptions

  • Enforce linting, style, and type checks

“If your CI lets bugs through, it’s no better than a sink with no soap.”


Chapter 3 – Trust Is Earned Through Testing

“You wouldn’t install a library with no tests —
So why are you shipping code like that yourself?”

In open source:

  • Tests earn trust

  • CI proves maturity

  • Examples guide usage

  • Bad hygiene is a red flag

That’s the standard.
And developers follow it — because it’s visible.


Internal Code Deserves the Same Discipline

The only difference between internal and open source code is accountability.
But users don’t care where the bug came from — they care that it happened.

If your internal module:

  • Has no tests …

  • Silently swallows errors …

  • Uses side effects instead of contracts …

  • Breaks when someone breathes on it …

… it wouldn’t survive 24 hours on GitHub.

So why is that tolerated in your own work?


Make Hygiene Observable

You can’t fix what you don’t measure:

  • Display test coverage

  • Badge your CI results

  • Show failure recovery times

  • Celebrate uptime from prevention, not just firefighting

“Don’t trust code because it’s yours. Trust it because it proves itself.”


Chapter 4 – Culture Eats Checklists for Breakfast

“Most bugs aren’t caused by bad engineers. They’re caused by broken culture.”

Everyone agrees testing matters.
Yet it’s the first thing dropped when deadlines hit or pressure rises.

That’s not a failure of knowledge.
It’s a failure of engineering culture.


Tiredness Is Predictable — Build Systems That Withstand It

Hospitals learned this the hard way:

  • In high-stress environments, handwashing compliance drops

  • Posters didn’t help

  • Systems and visibility did

Your team is no different.

Relying on personal willpower won’t scale.


Testing must be:

  • Enforced through CI

  • Expected in reviews

  • Measured and surfaced publicly (internal)

“Every untested PR that merges is a cultural decision — not just a technical one.”


Reward Boring Brilliance, Not 2AM Heroism

If the engineer who prevented the outage gets ignored,
but the one who patched it at 2:30AM gets praised —
you’re glorifying firefighting over engineering.

Change what you reward:

  • Tests that caught real bugs

  • Refactors that reduced surface area

  • CI improvements that shortened feedback loops

“You don’t notice best sysadmins.
You don’t notice best developers either — until they’re gone.”


Conclusion – Trusted Code Is Tested Code

“If you wouldn’t trust a surgeon without gloves,
why should anyone trust your untested code?”

You operate in a sterile, stable system:

  • Linux: hardened by thousands

  • PHP: versioned, predictable

  • Composer: peer-reviewed libraries

  • CI tools: ready to automate safety

And then your application breaks — because of you.
Not the system.
Not the tools.
Not the third-party code.

Your glue logic.
Your assumptions.
Your missing tests.


Testing is no tech debt. It’s not extra work. It’s not for “enterprise” teams only. Or whatever people say about it.

Testing is basic hygiene.
It’s how you earn trust — trust in your own application.


Call to Action

Today

  • Write a test for the thing you just merged.

  • Turn on CI checks for the project if it’s still running on “good intentions”.

This Week

  • Audit your riskiest integration logic.

  • Wrap it in tests. Track failure rates. Start small.

This Quarter

  • Make tests part of your definition of done.

  • Enforce quality gates in your CI.

  • Treat skipped tests like skipped security reviews — unacceptable.

“Good code is boring.
Safe code is essential.
Tested code is trusted.”

 

AI Writes Code. You Own the ‘Why.’

Mark Zuckerberg says AI will write most of Meta’s code in the next 12–18 months. If your first reaction is dread — congratulations, you’re a developer who already maintained code.

Because the problem isn’t writing code.

It’s understanding it. Maintaining it. Refactoring it two years later without breaking production on a Friday.

AI is getting scary-good at generating code. Not just autocompleting lines — we’re talking entire service layers, test suites, and infrastructure scripts. That feels like a superpower until you realize this superpower has no memory, no system knowledge, and no architectural intuition. Just vibes. “AI tools like GPT can help accelerate this process, but they are not the solution.” – The Wild West of Coding: Why We’re Still Burning Digital Cities – SUCKUP.de

We’re now past the point of discussing whether AI can write useful code. It can. The question is what happens after it does.

And here’s the brutal truth:

AI knows how to write code. It has no idea why the code exists.

It doesn’t know about that performance fix from 2021. It doesn’t understand your team’s domain language. It doesn’t realize that “active” isn’t just a string — it’s a business-critical contract baked into legal compliance.

If we don’t separate generation from intent, we’re going to drown in beautifully structured, semantically useless garbage.

This post presents a two-part strategy:

  1. Define a blueprint format that captures the what.

  2. Encode the why directly into your codebase.

The goal isn’t to stop AI from writing code — it’s to make sure it only writes code that deserves to exist.


Abstraction Got Us Here — But It’s Breaking

Every generation of developers inherits more power and more abstraction.
That power is only useful if we don’t forget how the system underneath works.

Let’s break it down:


🧱 Layer 0: Physical Switches & Punch Cards

Total control. No productivity. Every bit was your problem.

🧠 Layer 1: Assembly

Readable by machine and sad humans. Precision required. Errors fatal.

🔤 Layer 2: High-Level Languages (C, FORTRAN)

You write logic; the compiler handles machine details. This was the first big win: abstraction that didn’t cost traceability.

🧰 Layer 3: OOP and Dynamic Languages

Java, Python, PHP, C#. We got encapsulation, interfaces, and tooling. We also got frameworks, side effects, and runtime mysteries.

🔮 Layer 4: Frameworks, ORMs, DevTools

Laravel, Doctrine, Spring, React, ESLint. Magic happened. And so did performance bugs, leaky abstractions, and ten-minute stack traces.


Now?

🎲 Layer 5: LLMs (Large Language Models)

A stochastic machine trained to guess what code probably looks like based on tokens, not truth.

It’s autocomplete on steroids.
You say “Create a REST API for orders” — it gives you ten files and a Repository that “just works.”
Until it doesn’t.

Because here’s the core issue:

  • It doesn’t understand your domain.

  • It doesn’t know your technical debt.

  • It doesn’t track business rules.

  • It doesn’t care about your security policies.

And teams are merging this output like it’s a pull request from a trusted senior engineer.

Let me be clear:

LLMs are not teammates. They’re not compilers. They’re not even junior devs.

They’re trained to emit high-probability syntax. That’s it.

Yet we’re dropping their output straight into main, bypassing all the trust boundaries we spent decades learning to respect.

Remember the golden rule?

Don’t edit generated code.

We follow it for compilers, transpilers, ORMs, and IaC tools.
But when ChatGPT writes a controller? We treat it like gospel.

That’s technical debt in disguise. And it’s scaling faster than any abstraction layer before it.


The Strategy – Separate the ‘Why’ from the ‘How’

Here’s the fundamental mismatch:

LLMs generate the how. But only humans can define the why.

Yet we’re letting the “how” flow freely into production without anchoring it to the business context, architectural rationale, or historical landmines it depends on.

This isn’t a tooling problem. It’s a systems thinking failure.

To fix it, we need to separate generation from intent, and introduce a strict boundary between code that is guessed and code that is trusted.

Here’s the strategy:


🧱 Part 1: Define a Compiler-Compatible Blueprint Format (Own the Intent)

We don’t want AI writing raw PHP, Java, or Python.
We want it writing structured blueprints that describe behavior, constraints, and flow —
not implementation.

You then build a compiler that transforms these blueprints into safe, production-ready code using your stack, your rules, and your team’s architecture patterns.


✅ Blueprint Example (YAML – Compiler-Ready)

function: getActiveUserEmail
description: Return email of an active user or fail with domain-level exceptions

inputs:
  - name: userId
    type: Domain.UserId

output:
  type: Domain.EmailAddress

rules:
  - businessRule: Only active users can access the system
  - security:
      concern: InfoDisclosure
      severity: High
      mitigation: Throw domain-specific exceptions
  - maintainability:
      smell: MagicString
      notes: Replace 'active' string with enum

steps:
  - fetch:
      from: UserRepository
      method: findById
      input: userId
      output: user
  - guard:
      if: user == null
      then: throw DomainError.UserNotFound
  - guard:
      if: user.status != ACTIVE
      then: throw DomainError.UserInactive
  - return:
      value: user.email

⚙️ Why This Works:

  • Compiler-friendly: Each step maps cleanly to deterministic code generation.

  • LLM-compatible: Easy for AI to generate and validate.

  • Auditable: You can version this. You can diff it. You can reason about it.

  • Stack-agnostic: One blueprint, many possible code outputs (Laravel, Symfony, NestJS, Spring).

  • Intent-driven: You encode what needs to happen — and enforce how through rules.

Your compiler becomes the enforcement layer:

  • It checks the blueprint against your domain model.

  • It injects architecture-specific behavior (validation, DI, error handling).

  • It produces safe, maintainable, consistent code.

Blueprints are your contract with the machine.
Your compiler is the gatekeeper.
LLMs are just assistants — they don’t write production code; they write proposals.


🧩 Part 2: Embed the ‘Why’ Directly Into Your Codebase (Own the Context)

You don’t just need structure in generated code —
You need context in your existing code.

That’s where metadata comes in: structured, machine-readable, developer-accessible annotations that tell humans and LLMs why a piece of logic exists.


✅ Example (PHP Attributes)

#[BusinessRule("Only active users may access features")]
#[Security(concern: "UnauthorizedAccess", severity: "Critical")]
#[Maintainability(smell: "MagicString", notes: "Replace 'active' with enum")]
public function getActiveUserEmail(UserId $userId): EmailAddress
{
    // ...
}

These attributes:

  • Can be enforced in CI with static analysis (PHPStan, Psalm, custom Rector rules)

  • Provide structure for documentation generation

  • Are readable by future devs and future AI tools

  • Make implicit decisions explicit

Is this over-engineering?
Or is it the minimum bar if you expect maintainable AI-integrated systems?

Let’s be honest: if we’re still coding the same way five years from now — merging raw AI output without structure, rules, or traceability — it won’t be because it works.
It’ll be because we never had the discipline to build something better.

 

Why Codebases Rot Like Kitchens (and How to Stop It)

🍳 Intro: Why the Knife in the Sink Matters

A cluttered kitchen doesn’t happen all at once.

It starts with one knife left in the sink. One jar not put back. A cutting board with a little oil on it. Nobody panics. Nothing’s “broken.”

But over the next few hours—or days—something shifts.

You can’t find the clean spatula. The counter is sticky.
Cooking becomes frustrating.

And the worst part? You don’t notice the mess until it’s already changed how you behave.

  • You stop wiping.
  • You stop putting things back.
  • You start contributing to the chaos.

That’s the hidden cost of inconsistency: it doesn’t just create disorder.
It lowers the standard of care—until disorder becomes the default.

Codebases rot the same way.

  • It’s rarely one big refactor gone wrong or some infamous PR.
  • It’s a hardcoded string here, a missing test there.
  • A “temporary” workaround that never gets revisited.
  • A helper class that quietly becomes a dumping ground.

CI is green. Static analysis doesn’t complain.
But somehow, everything feels heavier than it used to.

This post is about that slow drift—and how to stop it.

Not with heroic rewrites. Not with prettier configs or PSR-12 debates.
But with a deeper understanding of what consistency really is, why it matters more than perfection, and how to build codebases that clean themselves—file by file, day by day.

Because the same principle that keeps kitchens functional over time applies to software:

Mess invites mess.
But more powerfully: order invites more order.

When everything has a place, it tends to stay in place.


🥄 1. Mess Invites Mess, But Order Invites More Order

Leave a knife in the sink, and somehow it gives everyone else permission.
A spoon shows up. Then a pan.
And by the end of the day, no one wants to clean—because now it’s a mess.

But if the counter is clear, the sink is empty, and everything’s where it belongs?
People wipe up right after chopping.
They put the spices back without being asked.

The environment sets the standard.
Not rules. Not motivation. Just what’s already true.

Code works the same way.

You open a file where everything is named clearly, small functions are composed intentionally, and the layout flows like it was built on rails—what do you do?
You match the tone. You respect the structure. You add with care.

But if you open a file that’s messy—unstructured, inconsistent, unpredictable—you patch it with a workaround. Or worse, you walk away.
Not because you’re lazy, but because the code is already signaling that quality isn’t expected here.


🧠 Consistency Sets the Behavioral Baseline

Mess doesn’t just grow because things break.
Mess grows because people adapt downward.

Even a “minor” inconsistency can cause disproportionate damage:

  • A misnamed method signals that naming isn’t important.

  • A missing test tells new devs testing is optional.

  • One file ignoring the service structure invites others to do the same.

It’s not about enforcement.
It’s about momentum.

A consistent codebase isn’t perfect—it’s predictable.
And predictability reduces friction for every contributor.


🧪 Real-World Parallel: The Clean Kitchen Effect

Behavioral psychology has a name for this: environmental priming.
People unconsciously mirror the standards around them.

In one study, a clean hallway with lemon-scented cleaner in the air made people more likely to pick up litter.
In another, visible disorder led to higher rates of dishonesty and vandalism.

Why?
Because humans tune their behavior to what seems “normal.”
And codebases are no different.


🛠 In Practice

In well-maintained projects:

  • New contributors write better code without being told.

  • Code reviews focus on logic, not formatting or structure.

  • Team velocity increases—not because of speed, but because of reduced friction.

In messy ones:

  • People hesitate to touch files.

  • Reviews become emotional minefields.

  • Every fix feels like pushing through mud.

So if you want better code without preaching or micromanaging?

Don’t write a new rule.
Write one clean, consistent file. And make it the new normal.

That’s how kitchens stay clean.
That’s how codebases stay maintainable.


🧠 2. Clean ≠ Structured. Structure = Predictable.

From a distance, the kitchen looks fine.
Counters wiped. No visible clutter. The sink is empty.

But then you open a drawer and find three different can openers.
The fridge is overstuffed with expired ingredients.
The spice rack is alphabetical—except for the one jar you need.

It’s not dirty. But it’s disorganized. And suddenly, something as simple as boiling pasta feels… harder than it should.

Codebases can fool you the same way.


🧼 Visual Cleanliness Hides Structural Rot

Developers often confuse style consistency with structural integrity.

“We’re following PSR-12—we’re good.”

But the real friction doesn’t come from indentation.
It comes from behavioral inconsistency:

  • A function mutates state but reads like it doesn’t.

  • A service mixes validation and persistence in the same method.

  • Ten files named Helper.php doing wildly different things.


🔍 Structure Creates Trust

When structure is consistent:

  • You can guess functionality from a filename.

  • You know where to add logic without asking.

  • You focus on what needs to change—not how to fit it in.

When structure is inconsistent:

  • Every file becomes an investigation.

  • Every change feels like a guess.

  • Every review becomes a negotiation.


🧠 Cognitive Load Theory: Death by Paper Cuts

Humans can only juggle 4–7 items in working memory at a time.

Every inconsistency:

  • Forces a context switch.

  • Introduces a new mental rule.

  • Steals attention from the actual problem.

Even if you never say it out loud, your brain reacts:

“Wait… how does this one work again?”

That hesitation is the real tax on team velocity.

Not slow builds.
Not flaky tests.
Just devs wasting brainpower navigating inconsistency.


🍳 Back to the Kitchen

Imagine:

  • Every time you cook, you spend 3 minutes looking for a pan.

  • You burn dishes because spices are mislabeled.

  • You break a plate trying to cram it into the wrong cabinet.

Eventually, nobody wants to cook.
Not because they can’t.
But because the system stopped helping.


🛠 In Practice

Here’s what actually helps:

  • Stop obsessing over formatting. Start enforcing structure.

  • Codify naming rules, error-handling patterns, return type expectations.

  • Maintain one golden file per feature type—let code teach code.

  • Use tools like Rector and PHPStan not just for correctness, but for design drift.

Anyone can clean a file.
But only structure makes it stay clean the next day.

Structured systems aren’t pretty.
They’re repeatable.


🔁 3. Consistency as Workflow, Not Willpower

Nobody wants to scrub a greasy pan.
But wiping the counter after slicing onions? Easy.
Tossing a sponge in the dishwasher? Automatic.
Taking out the trash because the bin is right there? Obvious.

Clean kitchens stay clean not alone through motivation—
but through systems that make the right action frictionless.


🤖 Stop Relying on Memory and Good Intentions

Developers love to say:

“Always write tests.”
“Stick to the architecture.”
“No more fat controllers.”

And for a week, it works.
Until someone’s tired.
Or new.
Or didn’t know the rule existed.

Then entropy wins—because you bet the house on willpower.


🔂 Ritual Beats Resolution

The teams that stay consistent aren’t more disciplined.
They’ve just made quality the path of least resistance.

They don’t debate style in every PR.
They don’t forget to test.
They don’t wonder where a new file belongs.

Why?

Because the workflow remembers for them.


⚙️ Make Quality Automatic

Good teams embed consistency into the process:

  • Pre-commit hooks

    No format, no type safety, no commit. End of story.

  • CI pipelines that validate behavior, not just syntax

    • Static analysis checks

    • Mutation coverage checks

    • Architectural boundaries checks

    • Naming conventions, return types, dead code checks

  • Code generators and file stubs

    Create a new controller? It already has the right layout.

  • “Golden” examples in-repo

    Skip the 12-page style guide. Copy the cleanest module.

  • Micro-rituals in code review

    • PR checklist: “Did we leave this better than we found it?”

    • Review warm-ups: “What’s the pattern here?”


🍂 The Kitchen Analogy Again

Want people to compost?
Don’t hide the bin in a closet.
Put a small, open one next to the cutting board.

Same principle in code:

If you want consistency, design the workflow
so that the right choice is the easiest one.


🛠 In Practice

Ask your team:

  • What do we correct in every PR?

  • What decisions keep getting re-explained?

  • What slows us down the most often?

Then build automation or scaffolding to solve just that.
Start with friction hotspots.

You’re not building bureaucracy.
You’re removing ambiguity.

Every time the system answers a question,
that’s one less decision the developer has to make.


Consistency doesn’t come from preaching.
It comes from design.

Design your workflow to default to quality.
Then watch the team rise to meet it.


🏗 4. Design for the Future, Not the Fix

There’s a kitchen that looks spotless—today.
But the trash can is too far from the prep station.
The knives are in the same drawer as the ladles.
There’s no counter near the stove.

Cooking technically works.
But every step is awkward. Every movement inefficient.
Eventually, people stop using it—not because it’s messy, but because it’s exhausting.


🧱 Codebases Rot the Same Way

They don’t collapse from mess.
They degrade from accumulated friction:

  • Logic split between controller, service, and a random helper.

  • Naming that hides purpose (ServiceHandler, ThingManager).

  • Features wedged into whatever file felt “close enough.”

You can still add features.
You can still ship.

But every change feels like surgery—because the architecture doesn’t support the work.


📐 Good Architecture Is Predictable

A good system isn’t one where code just works.
It’s one where new code fits.

If you can’t tell where the next feature goes,
you don’t have architecture—you have coincidence.


🧠 Architecture = Consistency at Scale

At the local level, consistency is about naming and formatting.
At the system level, it’s about boundaries and repeatability:

  • Controllers control. Services do business logic. 

  • Every feature follows the same shape.

  • Domain logic lives with the domain—not scattered across folders.

  • Folder structure maps to business concerns, not technical artifacts.


🧪 What This Looks Like in Practice

  • 🧩 Folders map to domains, not tech types

    • Billing/Invoices/MarkAsPaid.php

    • Not Services/InvoiceService.php

  • 🧱 Vertical slices over horizontal layers

    • Group by feature, not file type.

  • 🧠 Structure reveals design decisions

    • Your DDD shouldn’t live in a Confluence page.

  • 🚫 No catch-all files

    • If you have Helper.php, you’ve already lost the thread.

  • 🔁 One way to do a thing

    • Commands look like commands. Events look like events.

    • Failures throw exceptions. No silent null.


🌪 Most Systems Drift by Default

Architecture rarely dies from bad decisions.
It dies from unopposed ones:

  • A shortcut nobody reverts.

  • A pattern nobody questions.

  • A new dev copying a bad file because “that’s how it was done.”

If nothing enforces the rules, there are no rules.


🔐 Architecture Is Operational Psychology

It should tell your team:

  • Where things go

  • What “right” looks like

  • How to move safely and fast

If it doesn’t, your devs will guess.
And every guess will pull your system further apart.


Design not for what’s urgent.
Design for what’s needed.

Make your architecture answer questions before they’re asked.
Make it obvious where new work belongs.

If you don’t, the next “fix” will be a wedge.
And ten wedges later, you’re back in kitchen hell.


🕊 5. Consistency Isn’t Control. It’s Clarity.

“Don’t tell me how to code.”

We’ve all heard it.
Some of us have said it.

Because too often, consistency feels like micromanagement
Like senior devs enforcing their quirks instead of team standards.

But that’s the wrong frame.

Real consistency isn’t about control.
It’s about removing guesswork.


🧭 The Right Kind of Freedom

In a well-designed kitchen:

  • You don’t need to ask where the knife goes.

  • You don’t need a manual to find the trash.

  • You just move—freely, fluidly, confidently.

That’s not restriction. That’s clarity.

And clarity is what brings developers into a productive flow.


🔍 Predictability = Velocity

When a developer opens a new module and can instantly tell:

  • Where logic lives

  • How errors are handled

  • What a test should look like

They’re not slowed down.
They’re unlocked.

No waiting on context.
No decoding inconsistent patterns.

Just building, fast—and safely.


🛠 What Consistency Really Gives You

  • 🧠 Clarity → Fewer decisions, fewer mistakes

  • 🚀 Momentum → No mental pauses to decipher structure

  • 🛠 Autonomy → New devs can own changes without fear

  • 🤝 Trust → Reviews shift from “What is this?” to “How well does it solve the problem?”

This isn’t about obedience.
It’s about shared expectations.

The structure does the explaining.
The docs just reinforce the why.


🧨 The Wrong Kind of Freedom Slows Teams Down

“Just do what you think is best” sounds supportive.
But it opens the door to chaos:

  • Someone rewrites a module in a new paradigm.

  • Another adds raw SQL into a system using Doctrine.

  • A test is written in a completely different pattern.

Now everyone is “free”—and everyone is misaligned.

Velocity drops.
Reviews turn into rework.
Nobody trusts anything.


🎯 Teams Scale on Predictability, Not Talent

The best teams aren’t made of superstars.
They’re made of people who can move independently and still produce code that fits together.

That only works when the system defines consistency as clarity, not constraint.


Clarity makes creativity possible.
Predictability enables flow.
Shared structure builds trust.

That’s what real consistency gives you.

Not control—coherence.


🧹 Conclusion – Make Order the Default

Kitchens don’t stay clean because someone yells.
They stay clean because the system makes care easy:

  • The trash is close.

  • The knives are where you expect.

  • The dishwasher has a rhythm.

No micromanagement.
No friction.
Just design that encourages discipline.


🧑‍💻 Codebases Work the Same Way

You don’t need perfect code.
You need a foundation that:

  • Makes the right thing obvious

  • Makes the wrong thing feel weird

  • Makes improvement the path of least resistance

Mess invites mess.
But more importantly: order invites order.

When that order exists:

  • Developers contribute with confidence

  • Refactors happen in flow

  • New features fit in like they belonged from day one

Not because your team is better.
But because your system is smarter.


🧠 What You Can Do Today

 

Start small. But start with purpose.

  • Touch a file? Leave it cleaner than you found it.

  • Write a new service? Copy the best one—your template of excellence—not the most recent hack.

  • Add a pre-commit hook that blocks architectural drift, not just formatting violations.

  • Pick one “golden” module. Let it define what “done right” looks like.

Then build forward from that standard—consistently.

  • Turn your golden module into a scaffold.

  • Generate code that already follows the rules.

  • Automate structure, not just syntax.

Example:

make create:datatable InvoiceTable

Now every new DataTable starts clean, typed, and consistent—before anyone touches a line.

That’s how you scale quality.
Not by working harder. But by making the right thing the easiest thing.

The Inner Code Plumber

How Adaptive Systemic Intuition Lets You See Maintainability Before It Breaks

You open a PHP file. No red flags. No screaming errors. The CI was green. Static analysis reports nothing, “all good.” But something feels off.

  • The methods are over-explained and under-meaningful.
  • The logic is layered like geological sediment.
  • The file smells like tech debt, even if the tools won’t say so.

-> And there it is—that tight feeling in your gut.

Not panic. Not certainty. Just a whisper: “This will come back to bite us.”

That feeling isn’t mysticism. It’s not arrogance. It’s not just your “senior dev sixth sense.”

It’s something deeper—something more universal. Your experience is surfacing as Adaptive Systemic Intuition.

Just like a seasoned plumber can walk into a building and know, from the creaks and bends of the pipes, that trouble’s coming—even if everything flows for now—your brain is surfacing thousands of hours of coding, debugging, and review into a silent warning.

This post isn’t about patterns like SOLID or DRY.

  • The pattern behind those coding patterns.
  • The one you feel before you can explain.
  • The one that lets you see maintainability before a single bug is reported.
  • This is the abstract pattern. And it lives in you.

-> Let’s dig it up, name it, and sharpen it.

1. Code Through the Lens of “Why,” Not Just “How”

Fresh developers obsess over how.

  • How to write the syntax.
  • How to pass the test.
  • How to make the thing work.

But experienced developers? They ask why.

  • Why does this code exist?
  • Why is it structured this way?
  • Why are we solving this problem now, and what will it look like later?

It’s not about getting the function done. It’s about aligning the code with the context: the business goal, the system constraints, the team’s skill level, the organizational habits.

You’re not coding in a vacuum. You’re coding in an ecosystem.

Plumber Thinking:

You’ve already internalized this. In your code, structure reflects purpose.

When you clean up legacy code, you don’t just rewrite—you reflect: “What problem was this solving? Why this shape? Is that still true?”

Plumber Analogy:

A rookie plumber checks if the showerhead is clogged.

A seasoned plumber asks: “Is this the only tap with low pressure? What changed recently?”

Same situation—completely different insight.

Coding Wisdom:

“Simplicity in code isn’t about writing fewer lines; it’s about writing the clearest lines for the specific problem and its environment.”

Practice:

  • Ask yourself: Why does this feature even exist?
  • Review code with this lens: Does this structure still match its purpose?
  • Refactor not because you can, but because the current shape no longer serves the system’s real needs.

2. Feel the Friction: Your Body Knows Bad Code

You open a file.

Before reading a single line in detail—before reasoning, parsing, or even naming—the discomfort creeps in.

A sense of tension. A raised brow. A shift in posture.

You don’t have the error, but your body already knows: something’s off.

This isn’t new-age nonsense. It’s embodied cognition. Your thinking isn’t just in your head—it’s in your body.

  • You gesture when explaining bugs.
  • You pace when searching for the right name.
  • You draw to reason through architecture.

-> These aren’t quirks. They’re your body reasoning.

Plumber Thinking:

  • You already use this intuitively.
  • You whiteboard before writing complex logic.
  • You pause when nested logic feels brittle—even before tests fail.
  • You’ve said it yourself: naming, structure, nesting—all send physical signals.

-> Your gut? It’s hardware-accelerated static analysis.

Plumber Analogy:

A good plumber doesn’t just look—they listen for vibration, feel for pressure, sense the strain in old joints.

Code is no different. You don’t just read it—you sense it.

Coding Wisdom:

“Often, the feeling that something is wrong precedes the logical explanation. Your body, tuned to patterns, senses dissonance before your mind catches up.”

Practice:

  • Pay attention to physical signals: tight chest, hesitancy, frustration—these are pattern alerts.
  • If you can’t name the problem yet, don’t dismiss the feeling. Pause. Map it.
  • Sketch data flows before implementation. Where the drawing becomes messy, the code will too.

3. Debug in Your Head: Your Brain’s the Best Tool

A bug shows up.

The junior instinct? Jump into the code. Drop in var_dump(), sprinkle logs like confetti, stare at traces.

The senior instinct?

Pause; Think; Mentally simulate;

They don’t open the IDE immediately—they open the model in their mind.

They replay the logic, walk the data, test their assumptions internally.

This isn’t laziness—it’s efficiency.

Debugging is about reconciling expectation vs. reality. Tools help, but mental simulation is where the real gap is closed.

Plumber Thinking:

You don’t guess. You model.

When something breaks, your instinct isn’t to “try stuff”—it’s to ask: What’s the underlying assumption here?

You’ve trained yourself to see the root cause, not just the symptom.

You even help others find it by making them talk through their own code logic—a classic senior move.

Plumber Analogy:

A customer says, “No hot water.”

The amateur checks the heater.

The master plumber?
“Does it happen with every tap?”
“When did this start?”
“Was there plumbing work recently?”
They construct a systemic model before touching a single tool.

Coding Wisdom:

“Every bug is a surprise—an error in your mental model. Fixing code is easy. Fixing the model? That’s where real debugging lives.”

Practice:

  • When a bug appears, narrate what you expect should happen.
  • Walk the logic mentally before altering anything.
  • Ask others: “What do you think is happening, step by step?” Make the model visible.

4. Expect the Expected: Your Brain Predicts Code

As you gain experience, something strange happens:

You start expecting code to behave a certain way—even before reading the details.

You see a method named getUserToken(), and your brain assumes:

  • No side effects
  • Probably returns a string
  • Definitely doesn’t mutate state

If it does mutate state or throw an exception—it feels wrong.

That feeling? It’s not being picky. It’s your brain’s predictive model raising a flag.

Your mind isn’t just parsing—it’s forecasting.

When the code breaks that forecast, your cognitive system yells: Prediction error detected.

Keep in mind how developers actually process code:

  • Our working memory is small and gets overwhelmed quickly.

  • Our long-term memory excels at recognizing well-named, consistent patterns—especially when they match previously learned mental models.

That’s why fetchByNameOrThrowException(): User and fetchByNameIfExists(): ?User are vastly superior to fetchByName(): ?User. They encode intent directly into the name and return type. This isn’t nitpicking—it’s cognitive ergonomics. When naming clearly expresses behavior, you reduce cognitive load, prevent misreads, and accelerate understanding.

Memory Type Computer Analogy Developer Brain Function Coding Example
Working Memory (WM) RAM / CPU Cache (Small, Fast, Volatile) Active space for reasoning: tracking variables, current scope, flow. Limited (~4–7 chunks), disrupted easily. Debugging a loop: i = 0, item = user[0], tracking state = processing. Holding these while stepping through.
Long-Term Memory (LTM) Hard Drive / Database (Vast, Fast-Indexed when chunked) Stores learned patterns, idioms, APIs, naming conventions. Fast if knowledge is chunked (schema). Used for prediction. Recognizing getUserToken() as: no side effects, returns string. Instantly guessing method purpose & contract.
Procedural Memory BIOS / Firmware / Muscle Memory Automates repeatable behavior. IDE navigation, CLI fluency, common patterns. Invisible but critical for flow state. Typing function __construct( without thinking. Instinctively spotting and refactoring a code smell.

Plumber Thinking:

You’ve written consistently predictable code for years—typed, immutable, clean.

You’ve trained your brain to recognize rhythm, detect side effects, and enforce mental contracts.

You’ve said it yourself: “Naming, types, structure—they’re not just syntax, they’re signals.”

And when that signal’s broken? You feel it immediately—even if the CI doesn’t.

Plumber Analogy:

A plumber sees a pipe coupling that’s slightly misaligned.

No leak—yet. But something about the tension, the angle, the material… they know.

“That’s going to burst under pressure.”

Their prediction engine fires before failure.

Coding Wisdom:

“Well-written code teaches your brain what to expect. Violating those expectations isn’t clever—it’s cognitive sabotage.”

Practice:

  • When writing code, ask: What would another dev reasonably expect this to do?
  • When reviewing code, note every “surprise.” Why was it surprising?
  • Before running a method, predict: What will it return? What will it change? Then verify.

5. See the Shape: Visual Cues in Code

Before your eyes process names or logic, they notice shape. I already wrote a blog post about it 5 years ago: Do Not Fear The White Space in your Code – SUCKUP.de

Indentation. Whitespace. Block structure. Line rhythm.

You scan a file and just feel overwhelmed—or calm—before you’ve even read a single line.

That’s not a visual aesthetic thing. It’s a cognitive load thing.

When code looks chaotic, your brain assumes it is chaotic.

When it’s cleanly structured? Your mind can glide through it, predict it, trust it.

You’re not just reading lines. You’re navigating a landscape.

And cluttered terrain slows thinking.

Plumber Thinking:

You’ve long preached visual clarity.

Your spacing, your grouping, your naming conventions—they’re designed for legibility, not minimalism.

You know that white space is a cognitive tool, not wasted real estate.

You’ve even pointed out how nesting alone can induce bugs—because our brains lose scope perception when structure collapses.

Plumber Analogy:

Imagine opening a cabinet under a sink.

In one house, the pipes are clean, labeled, evenly spaced.

In another, they’re twisted, duct-taped, looping behind the drywall.

You already know which one will leak.

Coding Wisdom:

“Visually well-structured code isn’t pretty—it’s predictable. And predictable structure reduces mental strain.”

Practice:

  • Don’t just reformat for style—reformat for flow. Group related logic. Use white space meaningfully.
  • When reading legacy code, pause before diving in. Let your eyes scan shape. Does it suggest complexity?
  • Reformat deeply nested code just to feel its structure—see how much simpler it becomes.

6. Narrate the Flow: Sharing Your Mental Model

Experienced developers don’t just refactor silently. They narrate.

“I’m splitting this out because the logic forks too early.”
“I renamed this to make its responsibility explicit.”
“This service is the real boundary—we need to show that.”

They aren’t just explaining code. They’re revealing the mental model that shaped it.

Code is communication. And communication isn’t just what you write—it’s what others understand.

If they can’t follow your thinking, your code is a liability—no matter how clever it is.

Naming, abstraction, comments—these aren’t documentation. They’re narrative tools. They encode the architecture in other people’s minds.

Plumber Thinking:

You’ve internalized this.

When you restructure legacy code, you don’t just fix it—you expose its intent.

You use naming, typing, and PHPDoc to express meaning with precision.

You’ve said clearly: “Readable code tells a story. It has to be deletable and understandable—or it doesn’t belong.”

You narrate for the future reader—often yourself.

Plumber Analogy:

A plumber explains a reroute not with schematics, but clarity:

“We added a shutoff here so you can isolate the leak without cutting the entire house.”

The client gets it.

The next plumber gets it.

The design becomes shared knowledge.

Coding Wisdom:

“Readable code isn’t just syntactically correct—it’s semantically shared. It transmits the original model without distortion.”

Practice:

  • During code reviews, ask: Can I explain why this exists, not just how it works?
  • Use naming to encode roles and relationships, not just behavior.
  • Use lightweight comments to narrate intent—why this path was chosen.

7. Tools Are Great, But Your Brain’s the Boss

Linters. Static analyzers. Formatters. AI pair programmers.

They’re everywhere—and yes, they’re useful.

But don’t confuse assistance with authority.

A tool can tell you that a method is too long.

  • It can’t tell you whether it’s cohesive.
  • It can flag a missing type.
  • It can’t sense that the naming is misleading, or that the abstraction is rotting.

-> Your intuition? It gets there before the warning light comes on.

The danger isn’t using tools—it’s outsourcing your judgment to them.

Take PHPStan’s classic warning:

“Return type ‘User|null’ might be null here.”

Most developers see that and rush to “fix” it.
By fix, they usually mean: add a null check, throw an exception, or change the type hint to ?User.

But that’s not a fix—it’s a reflex.

What you should ask is:

  • Why is null a possible return in the first place?
  • Does this function really model optionality—or are we just afraid of declaring intent?
  • Was this ever meant to fail silently? Or did we just avoid thinking about the default case?

Here’s the brutal truth:
If the domain logic doesn’t expect a missing user, then returning ?User is a design error—not a typing issue.
It misleads every caller into treating the result as optional when it’s not.

Tools report symptoms. It’s your job to diagnose the cause.

So no—you don’t just slap on a null check.
You stop and rethink the contract:
Should this be fetchUserOrFail(int $id): User?
Or findUserIfExists(int $id): ?User—with the ?User explicitly declaring safe optionality?

Because every ?T is a semantic signal. If the meaning behind it is unclear or inconsistent, you’re not writing safe code—you’re writing ambiguous code that compiles.

Plumber Thinking:

You’ve automated everything that matters: PHPStan, Rector, PHP-CS-Fixer.

But you’ve also said clearly: “They catch noise. I care about signal.”

You still scan for intention, context, and design cohesion.

You don’t accept autofixes blindly—you ask, “Would I have made this change? Does this align with the system’s needs?”

Your gut is the boss. The tool is the assistant.

Plumber Analogy:

A plumber might use a pressure gauge, but they’ll still touch the pipe.

They’ll still listen for the high-pitched whine.

They’ll still trust the feel of a bad valve—even when the sensor says “normal.”

Why? Because machines don’t have experience.

Coding Wisdom:

“You can’t automate what you don’t understand. Tools amplify wisdom—but they can’t manufacture it.”

Practice:

  • Before running a tool, assess the code yourself. What feels off? What seems brittle?
  • When a tool suggests a change, ask why. Would you make it manually?
  • Build your own internal checklist. Use tools to confirm—not define—your decisions.

8. Reflect to Learn: Making Experience Stick

  • Writing code isn’t enough. Fixing bugs isn’t enough.
  • Even surviving legacy systems isn’t enough.
  • Reflection is where experience becomes intuition.

-> When something breaks—don’t just patch it.

Ask: Why did I expect it to work?

What assumption failed? What context changed?

That’s the real learning.

Every prediction failure is an opportunity to sharpen your internal model. Without reflection, you’re just firefighting. With it, you’re upgrading your mental OS.

Plumber Thinking:

You reflect by default.

Your retrospective discipline, your journaling of code decisions, your constant search for why something broke—that’s not routine. That’s professional cognition in motion.

You don’t just fix problems—you extract principles from them.

You’ve even asked yourself: “What would have prevented this mess five years ago?”

That’s elite-level introspection.

Plumber Analogy:

The apprentice replaces a faulty valve.

The veteran asks:

“Why does this kind of valve always fail in this setup?”
“What can I do to make the next failure impossible?”

They’re not just fixing—they’re building resilience into their understanding.

Coding Wisdom:

“Experience becomes expertise when filtered through reflection. Without that, you’re just aging, not learning.”

Practice:

  • After every tricky bug, write down the mistake in your mental model, not just the fix.
  • Use postmortems as a thinking tool, not a blame ritual.
  • When reading legacy code, ask: Why did they do it this way? What were the constraints then?

9. Principles Are Guides, Not Gospel

We love our principles.
SRP. DRY. KISS. YAGNI. SOLID.

They’re powerful. They’re useful. They’re even beautiful—when applied wisely.

But here’s the uncomfortable truth:
They’re not laws. They’re heuristics.

Applied blindly, they can create complexity instead of clarity. You’ve seen it: a jungle of micro-classes in the name of “SRP.”

  • A twisted abstraction tree justified by “DRY.”
  • A thousand interfaces no one asked for—just to “follow OCP.”

-> Principles must serve the code—not the other way around.

Plumber Thinking:

You live this tension.

You respect principles, but you don’t worship them.

You’ve rejected unnecessary abstractions that only exist to check a box.

You’ve said it yourself: “Deletable code matters more than theoretical purity.”

Your approach to DRY is practical: reduce harmful duplication, not introduce brittle indirection.

You favor clean pragmatism over cargo-cult architecture.

You don’t just follow rules—you ask why the rule exists, and when it stops helping.

Plumber Analogy:

A textbook might say to use a certain type of pipe junction.

But the veteran plumber on-site knows the real-world pressure, the angles, the space.

He breaks the “rule”—and the system works better for it.

Why? Because he understands the principle behind the rule, not just the rule itself.

Coding Wisdom:

“True mastery is knowing when to bend the rules—without breaking the system.”

Practice:

  • When applying a principle, ask: What problem is this solving here?
  • Audit patterns. If you’re layering abstractions “just because,” stop.
  • Teach juniors the context behind the principle—not just the acronym.

Final Thought: Trust Your Inner Code Plumber

  • You don’t need an error to know code is wrong.
  • You don’t need a profiler to sense a bottleneck.
  • You don’t need a tool to tell you a class is trying to do too much.

-> That whisper in your gut? That unease in your posture?

That’s Adaptive Systemic Intuition.

It’s not mysticism. It’s the compound interest of years spent reading, breaking, fixing, refactoring, and thinking.

Like a master plumber who hears a hum and knows what’s about to rupture—

  • you feel complexity before it becomes entropy.
  • you sense rigidity before maintainability collapses.

-> And most importantly: you can train this.

  • Every refactor.
  • Every naming battle.
  • Every painful legacy rescue mission.
  • Every “this feels wrong” moment that you chase down to root cause.

-> They all sharpen the pattern behind the patterns.

Call to Action: Trust—and Hone—Your Gut

Next time you open a file and it just feels “off,” do this:

  • Pause. Listen to the discomfort. Don’t ignore it.
  • Narrate. Try explaining why it feels wrong. Is it naming? Flow? Entanglement?
  • Mentally simulate. Imagine a small change—does it cascade dangerously?
  • Reflect. Think about other systems that failed for the same reasons.
  • Use tools, but interpret their output through your experience.
  • Talk it out. Share your gut feeling with another dev—see if it resonates.
  • Iterate. Turn every failure into pattern recognition. Make it systemic.

“Great code isn’t just functional. It feels right to work with.”

That feeling? That’s your internal plumber doing their job.

Let them work.

The PHPDoc Guide (2025 Edition)

  1. String-based pseudo-types

  2. Numeric & bounded pseudo-types

  3. Arrays, lists, and shapes

  4. Object & class-based types

  5. Callable types

  6. Generics with @template, @extends, @implements

  7. Constant values and enum simulations

  8. Conditional logic

  9. Pass-by-reference & @param-out

  10. False positive suppression

  11. Best practices

  12. Cheatsheet


 

You can’t trust a string. Or an array. Or half the type declarations you see in legacy PHP codebases.

And that’s not PHP’s fault. It’s yours—if you’re still writing function signatures like this:

function sendData(array $data): bool

This tells us nothing.
Not what’s in $data, not if keys are optional, not what the return bool even means.

PHPStan fixes this. With PHPDocs. Not for decoration. Not for old-school docblocks. But as strict, analyzable type contracts — guardrails for real-world codebases.

If you’re serious about:

  • Catching logic errors before runtime

  • Documenting your code without duplicating logic

  • Scaling PHP safely without turning everything into a typed mess

Then this guide is for you.


We’ll walk through real-world PHPStan PHPDoc patterns—from pseudo-types and generics to conditional logic and type-safe constants. All based on PHPStan, tested in real-world projects, and packed with concrete examples and copy-paste snippets.

No fluff. Just clean, pragmatic PHPDoc annotations that make your code safer and more readable. The best part? PhpStorm supports nearly everything we’re doing here out of the box — so code completion improves, and your overall DX gets a serious upgrade.

Let’s upgrade your PHPDocs from “comments” to contracts.


Background: Why PHPDocs Matter More Than Ever


PHP has come a long way since the wild west of PHP 5.x.

Today we’ve got:

8.0 with union types and constructor property promotion, …
8.1 with enums and readonly properties, …
8.2 with readonly classes and DNF types, …
8.3 with typed class constants and #[\Override], ...
8.4 with property hooks and asymmetric visibility, …

So… why bother with PHPDocs at all?


What Native Types Still Can’t Do

Native PHP types help—but they stop at the surface.

Native Type Can Do Can’t Do
string Ensure value is a string Can’t check if empty, numeric, constant
int Enforce integer input Can’t limit to > 0 or within bounds
array Accept arrays Can’t validate keys, shape, or index types
object Accept objects No info about which class, structure, or generics
bool Boolean logic Can’t express “this is only true on success”

That’s Where PHPStan Comes In

PHPStan treats your PHPDocs like a type system:

  • Refines scalar types (non-empty-string, positive-int, numeric-string)

  • Models structured arrays (array{key1: T, key2?: T})

  • Adds generics (@template T, Collection<T>)

  • Enforces contracts and conditionals (@assert, @return (T is Foo ? Bar : Baz))

  • Tracks pass-by-reference mutations (@param-out)

  • Restricts literal values ('asc'|'desc', Class::CONST_*, enums)

This gives you defensive typing for a dynamic language.


1. String-Based Pseudo-Types

Refine what “string” actually means


In PHP, string is a blunt instrument. It could mean:

  • "admin" – a valid username

  • "" – an empty input nobody should see

  • "123" – a number pretending to be a string

  • "SELECT * FROM users WHERE user_id = " . $id – a dangerous SQL statement

PHPStan sharpens this instrument with string pseudo-types that define intent, constraints, and trust boundaries.


Overview: String-Based Pseudo-Types

PHPStan Type Description Native Equivalent
string Any string, including '' string
non-empty-string String that cannot be '' string
numeric-string String that is guaranteed to represent a number ("123.45") string
literal-string A string known at compile-time (e.g. hardcoded, not user input) string
callable-string A string name of a globally callable function string
class-string<T> String that is a fully-qualified class name (optionally of type T) string

Real-World Examples


non-empty-string

Guarantees a string is never empty:

Playground | PHPStan

/**
 * @param non-empty-string $username
 */
function setUsername(string $username): void {
    // Safe, PHPStan guarantees it's not ''
    saveToDatabase($username);
}

setUsername('alice'); // OK
setUsername(''); // PHPStan error

numeric-string

Used when string inputs must represent numbers (e.g., form inputs):

Playground | PHPStan

/**
 * @param numeric-string $amount
 */
function convertToCents(string $amount): int {
    return (int)((float) $amount * 100);
}

convertToCents("19.95"); // OK
convertToCents("abc"); // PHPStan error

literal-string

Guards against injection vulnerabilities in SQL or dynamic calls:

Playground | PHPStan

/**
 * @param literal-string $sqlQuery
 */
function runQuery(string $sqlQuery): void {
    DB::raw($sqlQuery); // Only compile-time constants allowed
}

runQuery("SELECT * FROM users"); // OK
runQuery($_GET['query']); // PHPStan error

callable-string

Ensures string names reference valid callable functions:

Playground | PHPStan

/**
 * @param callable-string $callback
 */
function invoke(string $callback): void {
    $callback(); // Safe: PHPStan checks it's actually callable
}

invoke('trim'); // OK
invoke('undefinedFunction'); // PHPStan error

class-string<T>

Used in factories or DI containers:

Playground | PHPStan

/**
 * @template T of LoggerInterface
 * @param class-string<T> $class
 * @return T
 */
function createLogger(string $class): object {
    return new $class(); // Safe and strongly typed
}

createLogger(FileLogger::class); // OK
createLogger(DateTime::class); // PHPStan error: not a LoggerInterface

Out-of-the-Box Use Case: Constants-as-Strings

Use literal-string when defining keys for array-based configuration:

Playground | PHPStan

/**
 * @param (literal-string&'database_host') $configKey
 */
function getConfig(string $configKey): mixed {
    return $GLOBALS['config'][$configKey] ?? null;
}

getConfig('database_host'); // OK
getConfig($userInput); // PHPStan error

Key Takeaways

  • non-empty-string kills edge-case bugs.

  • literal-string hardens systems against injections.

  • callable-string and class-string<T> enable safe dynamic resolution.

  • Always prefer these over plain string when handling user input, SQL, config keys, or dynamic execution.


2. Numeric & Range-Based Pseudo-Types

Precision where int and float fall short


A parameter like int $page tells you nothing about valid input. Is 0 okay? What about -1? Or 999999? That’s not type safety—it’s type ambiguity.

PHPStan’s numeric pseudo-types embed actual constraints in the signature. Let’s make your numbers behave.


Overview: Numeric Pseudo-Types

PHPStan Type Description Native Equivalent
positive-int Integer > 0 int
negative-int Integer < 0 int
non-negative-int Integer ≥ 0 (0, 1, …) int
non-positive-int Integer ≤ 0 (0, -1, …) int
int<min, max> Integer within specified range (inclusive) int
numeric int, float, or numeric-string mixed
float Any float float

ℹ️ Note: PHPStan does not currently support float<min, max> // Add range for float · Issue #6963 · phpstan/phpstan


Real-World Examples


positive-int

Ensure ID-like values are never 0 or negative:

Playground | PHPStan

/**
 * @param positive-int $userId
 */
function getUser(int $userId): User {
    return User::find($userId);
}

getUser(42); // OK
getUser(0); // PHPStan error

non-negative-int

Zero is allowed, useful for offsets and indexes:

Playground | PHPStan

/**
 * @param non-negative-int $offset
 */
function paginate(int $offset, int $limit = 10): array {
    return getRows($offset, $limit);
}

paginate(0); // OK
paginate(-5); // PHPStan error

int<1, 10>

Constrain arbitrary ranges—perfect for ratings or percent caps:

Playground | PHPStan

/**
 * @param int<1, 10> $rating
 */
function setUserRating(int $rating): void {
    // Only 1–10 allowed
}

setUserRating(10); // OK setUserRating(20); // PHPStan error

negative-int

Useful for things like accounting deltas:

Playground | PHPStan

/**
 * @param negative-int $debt
 */
function recordDebt(int $debt): void {
    // Only negative values allowed
}

recordDebt(-100); // OK
recordDebt(0); // PHPStan error

numeric

Accept int, float, or numeric-string – great for dynamic APIs:

Playground | PHPStan

/**
 * @param numeric $value
 */
function normalize($value): float {
    return (float) $value;
}

normalize("5.4"); // OK
normalize(3); // OK
normalize("not a number"); // PHPStan error

Out-of-the-Box Use Case: Domain Constraints via Aliases

Combine @phpstan-type with range types to centralize constraints:

Playground | PHPStan

/** @phpstan-type Rating int<1, 5> */

/**
 * @param Rating $stars
 */
function rate(int $stars): void {
    // Only 1–5 allowed
}

Avoids hardcoding ranges across your codebase + PhpStorm version >= 2025.1 has support for it.


Key Takeaways

  • Native int and float don’t carry domain meaning—these do.

  • Use int<min,max> for bounded values. Be explicit.

  • Prefer numeric over mixed for conversion-safe values.

  • Enforce constraints without runtime guards—fail at analysis time.


3. Array & List-Based Pseudo-Types

Because array is a wildcard, not a type.


Saying array $data is like saying “vehicle” when you mean “electric scooter with one broken brake.” Arrays in PHP are:

  • numerically indexed or associative,

  • empty or structured,

  • sometimes lists, sometimes maps—and usually misunderstood.

PHPStan gives you the tools to lock this chaos down.


Overview: Array Pseudo-Types

PHPStan Type Description Native PHP Equivalent
array Anything that’s an array—uselessly vague array
list<T> Indexed from 0, consecutive integers, all values of T array
non-empty-list<T> Same as list<T>, but with at least one element array
array<TKey, TValue> Associative array with defined key/value types array
non-empty-array<TKey, V> Like above, but must have at least one key-value pair array
array{key1: T1, key2?: T2} Structured array with required and optional keys array
array-key Either int or string—valid PHP array keys only int|string

Examples by Use Case


list<int>

Use when you need guaranteed 0-indexed values:

Playground | PHPStan

/**
 * @return list<int>
 */
function getIds(): array {
    return [1, 2, 3]; // PHPStan ensures keys are 0,1,2,...
}

non-empty-list<string>

Use for arguments that must not be empty:

Playground | PHPStan

/**
 * @param non-empty-list<string> $emails
 */
function notify(array $emails): void {
    foreach ($emails as $email) {
        mail($email, 'Hello!');
    }
}

array<string, User>

Classic associative map:

Playground | PHPStan

/**
 * @return array<string, User>
 */
function getUserMap(): array {
    return ['admin' => new User(), 'guest' => new User()];
}

array{status: bool, message: string}

Great for structured return types:

Playground | PHPStan

/**
 * @return array{status: bool, message: string}
 */
function response(): array {
    return ['status' => true, 'message' => 'OK'];
}

array{ids: list<int>, error?: string}

Mixed structure with optional keys:

Playground | PHPStan

/**
 * @return array{ids: list<int>, error?: string}
 */
function fetch(): array {
    return ['ids' => [1, 2, 3]];
}

Advanced Use Case: Refactor To Aliases

Avoid repeating complex shapes:

Playground | PHPStan

/** @phpstan-type ApiResponse array{status: bool, message: string, data?: mixed} */

/**
 * @return ApiResponse
 */
function getJson(): array {
    return ['status' => true, 'message' => 'Success'];
}

Use Table: When to Pick Which

Use Case Type
Indexed, all same type list<T>
Must contain elements non-empty-list<T>
Map with defined key type array<K, V>
Structured data (like DTO) array{...}
Optional shape fields array{key?: T}
Dynamic lookup array-key for keys

Pro Tip: If your array{} shape is reused or growing—stop.

Use a Value Object. It’s clearer, testable, and doesn’t break when you add nullableTimestamp?.


Key Takeaways

  • array is a type smell—replace it with something precise.

  • Use list<> when order and index matter. Use array{} for shape.

  • Use @phpstan-type to create aliases for shared structures.

  • Don’t let critical business structures live in vague array<string, mixed> land.

  • Once array shape gets complicated: make a DTO or value object. Don’t be clever. Be explicit.

  • Prefer list<T> over T[] when order and dense 0-based indexing matter.

  • Use e.g. non-empty-list<T> for enforced content.


4. Object & Class-Based Pseudo-Types

Make your objects speak their real shape.


PHP’s object type tells you nothing. PHPStan’s object pseudo-types tell you everything: where the object came from, how it’s called, what it returns, and what it’s allowed to be.


Overview: Class & Object Pseudo-Types

Type Description Use Case
object Any object—completely generic Rare; dynamic APIs
ClassName Exact class or subclass instance 95% of all real code
self The class this is written in (not inheritable) Static factory methods
$this The current instance’s real type (used for method chaining) Fluent interfaces, traits
static The class called at runtime (supports late static binding) Factories returning subclass instances
class-string Any valid FQCN string (non-instantiable without context) Reflection, service locators
class-string<T> A string guaranteed to name a subclass of T Typed dynamic instantiation

Examples by Use Case


object

Use only when you don’t care what it is (usually bad practice):

/**
 * @param object $instance
 */
function dumpObject(object $instance): void {
    var_dump($instance);
}

self

Always returns the exact class it’s written in—not a child class:

class Factory {
    public static function make(): self {
        return new self(); // Always returns Factory
    }
}

$this

Used for fluent interfaces—important in traits or base classes:

trait Loggable {
    /** @return $this */
    public function log(string $msg): self {
        echo "[LOG] $msg\n";
        return $this;
    }
}

Used in:

class Order { use Loggable; }
$order = (new Order())->log('Created')->log('Paid');

PHPStan/PhpStorm knows log() returns Order, not Loggable. Magic.


static

Enables late static binding:

Playground | PHPStan

class Repository {
    public static function new(): static {
        return new static(); // Might be a child class
    }
}

class UserRepository extends Repository {}
$userRepo = UserRepository::new(); // Returns UserRepository

class-string<Foo>

Ties a string to a valid subclass of Foo—powerful for DI, factories, etc.

Playground | PHPStan

/**
 * @template T of Service
 * @param class-string<T> $fqcn
 * @return T
 */
function resolve(string $fqcn): object {
    return new $fqcn();
}

Enforces constraints even when instantiating from strings.


Use Table: Object Pseudo-Types

Use Case Type
Generic object input object
Static factory (same class) self
Chaining / trait methods $this
Factory w/ LSB (return child) static
Instantiating by string class-string<T>

Key Takeaways

  • Always use $this for chaining and traits.

  • Use static when returning child classes from base class factories.

  • class-string<T> makes dynamic instantiation type-safe.

  • Avoid object unless you’re writing a serializer.


5. Callable Types & Signatures

The callable type in PHP is incredibly flexible, but historically it lacked type safety. It could be a function name string, an array [object, 'methodName'], an array ['ClassName', 'methodName'], or a Closure. Just declaring a parameter as callable tells PHPStan (and you) almost nothing about what arguments it expects or what it returns.

PHPStan lets you document the signature of a callable.

Documenting Callable Signatures

The syntax for documenting a callable’s signature is:

callable(ParamType1, ParamType2, ...): ReturnType

This goes inside your @param or @return tag for the callable type.

  • Specify the expected types of parameters within the parentheses, separated by commas.
  • Specify the expected return type after a colon.
  • Use void if the callable is not expected to return a meaningful value.
  • Use mixed if the parameters or return type are truly unknown or can vary wildly (but try to be more specific!).

Examples of Documenting Callables

Closure with simple types

Playground | PHPStan

/**
 * @param callable(int, string): bool $validator
 */
function processData(callable $validator): void {
    $result = $validator(123, 'abc');
    // PHPStan knows $result is bool here
}

processData(fn(int $a, string $b): bool => is_numeric($a) && is_string($b)); // OK
processData(fn(string $a, int $b): bool => true); // PHPStan error: argument types mismatch

Array callable (object method)

Playground | PHPStan

class Service {
    public function handle(float $value): string { return (string) $value; }
}

/**
 * @param callable(float): string $callback
 */
function execute(callable $callback): void {
    $result = $callback(1.23);
    // PHPStan knows $result is string here
}

$service = new Service();
execute([$service, 'handle']); // OK
execute([$service, 'nonExistentMethod']); // PHPStan error

String callable (global function)

While callable-string ensures it’s a valid function name, using the signature syntax adds type safety for the call itself.

Playground | PHPStan

/**
 * @param callable(string): string $modifier
 */
function cleanInput(string $input, callable $modifier): string {
    return $modifier($input);
    // PHPStan knows the callable takes string and returns string
}

cleanInput(" hello ", 'trim'); // OK
cleanInput(" hello ", 'str_starts_with'); // PHPStan error: str_starts_with signature mismatch

Key Takeaways

  • Simply using callable is not enough for static analysis.
  • Always document the expected signature of a callable using callable(ParamTypes): ReturnType.
  • This provides type safety for the parameters passed to the callable and the value returned from it.

6. Generic Types with @template, @extends, @implements

Static typing for dynamic collections. 


Overview: Generic Type Annotations

Annotation Purpose Example
@template T Declares a generic placeholder class Collection
@template T of Foo Constrains T to Foo or subclass interface Repository
@param T / @return T Applies T in method context function add(T $item): void
@extends Collection<T> Specifies generic type when extending a class class UserCollection extends…
@implements Repo<T> Specifies concrete type in interface implementation class UserRepo implements…

✅ Generic Collection (Classic)

Playground | PHPStan

/**
 * @template T
 */
class Collection {
    /** @var list<T> */
    private array $items = [];

    /** @param T $item */
    public function add(mixed $item): void {
        $this->items[] = $item;
    }

    /** @return list<T> */
    public function all(): array {
        return $this->items;
    }
}

✅ @extends: Narrow Generic Type in Child Class

/**
 * @extends Collection<User>
 */
class UserCollection extends Collection {
    public function findByEmail(string $email): ?User {
        foreach ($this->all() as $user) {
            if ($user->email === $email) return $user;
        }
        return null;
    }
}

Now the collection is locked to User.


✅ @implements: Interfaces with Generics

Playground | PHPStan

/**
 * @template T of Model
 */
interface Repository {
    /** @return T|null */
    public function find(int $id): ?object;

    /** @param T $model */
    public function save(object $model): void;
}

Implement with:

/**
 * @implements Repository<User>
 */
class UserRepository implements Repository {
    public function find(int $id): ?User { /* ... */ }

    public function save(object $model): void {
        if (!$model instanceof User) {
            throw new InvalidArgumentException();
        }
    }
}

Use Table: Generic Constructs

Use Case Syntax
Reusable logic w/ varying types @template T
Type-safe item addition @param T $item
Type-safe container extension @extends Collection<T>
Typed interface implementation @implements Repository<T>

Key Takeaways

  • Use @template T for anything that should be reusable (collections, services, repositories).

  • @extends and @implements lock in the concrete type—no ambiguity.

  • PHPStan validates T everywhere it’s used: arrays, returns, conditionals.

  • And you can combine this with other phpdocs like callables: Playground | PHPStan

7. Enum-Like Constraints with Constants and Literal Values


Because magic strings are garbage fire waiting to happen.

Whether you’re representing statuses, directions, or modes—“freeform strings” are a playground for bugs. Typos go undetected. Invalid values sneak through. Conditional logic breaks silently.

PHPStan gives us better options—even before PHP 8.1 enums.


Overview: Enum-Like Type Constraints

Type Annotation Description Works With
`’value1′ ‘value2’` Specific literal values allowed
MyClass::CONST_* Any constant with matching prefix from a class All
`MyClass::CONST_A CONST_B` Specific constant values allowed
MyEnum Accepts any case of a native PHP 8.1+ enum PHP 8.1+
MyEnum::MyCase Specific enum case from PHP 8.1+ enum PHP 8.1+
key-of<array> / value-of<array> Restrict input to keys/values of a predefined array All

Literal Union Types

Playground | PHPStan

/**
 * @param 'asc'|'desc' $direction
 */
function sortResults(string $direction): void {
    // PHPStan enforces exactly 'asc' or 'desc'
}

sortResults('asc');   // OK
sortResults('ASC');   // ERROR: Not lowercase
sortResults('up');    // ERROR

Class Constants (Wildcard or Specific)

Playground | PHPStan

class Status {
    public const NEW = 'new';
    public const ACTIVE = 'active';
    public const BLOCKED = 'blocked';
}

/**
 * @param Status::NEW|Status::BLOCKED $status
 */
function blockUser(string $status): void {
    // PHPStan enforces only these constants
}

blockUser(Status::BLOCKED);   // OK
blockUser('blocked');         // ERROR: Use the constant, not the string

Or allow all constants matching a prefix:

/**
 * @param Status::STATUS_* $status
 */
function updateStatus(string $status): void {}

Native Enums (PHP 8.1+)

Playground | PHPStan

enum UserStatus: string {
    case ACTIVE = 'active';
    case BANNED = 'banned';
}

function setStatus(UserStatus $status): void {}

setStatus(UserStatus::ACTIVE);   // OK
setStatus('active');             // ERROR

Array-Based Constraints (key-of, value-of)

Playground | PHPStan (key-of)

Playground | PHPStan (value-of)

public const array ALLOWED_ROLES = ['admin' => 1, 'editor' => 2, 'viewer' => 3];

/**
 * @param key-of<self::ALLOWED_ROLES> $role
 */
public function assignRole(string $role): void {}

$foo->assignRole('editor');    // OK
$foo->assignRole('moderator'); // ERROR

Key Takeaways

  • Don’t trust freeform values—constrain them.

  • Use literal strings or class constants to build pseudo-enums.

  • Use native PHP enums when available, especially in domain logic.

  • Use key-of, value-of to enforce consistency in maps.


8. Conditional Types & Type Inference Logic


Because sometimes your types depend on the situation.

Static typing gets tricky when return types or variable values depend on conditions—especially input parameters or validation results.

PHPStan gives us smart annotations to express:

  • Types that change depending on a condition (@return ($flag is true ? A : B))

  • Variable type guarantees after a validation function (@phpstan-assert-*)

  • Smart refactoring of isValid() patterns with actual type info


Overview: Conditional Typing Patterns

Annotation Purpose Use Case Example
@return (cond ? T1 : T2) Return type depends on a parameter value Return string or array depending on $asArray
@phpstan-assert T $var Guarantees $var is of type T after method call assertNonEmpty($str) makes $str a non-empty-string
@phpstan-assert-if-true T $var If method returns true, $var is of type T isValidEmail($email) ensures non-empty-string if true
@phpstan-assert-if-false T $var If method returns false, $var is of type T isEmpty($arr) => if false, it’s non-empty-array
T is ClassName Conditional logic inside generics @return (T is User ? int : string)

Conditional Return Types Based on Flags

Playground | PHPStan

/**
 * @return ($asJson is true ? string : array{name: string, age: int})
 */
function getUserData(bool $asJson): string|array {
    $data = ['name' => 'Alice', 'age' => 30];
    return $asJson ? json_encode($data, \JSON_THROW_ON_ERROR) : $data;
}

$data = getUserData(false);
// PHPStan: knows $data is array{name: string, age: int}

$json = getUserData(true);
// PHPStan: knows $json is string

@phpstan-assert-if-true

Playground | PHPStan

final class Validator {
    /**
     * @phpstan-assert-if-true non-empty-string $email
     */
    public static function isValidEmail(?string $email): bool {
        return is_string($email) && $email !== '' && str_contains($email, '@');
    }
}

$email = $_POST['email'] ?? null;

if (Validator::isValidEmail($email)) {
    // PHPStan knows $email is non-empty-string here
    echo strtoupper($email);
}

@phpstan-assert-if-false

Playground | PHPStan

/**
 * @phpstan-assert-if-false int<min, 0> $score
 */
function isPositive(int $score): bool {
    return $score > 0;
}

$score = rand(-5, 5);

if (!isPositive($score)) {
    // PHPStan: $score is int<min, 0>
} else {
    // PHPStan: $score is int<1, max>
}

Conditional Return Types in Generics

Playground | PHPStan

/**
 * @template T of int|string
 * @param T $input
 * @return (T is int ? string : int)
 */
function invertType(int|string $input): string|int {
    return is_int($input) ? (string)$input : (int)$input;
}

$val1 = invertType(42);     // string
$val2 = invertType('123');  // int

@phpstan-assert for Defensive Contracts

Playground | PHPStan

/**
 * @phpstan-assert non-empty-array<string> $items
 */
function assertHasItems(array $items): void {
    if (empty($items)) {
        throw new InvalidArgumentException("Must contain at least one item.");
    }
}

Use these to enforce contracts even if native PHP types can’t.


Key Takeaways

  • Let your return types reflect real-world branching logic.

  • Use @phpstan-assert-if-true to encode validator results into type narrowing.

  • Use @return (condition ? A : B) to model toggle behavior safely.

  • Defensive types are better than defensive runtime checks—because they’re enforced before runtime.


9. Pass-by-Reference Variables with @param-out


Stop guessing what happens to that &$var.

PHP’s &$variable allows mutation by reference—which is great for parsers, validators, or multi-output functions, but it kills static type analysis.

PHPStan solves this with @param-out: explicitly documenting the final type of a referenced variable after a function call.

Think of it as “the function returns into this variable”—and now PHPStan knows what to expect.


Overview: @param-out Syntax

Annotation Purpose
@param-out T $var Describes the type $var will have after the function call finishes.
@param T $var Still describes the type it must be at input, if not uninitialized.

Parsing Into a Variable

Playground | PHPStan

/**
 * @param string $input
 * @param-out int|null $parsed
 */
function parseInt(string $input, ?int &$parsed): void {
    $parsed = filter_var($input, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
}
$raw = "42";
$result = null;

parseInt($raw, $result);

// PHPStan: $result is now int|null
if ($result !== null) {
    echo $result * 2;
}

Assigning a Default String

Playground | PHPStan

/**
 * @param-out non-empty-string $output
 */
function ensureValue(?string &$output): void {
    if ($output === null || $output === '') {
        $output = 'default';
    }
}
$name = null;
ensureValue($name);

// PHPStan: $name is now non-empty-string

Flagging Errors in Processing

Playground | PHPStan

/**
 * @param CsvFile $csv
 * @param-out list<string> $errors
 */
function processCsv(CsvFile $csv, array &$errors): bool {
    $errors = [];
    foreach ($csv->getRows() as $row) {
        if (!isValidRow($row)) {
            $errors[] = 'Invalid row: ' . json_encode($row);
        }
    }
    return empty($errors);
}
$errors = [];
$success = processCsv($file, $errors);

if (!$success) {
    foreach ($errors as $e) {
        echo $e;
    }
}

Now $errors is a guaranteed list of strings, only if the function sets it.


Key Takeaways

  • Use @param-out whenever a &$var is written to.

  • Combine with @param to describe both input and output intent.

  • Helps static analysis track types over mutation.

  • Prevents “guessing” about the variable’s final type in calling scope.


10. Suppressing False Positives with @phpstan-ignore-*


Sometimes static analysis gets it wrong. That’s OK— but make it surgical, not sloppy.

PHPStan is powerful, but not omniscient. In complex, dynamic, or legacy-heavy codebases, it might raise false positives—errors that you know are safe but the tool can’t fully reason about.

Don’t disable entire rules. Don’t drop your level.
Use @phpstan-ignore-* annotations surgically, with precision.


Overview: Suppression Annotations

Annotation Scope Best Use Case
@phpstan-ignore-next-line Next line only (please use the Identifier stuff not this) Dynamic access, legacy code edge cases
@phpstan-ignore-line Inline on same line (please use the Identifier stuff not this) Dynamic properties/methods, chained expressions
@phpstan-ignore-error Identifier Specific error type only Narrow suppression while keeping strict checks
parameters > ignoreErrors: (neon) Global suppression config Vendor code or unavoidable project-wide issues

Ignoring the Next Line

/** @phpstan-ignore-next-line */
echo $legacyObject->$dynamicProperty;

Useful when $dynamicProperty is validated externally, but PHPStan can’t infer that.


Inline Suppression

$val = $legacyObject->$property; /** @phpstan-ignore-line */

Keep suppression right next to the usage, especially helpful for one-liners.


Targeting Specific Errors

/** @phpstan-ignore-error function.alreadyNarrowedType (your comment here) */
if (is_string($value) && strlen($value) > 5) {
    echo $value;
}

This suppresses only the “already narrowed” warning, keeping everything else active.


Global Project Suppression (phpstan.neon)

parameters:
    ignoreErrors:
        - '#Call to undefined method LegacyClass::doMagic#'
        - 
            message: '#Access to an undefined property#'
            path: src/LegacyStuff/*.php

Use this only for vendor or legacy glue code. Never in new code.


Best Practices for Ignoring

Rule Why
Always comment Explain why the ignore is needed.
Suppress specific errors, not broad ones Use @phpstan-ignore-error, not ignore-next-line if possible.
Refactor > Ignore Most false positives come from bad typing. Improve types if you can.
Audit periodically Old ignores may hide real problems after refactors.

❌ Don’t Do This

/** @phpstan-ignore-next-line */
doSomethingRisky(); // No explanation, no control

Or worse:

parameters:
    level: 0

You’ve just turned off the fire alarm and set your desk on fire, a simple way of starting is the baseline feature where you can just ignore old errors: The Baseline | PHPStan


Key Takeaways

  • Only ignore what you must. Don’t make it a habit.

  • Prefer precise, contextual suppression (-error, -next-line) over global.

  • Always add a human-readable comment for future devs.

  • Refactor the real problem if PHPStan is right.


11. Best Practices for PHPStan PHPDocs


Make your types honest. Make your docs readable. Make your tools useful.

PHPStan doesn’t care how pretty your code looks—it cares if the types match, if edge cases are covered, and if your docs are aligned with reality. Writing good PHPDocs isn’t about verbosity. It’s about clarity, precision, and maintainability.


PHPDoc Hygiene Checklist

✅ Do This ❌ Instead Of This
Use PHP native types wherever possible Duplicating native types in @param tags
Add constraints with pseudo-types Vague types like array or mixed
Use shapes and lists (array{} / list<>) Indexed arrays with array<int,type> (unless needed)
Prefer @phpstan-type for reuse or DTO of the type is complex Copy-pasting complex types across files
Keep docs in sync with code Letting docs rot after refactors
Add purpose-driven descriptions Empty @param tags with no context

✅ Use Native Types First, PHPDoc Second

// ❌ Redundant
/**
 * @param int $id
 */
function getUser(int $id): User {}

// ✅ Clean
function getUser(int $id): User {}

// ✅ All-In (it depends)
/** * @param UserId $id Use the Value-Object to fetch the User. */ function getUser(UserId $id): User {}

✅ Add Value with PHPStan Types

// ✅ Enforce constraints
/**
 * @param positive-int $userId
 */
function getUserById(int $userId): User {}

✅ Prefer array{} Over Raw array

// ❌ Too generic
function getConfig(): array {}

// ✅ Self-documenting and type-safe
/**
 * @return array{host: string, port: int}
 */
function getConfig(): array {}

✅ Use list<T> or non-empty-list<T>

/**
 * @return non-empty-list<User>
 */
function getActiveUsers(): array {}

No more bugs from assuming the first item exists.


✅ Reuse with @phpstan-type

/**
 * @phpstan-type UserData array{
 *   id: int,
 *   name: non-empty-string,
 *   email: string
 * }
 */

Then:

/**
 * @return UserData
 */
public function getUserData(): array {}

Update once. Used often. But once again, if you need this everywhere, please create a DTO class for it. :)


✅ Generics with @template

/**
 * @template T
 * @param T $value
 * @return T
 */
function identity($value) { return $value; }

Now that’s a real generic—PHPStan can track the type through usage.


✅ Descriptions Are Not Dead

/**
 * Fetches total including tax.
 *
 * @param list<Item> $items Items in the cart.
 * @param positive-int $vat VAT rate in percent (e.g. 19).
 * @return float Total price incl. tax.
 */
function calculateTotal(array $items, int $vat): float {}

You’re writing code for humans and tools. Don’t forget the humans + you can use html (lists, string, …) here.


✅ Suppress Only When Necessary

// We’re dealing with dynamic properties from legacy __get
/** @phpstan-ignore-next-line */
echo $legacyObject->$name;

But don’t make this a crutch. Fix types instead.


✅ Keep Docs in Sync

When you rename a parameter, update the PHPDoc.
When you change the return type, fix the annotation.
Outdated docs are worse than none at all.


✅ Consider a DTO or Value Object

// ❌ This is too much for an array shape
/**
 * @return array{
 *   user: array{id: int, name: string},
 *   metadata: array{roles: list<string>, active: bool},
 *   settings?: array<string, mixed>
 * }
 */

Just… stop. Use a DTO. Your future self thanks you.


Key Takeaways

  • Don’t document what PHP already knows.

  • Document what PHP can’t express.

  • Use PHPStan’s extensions to document intent, not just shape.

  • Reuse types. Describe constraints. Avoid magic.


12. Cheatsheet & Final Summary


Let’s wrap this beast up the way it deserves: with a clear cheatsheet of everything PHPStan’s extended PHPDoc syntax gives you—and how to actually use it in production.

This isn’t theoretical. It’s not for linting your hobby script. This is how real teams keep real codebases from turning into type-anarchist dumpster fires.


Cheatsheet: PHPStan PHPDoc Types by Category

Scalar & Refined Pseudo-Types

Type Meaning
non-empty-string String that’s never ''
numeric-string String that parses to number ("123", "1.5")
literal-string String known at compile time
positive-int Integer > 0
non-negative-int Integer >= 0
negative-int Integer < 0
non-positive-int Integer <= 0
int<min, max> Integer between min and max (inclusive)
class-string<T> Fully qualified class name (subtype of T)
callable-string String name of a global callable

Array & List Types

Type Meaning
array<TKey, TValue> Associative array
non-empty-array<TKey, TValue> Associative array with at least one element
list<T> 0-based sequential array (indexed)
non-empty-list<T> list<T> with at least one element
array{key: T, ...} Structured array with required keys
array{key?: T} Structured array with optional keys
array-key `int

Object, Class

Type Meaning
object Any object
self Current class (not child)
$this Instance of calling class
static Late static binding return
class-string<T> String that must be a subtype of T

Callable

Syntax Meaning
callable Any valid PHP callable (closure, string, array callback)
callable(): void A callable that takes no parameters and returns nothing
callable(int): bool A callable that takes an int and returns a bool
callable(string, T): T Generic callable with multiple parameters and return type
Closure(T): R Closure with generic parameter and return type
callable-string String name of a globally callable function

Generics

Annotation Purpose
@template T Declare a type variable T
@template T of Foo Restrict T to be subtype of Foo
@param T $value Use generic T for parameter
@return T Return generic type T
@var T[] / list<T> Typed container
@extends Base<T> Specify type T when extending base class
@implements Interface<T> Specify type T for implementation

Structured Data & Reusability

Syntax Meaning
@phpstan-type Alias = Type Type alias for complex types
@phpstan-import-type Import alias from another scope/class

Constant & Enum-Like Constraints

Syntax Meaning
'foo'|'bar' Specific literal values allowed
MyClass::CONST_* All constants matching prefix in a class
MyClass::CONST_A | MyClass::CONST_B Specific constant values allowed
MyEnum::MyCase (PHP 8.1+) Specific native Enum case
key-of<array> / value-of<array> Constrain to keys/values of a known constant array

Type Assertions & Conditional Logic

Annotation Purpose
@phpstan-assert T $var Assert $var is type T after call
@phpstan-assert-if-true T $var If function returns true, $var is T
@phpstan-assert-if-false T $var If function returns false, $var is T
@return (cond ? T1 : T2) Conditional return type

By-Reference Output Tracking

Annotation Purpose
@param-out T $var $var will be of type T after the function

Suppression & Exceptions

Annotation Use Case
@phpstan-ignore-next-line Ignore all errors on the next line
@phpstan-ignore-line Ignore errors inline
@phpstan-ignore-error error-id Ignore specific error on current block

Final Thoughts

  • Treat PHPDocs as contracts, not comments.

  • Don’t use array when you mean non-empty-list<literal-string>be precise.

  • Refactor array shapes into DTOs when they get hairy.

  • Use @phpstan-type, @template, @assert, and @param-out to document behavior that PHP syntax alone can’t express.

  • Rely on tools, not convention. Let PHPStan hold the line.

 

SOLID Principles: From Paw Patrol to the Neighborhood

– A Senior Developer’s Pragmatic Guide