Composer and Namespaces in PHP
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
The era of a dozen require lines at the top of every file is over. Composer does two jobs: install third-party packages, and autoload your own classes by namespace.
Before you start
You need PHP 8.4 and Composer 2.8:
php -v
composer --version
On Windows, composer works in PowerShell once the installer has added the bin directory to PATH; if the command is not found, reopen the terminal before debugging anything else.
Initialising a project
mkdir blog-api && cd blog-api
composer init --name="demo/blog-api" --type=project --no-interaction
The generated composer.json looks roughly like this:
{
"name": "demo/blog-api",
"description": "Example project",
"type": "project",
"require": {
"php": ">=8.3"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"config": {
"sort-packages": true
}
}
PSR-4 maps a namespace prefix to a directory: App\Foo\Bar resolves to src/Foo/Bar.php. The prefix must end with a backslash and, in JSON, that backslash must itself be escaped — "App\\". Getting this wrong is the most common cause of “class not found” in a fresh project.
Namespaces
<?php
// src/Post/PostRepository.php
declare(strict_types=1);
namespace App\Post;
final class PostRepository
{
public function all(): array
{
return [];
}
}
<?php
// public/index.php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use App\Post\PostRepository;
$repo = new PostRepository();
var_dump($repo->all());
The rules that matter:
- One class per file, and the filename matches the class name. This is case-sensitive on Linux even though it is not on Windows or macOS — a mismatch that only fails in production.
namespaceis the first statement in the file, apart fromdeclare.useis only an alias. It does not load anything; the autoloader loads on first use, when the class is actually referenced.- Inside the same namespace, short names work. Across namespaces you need
useor the fully qualified\App\Post\PostRepository.
Variants of use:
use App\Post\PostRepository; // class
use App\Post\PostRepository as Repo; // aliased
use function App\Support\escape; // function
use const App\Support\VERSION; // constant
use App\Post\{Post, PostRepository}; // grouped
Installing packages
composer require guzzlehttp/guzzle
composer require --dev phpunit/phpunit
requiregoes intorequire(needed at runtime);--devgoes intorequire-dev, whichcomposer install --no-devskips on deploy.- Both update
composer.lock. Commit the lock file — it pins the exact version of every package, including transitive ones. - Do not commit
vendor/. Rebuild it from the lock file on each machine.
Version constraints:
| Constraint | Meaning |
|---|---|
^3.9 |
>=3.9 <4.0 — minor and patch upgrades allowed |
~3.9.2 |
>=3.9.2 <3.10 — patch level only |
3.9.* |
same as above |
>=3.9 <4.0 |
explicit range |
^ is the sensible default and also the one that bites: a major upgrade can break things, so run the tests after composer update.
The commands you actually use
composer install # install from the lock file (resolves composer.json only if there is none)
composer update # upgrade dependencies and rewrite the lock file
composer update vendor/package # upgrade one package
composer dump-autoload # regenerate the autoloader (run after adding classes)
composer outdated # list packages with newer versions
composer why guzzlehttp/psr7 # who pulled this package in
composer validate # check composer.json for mistakes
On CI and in production:
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction
--optimize-autoloader builds a classmap, which is faster than PSR-4 path resolution once you have a few hundred classes. The catch: the classmap is static, so new classes need composer dump-autoload -o before they resolve.
Autoloading functions and constants
PSR-4 handles classes only. Functions, constants and bootstrap files go under files:
{
"autoload": {
"psr-4": { "App\\": "src/" },
"files": ["src/Support/helpers.php"]
}
}
Files listed there are loaded on every request. Keep them to pure helper functions and constants — no database connections, no sessions, no side effects.
Scripts
The scripts block fixes common commands inside the project:
{
"scripts": {
"test": "phpunit",
"serve": "php -S localhost:8000 -t public"
}
}
composer test
composer run serve
Hooks such as post-install-cmd and post-update-cmd are fine for cache clearing or proxy generation. Do not put anything there that needs human confirmation.
Production notes
- Keep
APP_ENV, database passwords and API keys in environment variables. Add.envto.gitignoreand commit a.env.exampleinstead. - Deploy with
composer install, nevercomposer update. vendor/should be readable but not writable by the web user.- If you pin a PHP version in
composer.json, the deploy machine must match it orcomposer installfails on platform requirements.--ignore-platform-reqshides the problem rather than solving it.
Common mistakes
1. Namespace and directory disagree
App\Post\PostRepository living in src/Repository/PostRepository.php only works with a classmap. Put files where PSR-4 says they go.
2. New class not found in production
With --optimize-autoloader the classmap is static. Run composer dump-autoload -o after adding or renaming classes.
3. Editing files inside vendor/
The next composer install wipes them. Fork, use cweagans/composer-patches, or send a pull request upstream.
4. Using * or dev-main as a constraint
Builds stop being reproducible and can pick up an incompatible commit at any time. Pin production dependencies to a tag.
5. Forgetting require vendor/autoload.php
A CLI script that dies with “Class not found” usually missed this line, or used a relative path where __DIR__ was needed.
6. Side effects in files
Starting a session or connecting to a database there makes CLI scripts pay for it too. Keep files pure.
7. Platform drift
Dev runs PHP 8.4, the server runs 8.2, composer install fails. Declare the target explicitly:
{ "config": { "platform": { "php": "8.2.0" } } }
Summary
- Composer manages dependencies; PSR-4 maps namespaces to directories.
- The lock file gives reproducible installs; deploy with
composer install --no-dev -o. - Prefer
^constraints, and run the test suite after major upgrades. - Functions and constants go through
autoload.files, with no side effects. - Keep namespace, directory and filename aligned — that alone prevents most “class not found” errors.
Next: talking to a database with PDO, without building SQL by string concatenation.