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
 

Why Agile Won’t Fix Your Project

We’ve all heard the promises of Agile: faster delivery, more flexibility, and a better team dynamic. But let’s face it—Agile won’t save your project if the foundation isn’t right. Frameworks are tools, not magic wands, and success depends on one key factor: your people. Without commitment, purpose, and vision, even the best methodology falls flat.


Commitment Is the Foundation

In every successful project I’ve seen, there’s one constant: the team cares. They’re passionate about the project, the technology, or the customer. When people are engaged, they bring their A-game. Without that commitment, Agile rituals become hollow motions—stand-ups where no one stands up for quality and retrospectives that only scratch the surface.

But how do you ignite that spark?

  • Give Purpose: Teams perform better when they know why their work matters. Whether it’s a product that changes lives or a system that transforms workflows, connect the dots between their efforts and the bigger picture.
  • Empower Ownership: Nothing kills commitment faster than micromanagement. Give your team the autonomy to solve problems their way, and they’ll take pride in their work.
  • Set the North Star: Someone needs to have the big picture in mind. This isn’t about controlling every detail; it’s about providing clarity and direction. A team without a vision is just a group of people checking tasks off a backlog.

Agile: A Framework, Not a Fix

Agile isn’t inherently bad—it’s just not a silver bullet. A poorly implemented Agile process can feel like busy work, creating friction instead of flow. You’ve seen it: endless sprints with no tangible value, misaligned priorities, and teams that are Agile in name only.

The truth is, Agile is just a tool, and like any tool, its effectiveness depends on how you use it. Without the right culture, mindset, and leadership, Agile can’t thrive.


The Power of Flow

When your team is committed and has a clear purpose, they enter the elusive “flow” state. Flow is where the magic happens: work feels both challenging and enjoyable, and everything just clicks. Achieving flow requires more than process—it demands an environment that fosters focus, removes blockers, and aligns everyone toward shared goals.


Common Pitfalls in Agile Projects

  • Lack of Vision: If the goals are unclear, the team loses direction. Agile iterations can’t compensate for a missing strategy.
  • Token Empowerment: Teams are told they’re empowered, but their decisions are constantly overridden by management.
  • Process Over People: Agile becomes a checkbox exercise, focusing on rituals instead of outcomes.
  • Burnout Culture: Passion doesn’t mean working endless hours. A healthy pace is critical for long-term success.

Building the Right Environment

Here’s what makes the difference:

  1. Clarity and Direction: Define a clear vision and align the team’s work with it. Avoid vague objectives—make the goals tangible and achievable.
  2. Purposeful Leadership: Leaders need to support, not control. Their role is to remove obstacles, align efforts, and amplify strengths.
  3. Empowerment: Trust your team to make decisions. Give them the tools, autonomy, and confidence to innovate.
  4. Continuous Reflection: Agile emphasizes improvement, but that requires honesty. Create a safe space for open feedback and genuine introspection.

Final Thoughts: Commitment Over Process

At the end of the day, success isn’t about following Agile to the letter. It’s about creating an environment where your team thrives. Agility is just one piece of the puzzle. Your team’s commitment, passion, and purpose are what truly drive results.

So don’t just “go Agile.” Build a culture that values people over processes, outcomes over rituals, and vision over micromanagement. Agile might not save your project, but your team will.

🚀 Because in the end, your people are your greatest asset.

LLM Prompt Optimizations: Practical Techniques for Developers

Optimizing inputs for LLMs ensures better, more consistent outputs while leveraging the full potential of the model’s underlying capabilities. By understanding core concepts like tokenization, embeddings, self-attention, and context limits, you can tailor inputs to achieve desired outcomes reliably. Below, you’ll find fundamental techniques and best practices organized into practical strategies.

PS: You can use this content to automatically improve your prompt by asking as follows: https://chatgpt.com/share/6785a41d-72a0-8002-a1fe-52c14a5fb1e5


🎯 1. Controlling Probabilities: Guide Model Outputs

🧠 Theory: LLMs always follow probabilities when generating text. For every token, the model calculates a probability distribution based on the context provided. By carefully structuring inputs or presenting examples, we can shift the probabilities toward the desired outcome:

  • Providing more examples helps the model identify patterns and generate similar outputs.
  • Clear instructions reduce ambiguity, increasing the probability of generating focused responses.
  • Contextual clues and specific phrasing subtly guide the model to prioritize certain outputs.

⚙️ Technology: The model operates using token probabilities:

  • Each token (word or part of a word) is assigned a likelihood based on the input context.
  • By influencing the input, we can make certain tokens more likely to appear in the output.

For example:

  • A general query like “Explain energy sources” might distribute probabilities evenly across different energy types.
  • A more specific query like “Explain why solar energy is sustainable” shifts the probabilities toward solar-related tokens.

⚙️ Shifting Probabilities in Prompts: The structure and wording of your prompt significantly influence the token probabilities:

  • For specific outputs: Use targeted phrasing to increase the likelihood of desired responses: Explain why renewable energy reduces greenhouse gas emissions.
  • For diverse outputs: Frame open-ended questions to distribute probabilities across a broader range of topics: What are the different ways to generate clean energy?
  • Few-Shot Learning: Guide the model using few-shot learning to set patterns: Example 1: Input: Solar energy converts sunlight into electricity. Output: Solar energy is a renewable power source. Example 2: Input: Wind energy generates power using turbines. Output: Wind energy is clean and sustainable. Task: Input: Hydropower generates electricity from flowing water. Output:

💡 Prompt Tips:

  • Use clear, direct instructions for precise outputs: Write a PHP function that adds two integers and returns a structured response as an array.
  • Use contextual clues to steer the response: Explain why PHP is particularly suited for web development.

💻 Code Tips: LLMs break down code and comments into tokens, so structuring your PHPDocs helps focus probabilities effectively. Provide clarity and guidance through structured documentation:

/**
 * Adds two integers and returns a structured response.
 *
 * @param int $a The first number.
 * @param int $b The second number.
 * 
 * @return array{result: int, message: string} A structured response with the sum and a message.
 */
function addIntegers(int $a, int $b): array {
    $sum = $a + $b;

    return [
        'result' => $sum,
        'message' => "The sum of $a and $b is $sum."
    ];
}
  • Include examples in PHPDocs to further refine the probabilities of correct completions: /** * Example: * Input: addIntegers(3, 5) * Output: [‘result’ => 8, ‘message’ => ‘The sum of 3 and 5 is 8’] */

✂️ 2. Tokenization and Embeddings: Use Context Efficiently

🧠 Theory: LLMs break down words into tokens (numbers) to relate them to each other in multidimensional embeddings (vectors). The more meaningful context you provide, the better the model can interpret relationships and generate accurate outputs:

  • Tokens like “renewable energy” and “sustainability” have semantic proximity in the embedding space.
  • More context allows the model to generate richer and more coherent responses.

⚙️ Technology:

  • Tokens are the smallest units the model processes. For example, “solar” and “energy” may be separate tokens, or in compound languages like German, one long word might be broken into multiple tokens.
  • Embeddings map these tokens into vectors, enabling the model to identify their relationships in high-dimensional space.

⚙️ Optimizing Tokenization in Prompts: To make the most of tokenization and embeddings:

  • Minimize irrelevant tokens: Focus on core concepts and avoid verbose instructions.
  • Include context-rich phrases: Relevant terms improve the embedding connections.
  • Simplify Language: Use concise phrasing to minimize token count: Solar energy is renewable and reduces emissions.
  • Remove Redundancy: Eliminate repeated or unnecessary words: Explain why solar energy is sustainable.

💡 Prompt Tips:

  • Include only essential terms for better embedding proximity: Describe how solar panels generate electricity using photovoltaic cells.
  • Avoid vague or verbose phrasing: Explain solar energy and its uses in a way that a normal person can understand and provide details.
  • Use specific language to avoid diluting the context: Explain why solar energy is considered environmentally friendly and cost-effective.
  • Avoid vague instructions that lack actionable context: Explain me solar energy.

💻 Code Tips: Write compact and clear PHPDocs to save tokens and improve context:

/**
 * Converts raw user input into a structured format.
 *
 * @param string $input Raw input data.
 * 
 * @return array{key: int, value: string} Structured output.
 */
function parseInput(string $input): array {
    $parts = explode(":", $input);

    return [
        'key' => (int)$parts[0],
        'value' => trim($parts[1])
    ];
}
  • Use compact and descriptive documentation to maximize token efficiency: /** * Example: * Input: “42:Hello” * Output: [‘foo’ => 42, ‘bar’ => ‘Hello’] */

🧭 3. Self-Attention and Structure: Prioritize Context

