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.

PHP 8.4 · Composer 2.8

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.
  • namespace is the first statement in the file, apart from declare.
  • use is 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 use or 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
  • require goes into require (needed at runtime); --dev goes into require-dev, which composer install --no-dev skips 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 .env to .gitignore and commit a .env.example instead.
  • Deploy with composer install, never composer 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 or composer install fails on platform requirements. --ignore-platform-reqs hides 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.