A namespace gives classes, interfaces, traits, enums, functions, and constants hierarchical names, so separate libraries can safely use the same short name.
use only resolves names while compiling the current file; it does not load a file, and dynamic strings do not apply imports. Functions and constants also have a global fallback that class-like names lack.
Declare the namespace and imports at the top of each file, pass SomeType::class when constructing dynamically, and keep Composer’s PSR-4 mapping exact in name, path, and case.
What it is and why it exists
A namespace is part of a declaration’s name, not a runtime container. App\Billing\Invoice and Vendor\Archive\Invoice both have the short name Invoice, but they are different classes. A project can therefore combine its own code with third-party packages without inventing a prefix for every type.
Namespaces can contain classes, interfaces, traits, enums, functions, and constants. They do not isolate variables or establish file scope. Ordinary variables still follow PHP’s existing variable-scope rules; a namespace changes only the resolution of supported declarations and their references.
You meet namespaces in nearly every modern PHP project. Framework components, Composer packages, test classes, and domain code all use complete names to distinguish ownership. Even a small entry script applies the same rules when it imports DateTimeImmutable, an exception type, or a library class.
Namespaces solve name collision and organization. Autoloading solves how PHP finds and loads a definition file when code needs a class-like name. PSR-4 often connects the two, but they are separate mechanisms: a correct use statement cannot repair a broken autoload mapping, and a correct mapping cannot repair the wrong class name.
How it works
Declarations and extent
The unbracketed form, namespace App\Billing;, applies from that point to the end of the file or the next namespace declaration. The bracketed form, namespace App\Billing { ... }, explicitly encloses code. One file cannot mix the two forms; production projects usually keep one namespace per file, while fixtures and demonstrations more often put several together.
A namespace declaration must be the first statement in its file, although declare may precede it. The opening <?php, whitespace, and comments are not statements; output, variable assignments, and require cannot come first. If a namespaced file also needs global code, it must use a bracketed namespace { ... } block.
Backslashes separate levels in a namespace name. The name does not inherently have to match a directory, but an autoloading standard can add that correspondence. Use stable organizational boundaries such as App\Billing; do not mechanically turn every folder into another layer of business taxonomy.
Four name forms
PHP resolves statically written class-like names when compiling a file. Class-like names include classes, interfaces, traits, and enums; functions and constants use similar but separate import tables.
| Form | Example | Resolution |
|---|---|---|
| Unqualified name | Invoice | Use the corresponding import alias first; a class-like name otherwise belongs to the current namespace |
| Qualified name | Model\Invoice | Replace the first segment if it is an import alias; otherwise append the name to the current namespace |
| Fully qualified name | \Vendor\Billing\Invoice | Start at the global root and ignore the current namespace |
namespace-relative name | namespace\Invoice | Explicitly append the name to the current namespace |
A fully qualified name starts with a backslash and is unambiguous, but repeating long names everywhere can obscure the business code. Usually you import once at the top and use a short name in the body. Diagnostics, configuration, and dynamic construction often need the complete name as a string without the leading backslash.
Import tables and aliases
use Vendor\Billing\Invoice; establishes the alias Invoice for class-like names in the current file. use Vendor\Billing\Invoice as VendorInvoice; establishes an explicit namespace alias . An alias resolves two imports with the same short name, and it can also name a namespace prefix, as in use Vendor\Billing as Billing;.
Functions and constants do not borrow the class-like import table. They use use function Vendor\Text\slugify; and use const Vendor\Config\VERSION;, respectively. Group imports can share a prefix, but mixed groups still need the function and const keywords, so several ordinary imports are often easier to read than a long mixed group.
An import takes effect at compile time and applies only to its file. An included file does not inherit the caller’s imports, and the caller does not receive imports from the included file. Keeping use declarations directly after namespace and before other declarations makes that file-level convention visible.
Global fallback for functions and constants
An unimported, unqualified class-like name resolves only in the current namespace; it does not automatically try a global class. Inside App\Billing, new DateTimeImmutable() means App\Billing\DateTimeImmutable. Import the built-in class or write new \DateTimeImmutable() to use it.
Unqualified functions and constants behave differently. PHP first tries the same name in the current namespace and then falls back to the global name if it finds none. Consequently, a local function may shadow strlen(), while \strlen() always selects the global function. A leading backslash makes the target stable if calling that built-in is part of the contract.
Qualified function names, qualified constant names, and fully qualified names do not perform this fallback. An import also determines the target directly. During review, distinguish “what this file imported” from “what PHP tried globally after an unqualified function was missing”; the steps happen at different phases.
Static names and dynamic strings
Invoice::class applies the current file’s imports to produce a complete class-name string, and obtaining that string alone does not load the class. It is useful in dependency-injection configuration, factory maps, and serialization metadata because refactoring tools can recognize it more reliably.
A dynamic string is not sent back through import resolution. new $class, class_exists($class), and string entries in callback arrays use the name contained in the string; a value of 'Invoice' means the global short name, not an imported App\Models\Invoice. Pass Invoice::class from the caller or keep class constants in a trusted map instead of assembling names manually.
Included files retain their own name context
A file targeted by include or require compiles the namespace declarations and imports in its own source. A caller in App\Web does not automatically place an included file with no namespace declaration into App\Web. Classes declared by that file remain in global scope.
An included file can access the variable scope at the inclusion site, but that variable rule does not change declaration names. Treating variable scope and namespaces as the same boundary creates accidental dependencies that no use list reveals.
Including a file that declares a class or function twice can still fail with a duplicate declaration; a namespace does not turn the second load into a separate copy. Use require_once when a declaration file must be loaded exactly once, although application classes should usually be loaded on demand by an autoloader.
PSR-4 cannot discover free functions one at a time. An entry point can load a function library explicitly, or Composer can load it through autoload.files; either way, its namespace declaration and function imports remain independently resolved per file.
Examples
The local PHP 8.3.33 CLI produced the output for all three examples. They build from colliding class names to global fallback and dynamic class names.
Distinguishing same-named classes with aliases
Two packages can both declare Receipt. The global code imports one under its original name and one under an alias, keeping each short name unique in this file.
<?php
declare(strict_types=1);
namespace Commerce\Sales {
final class Receipt
{
public function label(): string
{
return 'sales receipt';
}
}
}
namespace Commerce\Returns {
final class Receipt
{
public function label(): string
{
return 'return receipt';
}
}
}
namespace {
use Commerce\Returns\Receipt as ReturnReceipt;
use Commerce\Sales\Receipt;
$issued = new Receipt();
$returned = new ReturnReceipt();
printf("%s: %s\n", $issued::class, $issued->label());
printf("%s: %s\n", $returned::class, $returned->label());
}Commerce\Sales\Receipt: sales receipt
Commerce\Returns\Receipt: return receiptReceipt and ReturnReceipt are only local spellings in the last namespace block. The objects still belong to the complete names used at declaration. An alias neither creates a new class nor changes the name returned by reflection, logs, or $object::class.
The bracketed syntax lets this single-file example declare two namespaces and execute global code. A real PSR-4 project would normally put the classes in separate files and let its entry point load Composer’s autoloader.
Observing function fallback
App\Text declares its own strlen(), so an unqualified call selects it. PHP falls back to the global function only because no local count() exists; the leading backslash skips local candidates.
<?php
declare(strict_types=1);
namespace App\Text {
function strlen(string $value): int
{
return \strlen($value) * 10;
}
const MODE = 'namespaced';
function report(): void
{
printf("local strlen: %d\n", strlen('php'));
printf("global strlen: %d\n", \strlen('php'));
printf("global count: %d\n", count(['a', 'b', 'c']));
printf("mode: %s\n", MODE);
}
}
namespace {
\App\Text\report();
}local strlen: 30
global strlen: 3
global count: 3
mode: namespacedThe local strlen() must call \strlen() internally; otherwise it recurses into itself. The unprefixed count() works only because there is no App\Text\count(). If the global target is part of the contract, spelling it explicitly is more robust.
The constant MODE also belongs to App\Text. If PHP finds neither that constant nor a global constant with the same name, it raises an undefined-constant error instead of silently treating the name as a string.
Passing complete names to dynamic construction
An import affects the static expression Invoice::class, but it does not rewrite the ordinary string 'Invoice'. The factory accepts an already resolved complete name, so it does not have to guess the caller’s namespace context.
<?php
declare(strict_types=1);
namespace App\Models {
final class Invoice {}
}
namespace App\Factories {
use App\Models\Invoice;
final class Factory
{
public static function make(string $class): object
{
return new $class();
}
public static function localName(): string
{
return namespace\Factory::class;
}
}
function candidateNames(): array
{
return ['Invoice', Invoice::class, \App\Models\Invoice::class];
}
}
namespace {
use App\Factories\Factory;
use function App\Factories\candidateNames;
foreach (candidateNames() as $candidate) {
printf("%s => %s\n", $candidate, class_exists($candidate) ? 'found' : 'missing');
}
echo Factory::make(\App\Models\Invoice::class)::class, "\n";
echo Factory::localName(), "\n";
}Invoice => missing
App\Models\Invoice => found
App\Models\Invoice => found
App\Models\Invoice
App\Factories\FactoryThe last two values in the array are identical: imported Invoice::class and the fully qualified source name both produce App\Models\Invoice. The leading backslash marks the root in source code; it is not retained in the resulting class-name string.
namespace\Factory::class explicitly refers to Factory in the current namespace. It is occasionally useful in fixtures that move a whole namespace. Public configuration usually reads more clearly with an imported Factory::class that readers can trace directly.
Pitfalls
Mistaking an import for a load
When code first uses a class-like name, PHP may pass the resolved complete name to registered autoloaders. If vendor/autoload.php was not loaded, Composer’s map is stale, or the path does not match, changing the use declaration cosmetically still ends in Class ... not found.
Fix: load the autoloader at the application entry point, run composer dump-autoload after changing Composer autoload configuration, and exercise the resolved name through class_exists(ExpectedType::class) or the real construction path. Log the exact complete name received by the autoloader before inspecting its mapping.
Reusing one short name in two imports
Removing one import and scattering fully qualified names through the body may run, but it spreads the conflict policy across call sites. Reducing both types to cryptic initials merely exchanges the collision for a readability defect.
Fix: keep the natural short name for one domain type and give the other a role- or source-bearing alias such as VendorLogger or AuditLogger. Aliases are local to a file, so make a clear choice independently in each file.
Assuming dynamic strings use imports
Manually joining __NAMESPACE__ . '\\' . $suffix fits only a closed protocol in which every target genuinely belongs to the current namespace. If $suffix comes from a request, this also turns external input into a type-selection mechanism and may autoload and construct an unintended class with side effects.
Fix: for a closed set, use an explicit allowlist from external keys to class constants such as Invoice::class. Validate the shared interface after construction and reject unknown keys before construction; never use eval or treat user input directly as a class name.
Forgetting that classes do not fall back globally
This often appears after a global script is moved into a namespace. Its strlen() calls keep working, while built-in classes suddenly report as missing, making an extension or Composer problem look more likely than a resolution error.
Fix: import global classes you use or spell the complete name \DateTimeImmutable at a small number of call sites. When moving a file, inventory class-like names, functions, and constants separately rather than inferring one category’s behavior from another.
Letting PSR-4 case or hierarchy drift
A case-insensitive development filesystem can hide the difference between billing and Billing until deployment to a case-sensitive system. Another common drift puts a file under Services while its declaration says Service, sending the autoloader to a different path.
Fix: update namespaces and references in the same change that moves a file, then run the real entry point or tests in case-sensitive CI. Verify expected classes through Composer’s autoloader rather than running only php -l on individual files.
Confusing imports with closure capture
Generated code sometimes puts a class import inside a method or tries to import a variable with a file-level use. Both fail: a namespace import is a top-level declaration, while closure use (...) can only follow an anonymous function’s parameter list.
Fix: call the operations namespace import and closure capture during review. Put class names in the import area after the namespace declaration, put runtime variables in the closure capture list, and separately inspect lifetime and mutability for by-reference captures.
The compile-time boundary of name resolution
PHP processes namespace declarations, imports, and static class-like references while compiling a file. After use App\Models\Invoice as Bill; establishes an alias, Bill::class, new Bill(), and the type declaration Bill $invoice all refer to the same complete name. An alias is neither a second class in the runtime table nor class_alias(); the latter establishes another class name visible to runtime lookup and has substantially stronger semantics.
Import tables are separate by symbol kind. A file may simultaneously import class Vendor\Clock, function Vendor\Clock(), and constant Vendor\Clock; their short names are identical but do not overwrite one another. That code is legal but hard to read, so choose names that communicate kind and role unless a library API forces the collision.
The result of ::class is a string, and obtaining it does not trigger autoloading. Passing that string to new, to the default autoloading path of class_exists(), or to a reflection operation that needs the type may then call an autoloader. This phase boundary explains why container configuration can safely collect names of unloaded classes, and why merely printing Type::class cannot prove the mapping works.
__NAMESPACE__ similarly produces the current namespace string. It fits diagnostics and limited metaprogramming, but concatenated strings do not receive the same support from static analysis and rename tools as ::class. Prefer an explicit map of class constants when the target set is known.
A namespace is not an access-control boundary
A namespace grants no special access to code in the same namespace. public, protected, and private still follow class and inheritance relationships, not name prefixes. PHP has no namespace-level “package private” modifier.
Any code can refer to an accessible public declaration by its complete name without writing use. An import only changes local spelling; it neither grants permission nor hides a type. Do not treat a hard-to-guess namespace name as a security control.
A namespace also does not install or version a package. Composer package dependencies, autoload configuration, and release boundaries do that work. Two packages can still incorrectly declare the same complete class name and collide when loaded.
If an architecture requires modules to call one another only through public interfaces, enforce it with directory ownership, Composer package boundaries, static-analysis rules, and tests. Namespaces can express the design, but they do not prevent a cross-boundary reference by themselves.
The PSR-4 and Composer boundary
PSR-4 defines an autoloading convention from a complete class-like name to a file path. If the prefix Acme\ maps to src/, an autoloader removes that prefix, turns the remaining namespace separators into directory separators, and adds .php to the terminating class name. Acme\Billing\InvoiceWriter, for example, maps to src/Billing/InvoiceWriter.php.
Composer records the mapping under autoload.psr-4 in composer.json. Test-only names belong under the root package’s autoload-dev.psr-4, so they are not exposed to projects that consume the package. Run composer dump-autoload after changing the configuration, and load vendor/autoload.php once from the entry point.
{
"autoload": {
"psr-4": {
"Acme\\": "src/"
},
"files": ["src/functions.php"]
},
"autoload-dev": {
"psr-4": {
"Acme\\Tests\\": "tests/"
}
}
}PSR-4 handles class-like declarations such as classes, interfaces, traits, and enums; it does not lazily autoload free functions or constants. Composer’s autoload.files can include a function file when initializing the autoloader, but that loading model differs from one-file-per-class PSR-4 lookup. A static utility class is another option for a large function API, but the API shape, not an accommodation for the loader, should drive that modeling choice.
One prefix can map to several base directories, which Composer searches in configuration order. Overlapping prefixes can also exist, with the more specific prefix expressing narrower ownership. Do not rely on whichever duplicate definition happens to be found first; two definitions for one complete class name make deployment depend on installed contents and loading order.
The only connection between use and PSR-4 is the resolved complete name. The compiler first resolves InvoiceWriter to Acme\Billing\InvoiceWriter; an autoloader then tries to find the file. Debug in that order and record two facts: the exact name PHP requested and the exact path implied by the prefix mapping.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug