Modern PHP

Lesson 1 of 5

Namespaces, PSR and Autoloading

"Let's call something a rigid designator if in every possible world it designates the same object." — Saul Kripke, Naming and Necessity (1980)

\App\Domain\Order is a rigid designator. It means the same class no matter where it is written, which file it appears in, or what else is in scope. Order, on its own, is not: it means whatever the surrounding context says it means, and the surrounding context is a set of rules most PHP developers have absorbed by osmosis rather than learned.

Part I already covered the mechanism that turns a name into a file — Composer, PSR-4, the generated maps. This chapter is about the layer above it: how PHP itself resolves a name before any autoloader is consulted, and why the rules are not the same for classes, functions and constants.

The asymmetry nobody mentions

Start with the rule that is genuinely surprising and genuinely load-bearing:

PHP — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

Functions and constants fall back to the global namespace; classes do not. strlen() works unqualified inside App because PHP looks in App, finds nothing, and tries global. new Widget() fails with Class "App\Widget" not found — it looked in App, found nothing, and stopped.

This asymmetry is not an oversight. It exists because classes are almost always autoloaded and functions almost never are: if class names fell back to global, every mistyped or un-imported class name would trigger a global autoload attempt, and a missing use statement would produce a confusing error from somewhere else entirely. Functions, by contrast, are mostly the built-in library, and requiring \strlen() everywhere would have been intolerable.

The practical consequence is the one people meet: you must use a class or fully qualify it, but you need not do either for a function. Which is why a file full of use statements at the top is normal PHP and nobody imports strlen.

An analogy

Think of how you refer to people in a large office. Inside your own team, "Ada" is unambiguous and everyone knows who you mean. Say "Ada" in a company-wide meeting and it is a question rather than a reference. So the organisation develops two conventions at once: for a small set of universally known people — the managing director, the head of security — a first name works anywhere, because there is only one and everyone has met them. For everyone else you say the full name the first time and then use the short form for the rest of the conversation, which is exactly what a use statement is: an announcement at the top of the document that for the duration, this short name means that specific person. PHP's asymmetry is the same distinction. The built-in functions are the universally known handful; your classes are everybody else.

::class is not a lookup

The magic constant most people treat as "the name of this class" is doing something more specific:

PHP — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

NoSuchClass::class returns App\NoSuchClass without error, and class_exists on the result is false. That is the whole point: ::class performs name resolution, not a lookup. It applies the current namespace and any use statements, produces a string at compile time, and never touches the autoloader.

That is precisely why it should be used in place of hand-written class-name strings everywhere. The string 'App\Thing' and Thing::class are the same bytes, but only the second one is understood by an IDE's rename refactoring, flagged by static analysis when the class moves, and updated automatically when the namespace changes. It costs nothing and it converts a silent breakage into a visible one.

Names that arrive as strings

The rule that catches everyone building something dynamic:

PHP — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

new $short() with $short = 'Thing' fails, because a class name in a variable is treated as already fully qualified — the current namespace is not applied and use statements are not consulted. They cannot be: use is resolved at compile time and a variable's contents are not known until runtime.

The same rule governs every function that takes a class name as a string, which is a long list: class_exists, is_subclass_of, method_exists, container bindings, event listener registration, and every framework config file that names a class. In all of them, a bare 'Thing' means \Thing and nothing else.

Thing::class fixes all of it at once, which is why the constant exists. Before PHP 5.5 the only option was a hand-written fully-qualified string, and an enormous amount of older framework configuration still looks that way.

There is one situation where a string genuinely is the only option, and it is worth recognising so the rule does not get applied where it cannot work: a class name read from data — a database column, a queue payload, a configuration file loaded at runtime. A job record storing App\Jobs\SendInvoice has no compile-time moment at which ::class could have been evaluated, so the string is unavoidable.

What that costs is a rename hazard the tooling cannot see, and the mitigation is to narrow where the string is trusted rather than to eliminate it. Validate it against an allow-list before instantiating, because a class name arriving from data and passed straight to new is a genuine security concern — an attacker who controls it can instantiate arbitrary classes, and combined with a constructor that does work, that is a real exploitation path rather than a theoretical one. Frameworks that persist class names (queues, event stores) handle this with a registry mapping short aliases to real names, which solves both problems at once: renames update one map, and only mapped names can be instantiated.

A little history