🧠 Theory: LLMs work with the principle of self-attention, where the input tokens are interrelated with each other to determine the relevance and context. This mechanism assigns importance scores to tokens, ensuring that the most relevant words and their relationships are prioritized.

⚙️ Technology:

  • Self-attention layers: Compare each token with every other token in the input to generate an attention score.
  • Multi-head attention: Allows the model to consider multiple perspectives simultaneously, balancing relevance and context.
  • Pitfall: Too many irrelevant tokens dilute the attention scores, leading to distorted outputs.

⚙️ Optimizing Structure in Prompts:

  • Structure Your Inputs: Use lists, steps, or sections to emphasize relationships: Compare the benefits of solar and wind energy: 1. Environmental impact 2. Cost-efficiency 3. Scalability
  • Minimize Irrelevant Tokens: Keep prompts focused and free from extraneous details.

💡 Prompt Tips:

  • Well-Structured: Organize tasks into sections: Explain the environmental and economic benefits of renewable energy in two sections: 1. Environmental 2. Economic
  • Unstructured: Avoid asking everything at once: What are the environmental and economic benefits of renewable energy?

💻 Code Tips: In PHPDocs, organize information logically to enhance clarity and guide models effectively:

/**
 * Calculates the cost efficiency of renewable energy.
 *
 * Steps:
 * 1. Evaluate savings-to-investment ratio.
 * 2. Return a percentage efficiency score.
 *
 * @param float $investment Initial investment cost.
 * @param float $savings Annual savings.
 * 
 * @return float Efficiency percentage.
 */
function calculateEfficiency(float $investment, float $savings): float {
    return ($savings / $investment) * 100;
}

🧹 4. Context Management and Token Limits

🧠 Theory: LLMs operate within a fixed token limit (e.g., ~8k tokens for GPT-4), encompassing both input and output. Efficiently managing context ensures relevant information is prioritized while avoiding irrelevant or redundant content.

⚙️ Technology:

  • Chunking: Break long inputs into smaller, manageable parts: Step 1: Summarize the introduction of the report. Step 2: Extract key arguments from Section 1. Step 3: Combine summaries for a final overview.
  • Iterative Summarization: Condense sections before integrating them: Summarize Section 1: Solar energy’s benefits. Summarize Section 2: Wind energy’s benefits. Combine both summaries.
  • Pitfall: Excessive context can truncate critical data due to token limits.

💡 Prompt Tips:

  • For large inputs, use step-by-step processing: Step 1: Summarize the introduction of the document. Step 2: Extract key arguments from Section 1. Step 3: Combine these points into a cohesive summary.
  • Avoid presenting the full text in a single prompt: Summarize this 20-page document.
  • Focus on specific sections or tasks: Summarize the introduction and key points from Section 1.

💻 Code Tips: Divide tasks into smaller functions to handle token limits better:

function summarizeSection(string $section): string {
    // Summarize section content.
}

function combineSummaries(array $summaries): string {
    // Merge individual summaries.
}

🎨 5. Reasoning and Goals: Strengthen Prompt Direction

🧠 Theory: LLMs generate better results when the reasoning behind a task and its intended goal are explicitly stated. This guides the model’s probabilities toward meaningful and relevant outcomes.

⚙️ Technology:

  • Explicit reasoning provides semantic depth, helping the model focus on the task’s purpose.
  • Explaining the goal improves alignment with user expectations and narrows token probabilities.

💡 Prompt Tips:

  • State the reason for the task and its goal: Explain renewable energy because I need to create an introductory guide for high school students.
  • Avoid generic prompts without a clear goal: Describe renewable energy.

💻 Code Tips: Use PHPDocs to explain both the reasoning and expected outcomes of a function:

/**
 * Generates a detailed user profile report.
 *
 * This function is designed to create a comprehensive profile report based on user data inputs. 
 * It is useful for analytical dashboards requiring well-structured user insights.
 *
 * @param array{name: string, age: int, email: string} $userData The user data array.
 * 
 * @return string A formatted profile report.
 */
function generateProfileReport(array $userData): string {
    return sprintf(
        "User Profile:\nName: %s\nAge: %d\nEmail: %s\n",
        $userData['name'],
        $userData['age'],
        $userData['email']
    );
}

🛠️ 6. Iterative Refinement: Simplify Complex Tasks