PHP got namespaces in 5.3, in 2009, which is late — fourteen years in, and after the ecosystem had already solved the problem another way. The prevailing convention until then was the PEAR naming scheme, where a class called Zend_Db_Table_Abstract encoded its own path in underscores, and PSR-0 formalised that by specifying that underscores in a class name become directory separators. It worked, and its legacy is why very old PHP code has such long class names. The famously contentious part of the eventual implementation was the separator. The obvious candidates — :: and . — were already taken by static access and string concatenation, and after an extended argument the backslash was chosen, to widespread dismay, because it was one of the few characters available. The practical cost of that decision is a specific class of bug you will meet: "App\Thing" in a double-quoted string is not the class you meant, because \T is an escape sequence, and the name silently becomes something else. In single quotes it is fine. This is the reason ::class was added in 5.5 and the reason to prefer it over a string, and it is a rare case where a syntax decision produced a bug class large enough to require a language feature to repair.

Importing, and what use actually imports

use has three forms, and the two most useful are the least used:

PHP — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

Three things worth pinning down. use is file-scoped, not namespace-scoped: it applies from that statement to the end of the file, and a second file in the same namespace imports nothing automatically. use affects names, not strings — after importing Generator, the literal 'Generator' still means \Generator, which is the same rule as the previous section and the reason it keeps mattering.

And use function and use const exist. They are rare in application code and common in well-written libraries, because without them every unqualified function call inside a namespace does a two-step lookup — current namespace, then global — and importing the function makes the resolution direct. The performance difference is small; the clarity is not, since an imported function is visible at the top of the file like every other dependency.

The big question

Do the PSRs actually matter, or are they a committee's paperwork? The evidence is unusually clear, and it is Composer. PSR-0 and later PSR-4 did one thing: they agreed on how a class name maps to a file path. That is not a technical achievement — any of a dozen schemes would have worked — and its value came entirely from being agreed, because a shared convention is what allowed a package manager to install arbitrary code from strangers and have it load without configuration. Every library after that point could assume its consumers could find its classes, which is the precondition for an ecosystem rather than a collection of downloads. The other PSRs vary. PSR-1 and PSR-12 are coding style, which matters for shared codebases and is otherwise taste; PSR-7 (HTTP messages) and PSR-11 (containers) are genuinely load-bearing interoperability standards that let a middleware written for one framework run in another; some of the rest are little used. The general lesson is worth extracting because it applies well beyond PHP: the standards that succeed are the ones that let independently-written things fit together, and the ones that fail are the ones that tell you how to write code you were going to write anyway. Interoperability is the product; agreement is the mechanism; and being technically optimal has almost nothing to do with it.

The standards worth knowing by number

PSR references get thrown around as though everyone has read them. Most are short, and only a handful affect daily work.

PSR-4 is the autoloading standard from Part I: a namespace prefix maps to a base directory, and everything below the prefix mirrors the directory structure. It replaced PSR-0, whose distinguishing rule was that underscores in a class name became directory separators — a rule inherited from the pre-namespace era, when Zend_Db_Table was how you faked namespacing. Dropping that is why modern PHP trees are shallow, and it is the reason you occasionally still meet a very old library with underscored class names.

PSR-1 is basic coding standards and is mostly uncontroversial: class names in StudlyCaps, method names in camelCase, class constants in upper snake case, and one notable rule with teeth — a file should either declare symbols or have side effects, not both. That is the rule behind putting no executable code in a class file, and it exists because a file that defines a class and also connects to a database cannot be autoloaded safely.

PSR-12 is the extended style guide — indentation, line length, brace placement, where use statements go. Its actual value is not that its choices are correct but that they are decided, which converts style from something to argue about into something a tool enforces. php-cs-fixer and phpcs both implement it, and running one in CI removes the entire category of review comment.

Then the interoperability set, which is where the standards earn their existence. PSR-3 (logging), PSR-7 (HTTP messages), PSR-11 (containers), PSR-14 (events) and PSR-15 (HTTP handlers) are all interfaces rather than rules — a package types against Psr\Log\LoggerInterface and works with Monolog, or your logger, or a fake in a test, with no coupling to any of them. PSR-15 middleware written for one framework genuinely runs in another.

That split is the thing worth internalising about the whole programme: the style PSRs make a codebase consistent, and the interface PSRs make independently written packages fit together. Only the second kind could not have been achieved any other way.

Organising a namespace

The rules above say nothing about how to choose namespaces, which is the part people actually agonise over. A few observations that follow from the mechanism rather than from taste.

A namespace is a naming device, not a boundary the language enforces. Nothing is private to a namespace, nothing is prevented from reaching across one, and there is no such thing as internal visibility — a class in App\Infrastructure can be instantiated from App\Domain with no complaint at all. If you want boundaries, you need discipline, review, or a static analysis tool configured to fail the build; the namespace itself is only a label.

Depth costs nothing at runtime and something in readability. App\Domain\Billing\Invoice\InvoiceLineItemFactory resolves exactly as fast as App\Invoice, so the argument for shallower trees is entirely about humans — and the usual failure is mirroring an organisational chart or a design pattern taxonomy rather than the thing the software is about.

The one rule that is not taste: the namespace must match the PSR-4 prefix and directory, because that is what makes the autoloader work. A file whose namespace and path disagree loads fine when something else has already included it and fails mysteriously when it has not, which is the "works locally, fails in production" case from Part I. It is worth having a tool check this rather than eyes — composer dump-autoload will warn, and most static analysers will too.

Beyond that, two patterns recur often enough to be worth naming, and they disagree.

Layer-first puts the architectural role at the top: App\Controllers, App\Models, App\Services, App\Repositories. It is what most framework skeletons ship with, it is immediately navigable to anyone who knows the framework, and its weakness shows up as an application grows — a single feature ends up scattered across five directories, so changing "billing" means touching files that are never near each other, and nothing in the structure records that they belong together.

Feature-first puts the domain at the top: App\Billing\Invoice, App\Billing\InvoiceController, App\Shipping\Label. A feature is then one directory, which makes it obvious what belongs to it, easy to see its boundaries, and possible to delete cleanly. The weakness is that it needs decisions nobody has enough information to make early — you have to know what the features are before you have built them — and a premature split produces directories that do not match how the software actually turned out.

The honest guidance is that the first is right for small applications and the second for large ones, which is unhelpful at the moment you must choose. What is more useful is knowing that the migration is cheap in PHP specifically. Because a namespace is only a name, moving a class between namespaces is a rename plus a directory move, both of which an IDE does reliably and neither of which changes any behaviour — there is no compiled artefact to rebuild, no module graph to update, no header to fix. So the cost of starting layer-first and reorganising later is unusually low, which argues for deferring the decision rather than agonising over it.

What is not cheap is a class that has accumulated dependencies in both directions across a boundary you later want to enforce. That is a design problem wearing a namespace problem's clothes, and no amount of moving files fixes it.

Asking the runtime instead of reading

When a name does not resolve, reading harder rarely helps. The runtime knows things the source does not say:

PHP — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

class_exists($name, autoload: false) is the useful pair to the ordinary call: the first asks could this be loaded, the second asks is it loaded already. A difference between them tells you the autoloader is doing work you may not have expected.

ReflectionClass::getFileName() is the one that most often ends an investigation. In a codebase with both Composer autoloading and legacy require statements — which is most codebases with any history — the file a class came from can genuinely differ from the file you are editing, and no amount of careful reading will reveal it. Asking takes one line.

From my notebook

The measurement that taught me something here was NoSuchClass::class returning a string without complaint. I had used ::class for years with a vague model of it as "get the name of this class", which carries an implication that the class is there — and it is not doing that at all; it is doing pure textual resolution and it will happily produce a name for something that has never existed. Once seen, several things fall into place that I had held separately: why ::class works in a docblock context where nothing is loaded, why it is safe in a config file listing classes that may not be installed, and why it costs literally nothing at runtime. It is a compile-time string operation wearing a class-lookup's clothes. I had been right about when to use it and wrong about what it was, which is a comfortable kind of wrong to be — right up until you need to reason about a case the folk model does not cover.

In the real world

The namespace bug that costs the most time is the one where nothing is wrong with the namespace at all. A class is not found; the developer checks the namespace line, checks the use statement, checks the file path, and all three are correct — because the actual problem is a stale Composer classmap, a case difference that only matters on Linux, or a require somewhere that loaded a second copy of the class from a different path. The diagnostic that cuts through all of it is to stop reasoning and ask the runtime: class_exists() tells you whether it is loadable, (new ReflectionClass($name))->getFileName() tells you which file it actually came from, and get_declared_classes() tells you what is present right now. The second of those is the one people forget and the one that most often produces the answer, because "which file did this class come from" is frequently a different question from "which file did I expect" — and in a codebase with both Composer autoloading and legacy require statements, the two can disagree in ways no amount of reading will reveal.