🧠 Theory:
Breaking down complex tasks into smaller, manageable steps improves accuracy and ensures the model generates focused and coherent outputs. This method allows you to iteratively refine results, combining outputs from smaller subtasks into a complete solution.

⚙️ Technology:

  • Chunking: Split large tasks into multiple smaller ones to avoid overwhelming the model.
  • Validation: Intermediate outputs can be validated before moving to the next step, minimizing errors.
  • Recombination: Smaller validated outputs are merged for the final result.

💡 Prompt Tips:

  • For multi-step tasks, provide clear, incremental instructions: Step 1: Summarize the environmental benefits of solar energy. Step 2: Describe the cost savings associated with solar energy. Step 3: Combine these summaries into a single paragraph.
  • Avoid handling complex tasks in a single step: Explain the environmental benefits and cost savings of solar energy in one response.

💻 Code Tips: Ask the LLM to create the code step by step and ask for confirmation after each step so that the LLM can focus on one aspect of the implementation at a time. Focus on one aspect of the implementation at a time.


🔗 7. Cross-Contextual Coherence: Maintain Consistency

🧠 Theory:
LLMs lack persistent memory between interactions, making it essential to reintroduce necessary context for consistent responses across prompts. By maintaining cross-contextual coherence, outputs remain aligned and relevant, even in multi-step interactions.

⚙️ Technology:

  • Use context bridging: Reference key elements from previous responses to maintain relevance.
  • Store critical details in persistent structures, such as arrays or JSON, to reintroduce when needed.
  • Avoid overloading with irrelevant details, which can dilute coherence.

💡 Prompt Tips:

  • Reintroduce essential context from previous interactions: Based on our discussion about renewable energy, specifically solar power, explain the benefits of wind energy.
  • Summarize intermediate outputs for clarity: Summarize the main benefits of renewable energy. Then expand on solar and wind energy.

💻 Code Tips: Use seperated files for Code-Examples that we can provide e.g. Custom GPTs so it can learn from learnings/findings this way.


🌍 8. Style and Tone: Adapt Outputs to the Audience

🧠 Theory: LLMs generate better responses when the desired style and tone are explicitly stated. By matching the tone to the audience, you can make content more engaging and effective.

⚙️ Technology:

  • The model uses semantic cues in the prompt to adjust style and tone.
  • Specific words and phrases like “formal,” “casual,” or “technical” help steer the model’s output.

💡 Prompt Tips:

  • Specify the tone and audience: Write a technical explanation of solar panels for an engineering audience.
  • Adjust the style for different contexts: Explain solar panels in a simple and friendly tone for kids.

💻 Code Tips: In PHPDocs, define the intended audience and tone to guide LLM-generated documentation:

/**
 * Calculates the total energy output of a solar panel system.
 *
 * Intended Audience: Engineers and technical experts.
 * Tone: Formal and technical.
 *
 * @param float $panelArea The total area of solar panels in square meters.
 * @param float $efficiency The efficiency rate of the solar panels (0-1).
 * @param float $sunlightHours Daily sunlight hours.
 * 
 * @return float Total energy output in kilowatt-hours.
 */
function calculateSolarOutput(float $panelArea, float $efficiency, float $sunlightHours): float {
    return $panelArea * $efficiency * $sunlightHours;
}

🔍 9. Fine-Tuning and Domain Expertise

🧠 Theory: Fine-tuning allows LLMs to specialize in specific domains by further training them on domain-specific datasets. This enhances their ability to generate accurate, relevant, and nuanced outputs tailored to specialized tasks or fields.

⚙️ Technology:

  • Fine-tuning adjusts the weights of a pre-trained model by using a curated dataset that focuses on a specific domain.
  • This process requires labeled data and computational resources but significantly improves task performance in niche areas.

💡 Prompt Tips:

  • Use fine-tuning to simplify prompts for repeated tasks: Generate a legal brief summarizing the key points from this case.
  • Without fine-tuning, include detailed instructions and examples in your prompt: Write a summary of this legal case focusing on liability and negligence, using a formal tone.

💻 Code Tips: When fine-tuning is not an option, structure your PHPDocs to include domain-specific context for LLMs:

/**
 * Generates a compliance report for renewable energy projects.
 *
 * This function creates a detailed compliance report tailored for regulatory agencies. It checks for adherence to
 * energy efficiency standards and sustainability guidelines.
 *
 * @param array<string, mixed> $projectData Details of the renewable energy project.
 * @param string $region The region for which the compliance report is generated.
 * 
 * @return string The compliance report in a formatted string.
 */