Common Mistakes

  1. Expecting classes to fall back to global. Functions and constants do; classes do not. Measured: new Widget() failed inside App.

  2. Writing class names as literal strings. 'Thing' means \Thing regardless of namespace or imports.

  3. Using a double-quoted string for a namespaced name. "App\Thing" contains an escape sequence; single quotes or ::class.

  4. Assuming ::class checks the class exists. NoSuchClass::class returned a name happily; nothing is loaded.

  5. Expecting use to apply to a whole namespace. It is file-scoped, and a second file imports nothing automatically.

  6. Expecting use to affect strings. After importing, the literal 'Generator' still means \Generator.

  7. Treating a namespace as an access boundary. Nothing is private to a namespace; it is a label, not a wall.

  8. Letting the namespace and directory diverge. The autoloader cannot find it, and it fails only when nothing else loaded it first.

  9. Ignoring use function and use const. They exist, they make the dependency visible, and they remove a two-step lookup.

  10. Debugging "class not found" by reading. Ask the runtime which file it came from, with reflection.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Warm-up
Why can you call strlen() inside a namespace but not new Widget() for a global class?
Model answer — Because functions and constants fall back to the global namespace and classes do not. An unqualified function is looked for in the current namespace and then globally; an unqualified class name is looked for in the current namespace only, and fails there. That is deliberate: classes are autoloaded, so a fallback would make every missing use statement trigger a confusing global load attempt, while functions are mostly built-ins and requiring \strlen() everywhere would be intolerable.
Intermediate
What does Thing::class actually do?
Model answer — Pure name resolution at compile time. It applies the current namespace and any use statements and produces a string — it does not load or check anything, which you can see by writing NoSuchClass::class, which returns a fully-qualified name for a class that does not exist. That is exactly why it should replace hand-written class-name strings: the bytes are identical, but only ::class is followed by a rename refactoring and flagged by static analysis when the class moves.
Advanced
A class name held in a variable fails to instantiate, but the same name works when written literally. Why?
Model answer — Because a class name in a variable is resolved as fully qualified — the current namespace is not applied and use statements are not consulted. It cannot work otherwise: use is resolved at compile time and the variable's contents are not known until runtime, so there is nothing for the compiler to rewrite. The same applies to every string-taking API — class_exists, container bindings, config files naming classes — and the fix is to put Thing::class into the variable rather than a bare string, since that resolves correctly at the point it is written.
micro1Practical: ::class over strings, and why.GoogleDeepest — the fallback asymmetry and the reason for it.MetaCompile-time versus runtime resolution.AmazonDiagnosing class-not-found with reflection rather than reading.

Hands-on

Hands-on

Build the resolver this chapter measured. Implement resolves, the fallback rule by symbol kind; resolveName, turning a written name into a fully-qualified one; fromVariable, the dynamic-name rule; classConstant, what ::class produces; and isSafeReference, whether a reference survives a rename. A stub scores zero, so a passing run means the model is real.

PHP — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

Summary

  • Functions and constants fall back to the global namespace; classes do not. Measured: new Widget() failed inside App.
  • The asymmetry is deliberate — classes are autoloaded, so a fallback would turn every missing import into a confusing global load.
  • ::class is compile-time name resolution, not a lookup. NoSuchClass::class returned a name without error.
  • Prefer ::class to a literal string. Identical bytes; only one is followed by renames and checked by analysis.
  • A class name in a variable is always fully qualified — namespace and imports are not applied, because they resolve at compile time.
  • The same applies to every string-taking API: class_exists, container bindings, config files naming classes.
  • use is file-scoped and affects names, not strings. A second file in the same namespace imports nothing.
  • use function and use const exist and remove a two-step lookup while making the dependency visible.
  • A namespace is a label, not a boundary. Nothing is private to one; enforcement needs tooling.
  • "App\Thing" in double quotes is not the class you meant — \T is an escape sequence.

Next: Interfaces, Traits and Composition — three ways to share behaviour, what each actually creates, and when a trait is the wrong answer.

Editorial reflection

Namespaces are the clearest case in this volume of a feature whose design was constrained almost entirely by history rather than by what would have been best. The separator is a backslash because the good characters were taken. The fallback rules differ by symbol kind because classes were already autoloaded and functions were already global. ::class exists because string class names were already everywhere and needed a safer replacement that looked similar enough to adopt. Not one of those decisions would be made the same way in a language designed today, and every one of them is defensible given what could not be changed. I find that worth sitting with, because the instinct when meeting a design like this is to conclude that the designers were careless, and the evidence here points the other way: they were repeatedly choosing the least bad option available inside constraints they had inherited. That is what most engineering actually is, and the reason a volume like this measures rather than editorialises is that the measurement tells you what was decided while the reputation only tells you how people felt about it.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.