function generateComplianceReport(array $projectData, string $region): string {
    // Example report generation logic.
    return sprintf(
        "Compliance Report for %s:\nProject: %s\nStatus: %s\n",
        $region,
        $projectData['name'] ?? 'Unnamed Project',
        $projectData['status'] ?? 'Pending Review'
    );
}

The Wild West of Coding: Why We’re Still Burning Digital Cities


The year 2050 is closer than the year 1990, yet we’re still writing code like it’s the 1800s.

It’s 2025, and while we’ve made incredible strides in software development—automated memory management, static analysis tools, refactoring IDEs, and AI copilots like ChatGPT—it still feels like the Wild West. Sure, the tools are better, but the way we approach software remains chaotic, inefficient, and overly reliant on custom solutions.

Every day, thousands of developers solve the same problems repeatedly. Companies roll out their own authentication systems, file upload handlers, and error trackers. Many of these are flawed. Vulnerabilities creep in because we’re not building resilient systems—we’re building digital bonfires.

This isn’t progress. This is the time before the First Industrial Revolution of Software Development.


Lessons from History: What the Past Teaches About Our Digital Fires

1. The Great Fire of Hamburg (1842): Building Without Safety

In 1842, a quarter of Hamburg burned to the ground. The city’s lack of fire safety standards—wooden buildings, narrow streets, no prevention systems—made disaster inevitable.

Software today mirrors this recklessness:

  • File upload systems without malware checks.
  • APIs with vulnerabilities because “it’s faster to skip security.”
  • Custom-built logging systems without consistency or reliability.

After the fire, Hamburg rebuilt with fireproof materials and strict regulations. We need the same shift in software development: adopting universal standards, secure-by-design frameworks, and centralized tools to prevent disaster before it happens.


2. Electrical Sockets in the Early 1900s: Chaos Without Standards

Before standardization, electrical sockets were a mess. Every region had its own plug type, making interoperability nearly impossible. Plugging in a device often meant wasting time finding the right adapter.

Software development today is no different:

  • APIs lack consistent patterns.
  • Libraries solve the same problem in incompatible ways.
  • Developers reinvent logging, error handling, and authentication with every project.

The solution? Standardized, language-agnostic tools—the equivalent of universal plug designs. Imagine APIs and services that integrate seamlessly across languages and frameworks:

  • Centralized logging and error tracking APIs, similar to Sentry but designed for internal use with cross-language compatibility.
  • High-performance Unix socket APIs for tasks like logging, monitoring, and file scanning.
  • Shared SDKs for foundational needs like security or metrics collection.

By building shared infrastructure, we could eliminate redundant work and improve reliability across projects.


3. The Facade Problem: Buying Software for Looks, Not Stability

Imagine buying a house because the facade looks amazing but never checking the foundation. That’s how most software is evaluated today:

  • Buyers focus on flashy UIs and marketing demos.
  • Security, scalability, and maintainability are often ignored.

This approach leads to brittle, insecure systems. What’s missing? Inspectors for digital bridges—specialized roles that assess software foundations, enforce accountability, and ensure systems are solid beneath their shiny exteriors.


Moving Toward the First Industrial Revolution of Software Development

The Industrial Revolution replaced handcrafting with standardization and mass production. Software development is still stuck in its pre-industrial phase:

  • Custom solutions are built repeatedly for the same problems.
  • Accountability is rare—no one ensures the “bridge” (software) is stable before it’s deployed.

To mature as an industry, we need:

1. Universal Blueprints

Developers today still create bespoke solutions for common problems. We need standardized tools and APIs, such as:

  • A unified SDK for antivirus scanning, accessible via /usr/sbin/antivirus or unix:///var/run/antivirus.
  • Centralized APIs for error tracking, metrics, and monitoring, with cross-language support.

2. Specialized Roles

In the 19th century, collapsing bridges led to the creation of specialized roles: architects for design, builders for execution, and inspectors for safety. Software teams need similar specialization:

  • System Inspectors to evaluate software for security, scalability, and maintainability.
  • Digital Firefighters to enforce standards and proactively address vulnerabilities.

3. Accountability

When a bridge collapses, someone is held responsible. In software, failures are often patched silently or ignored. We need:

  • Transparency: Bugs and vulnerabilities must be documented openly.
  • Retrospectives: Focused on systemic improvements, not just quick fixes.

Building Digital Cities That Don’t Burn

Imagine a future where software is built like modern cities:

  • Fireproof Systems: Universal standards for security, maintainability, and testing.
  • Digital Firefighters: Publicly funded teams safeguarding critical infrastructure.
  • Inspectors for Digital Bridges: Specialized roles ensuring software is built to last.

AI tools like GPT can help accelerate this process, but they are not the solution. AI is like the steam engine of programming—amplifying productivity but requiring skilled operators. If we don’t lay the right foundations, AI will only magnify our inefficiencies.

This future isn’t about writing more code—it’s about creating resilient, scalable systems that stand the test of time. The tools to build fireproof digital cities are already here. The question is: are we ready to use them?


Let’s move beyond the Wild West of coding and into the Industrial Revolution our industry desperately needs. It’s time to stop building bonfires and start building something that lasts.

Legacy Codebase: A Love Story

After some years, working with a > 10 years old legacy PHP codebase, I can truly say: you can escape the legacy codebase and introduce whatever is helpful, in a well-maintained system.

Here are 5 important steps that I have done:

  • Custom error handling: Reporting notices for developers, report bad “assert” calls in the dev container, report bad indexes, reporting wrong code usage, …
  • Autocompletion for everything: classes, properties, SQL queries, CSS, HTML, JavaScript in PHP (e.g. via /* @lang JavaScript */ in PhpStorm), …
  • Static-Code Analysis: Preventing bugs is even better than fixing bugs, so just stop stupid bugs and use types in your code.
  • Automate the refactoring: With tools like PHP-CS-Fixer or Rector you can not only fix your code one time, you can fix any future wrong usage of the code.
  • Do not use strings for code: Just use constants, classes, properties, … use something that can be processes by your static-code analysis and something where you will have autocompletion.

Here are 5 additional steps that I already introduce:

  • Sentry: External error collecting (aggregating) tool + custom handler to see e.g. IDs of every Active Record object.
  • Generics: via PHPDocs + autocompletion via PhpStorm
  • No “mixed” types: Now we use something like, e.g. “array<int, string>” instead of “array”.
  • PSR standards: e.g. PSR-15 request handler, PSR-11 container, PSR-3 logger, …
  • Code Style: One code style to rule them all, we use PHP-CS-Fixer and PHP-Code-Sniffer to check / fix our code style for all ~ 10,000 PHP classes.

Here is what helped me mostly while working with old existing code.

First rule, first: 🥇 think or / and ask someone in the team

Analyzing: Here are some things that helped my analyzing software problems in our codebase.

  • Errors: Better error handling / reporting with a custom error handler, with all relevant information.
  • Understandable logging: Hint, you can just use syslog for medium-sized applications.
  • Grouping errors: Displaying and grouping all the stuff (PHP / JS / errors + our own custom reports) into Sentry (https://sentry.io/), now you can easily see how e.g. how many customers are effected from an error.
  • git history: Often new bugs were introduced with the latest changes (at least in often used components), so that good commit messages are really helpful to find this changes. (https://github.com/voku/dotfiles/wiki/git-commit-messages)
  • Local containers: If you can just download the application with a database dump from yesterday, you can analyze many problems without touching any external server.
  • Linux tools: mytop, strace, htop, iotop, lsof, …
  • Database tools:  EXPLAIN [SQL], IDE integration / autocompletion, …

Fixing: Here are some tricks for fixing existing code more easily.

  • IDE: PhpStorm with auto-completion and suggestions (including results from static analysis)
  • auto-code-style formatter: (as pre-commit hook) is also helpful because I do not need to think about this anymore while fixing code 
  • git stuff: sometimes it can also be helpful to understand git and how to revert or cherry-pick some changes

Preventing: Here are some hints how you can prevent some bugs.

  • custom static analysis rules: http://suckup.de/2022/07/php-code-quality-with-custom-tooling-extensions/
  • root cause: fixing the root cause of a problem, sometimes this is very hard because you need to fully understand the problem first, bust mostly spending this time is a good investment
  • testing: writing a test is always a good idea, at least to prevent the same problem

Job: If you now would like to work with this codebase (PHP 8 | MySQL 8 | Ubuntu), please contact me and take a look at this job offer: https://meerx.de/karriere/softwareentwickler-softwareentwicklerin/