Week 4: Composer, Autoloading & Error Handling

Last week ended on PSR-4 as a concept. This week makes it real: installing Composer, using it to autoload your own classes with zero manual require calls, pulling in a third-party package from Packagist, and replacing ad-hoc error checks with PHP's exception system.

Module 4 of 28 Week 4 of 32 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Set up Composer & autoload your own classes via composer.json
  • Install and use a real third-party package from Packagist
  • Handle errors with exceptions, try/catch/finally & custom exception classes

1. Composer Setup

Composer is PHP's dependency manager — it installs packages, resolves version constraints, and generates the autoloader every non-trivial PHP project relies on. Install it once per machine, then initialize a project:

terminal
# macOS/Linux
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

# Windows: download Composer-Setup.exe from getcomposer.org

composer --version
# Composer version 2.7.x

mkdir week-04 && cd week-04
composer init --name="you/week-04" --type=project -n

composer init creates a composer.json — the manifest describing your project's name, dependencies and autoloading rules:

composer.json
{
    "name": "you/week-04",
    "type": "project",
    "require": {},
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

2. Autoloading Your Own Code

The "psr-4": { "App\\": "src/" } mapping tells Composer: any class under the App namespace lives in src/, following the same path structure as the namespace. Generate the autoloader and it just works:

src/Shop/Product.php
<?php
declare(strict_types=1);

namespace App\Shop;

class Product
{
    public function __construct(public string $name, public float $price) {}
}
index.php
<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use App\Shop\Product;

$item = new Product("Keyboard", 89.99);
echo $item->name;
terminal
composer dump-autoload
php index.php
# Keyboard

One require __DIR__ . '/vendor/autoload.php'; at the entry point replaces every manual require for every class you'll ever write — Composer's autoloader resolves App\Shop\Product to src/Shop/Product.php on demand, the moment the class is first referenced.

3. Installing Packages from Packagist

Packagist is PHP's central package registry — composer require installs a package and adds it to composer.json automatically:

terminal
composer require nesbot/carbon
date-example.php
<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Carbon\Carbon;

$launch = Carbon::parse('2026-09-01');
echo $launch->diffForHumans(); // "in X weeks" -- relative to now
echo $launch->format('l, F jS Y'); // "Tuesday, September 1st 2026"

Carbon is a widely used date/time library and a good first real dependency to install — it's also what Laravel uses internally for every timestamp starting Week 9. Composer resolves its version against your PHP version, writes the resolved versions into composer.lock, and downloads it into vendor/.

Commit composer.lock, ignore vendor/

composer.lock pins the exact resolved version of every dependency so every machine building this project gets identical versions — commit it. vendor/ is regenerable from composer.json/composer.lock via composer install, so it belongs in .gitignore, the same way node_modules/ does for a JS project.

4. Exceptions

Instead of returning a special "error" value and hoping every caller checks for it, PHP lets a function throw an exception that unwinds the call stack until something catches it:

exceptions.php
<?php
declare(strict_types=1);

function divide(float $a, float $b): float
{
    if ($b === 0.0) {
        throw new \InvalidArgumentException("Cannot divide by zero");
    }
    return $a / $b;
}

try {
    echo divide(10, 0);
} catch (\InvalidArgumentException $e) {
    echo "Error: " . $e->getMessage();
} finally {
    echo "\nDivision attempt finished.";
}

finally runs whether or not an exception was thrown or caught — useful for cleanup like closing a file handle or database connection. PHP's built-in exception hierarchy covers most common cases: InvalidArgumentException, OutOfRangeException, RuntimeException, and their common ancestor \Exception. Catch the most specific type your code can meaningfully recover from.

5. Custom Exception Classes

A custom exception class documents intent and lets calling code catch precisely the failure it knows how to handle, without swallowing unrelated errors:

src/Shop/InsufficientStockException.php
<?php
declare(strict_types=1);

namespace App\Shop;

class InsufficientStockException extends \RuntimeException
{
    public function __construct(
        public readonly string $productName,
        public readonly int $requested,
        public readonly int $available,
    ) {
        parent::__construct(
            "Cannot fulfill {$requested}x {$productName}: only {$available} in stock"
        );
    }
}
checkout.php
<?php
use App\Shop\InsufficientStockException;

try {
    $order->place();
} catch (InsufficientStockException $e) {
    // Specific, structured data to work with -- not just a string to parse
    logStockIssue($e->productName, $e->requested, $e->available);
    echo "Sorry, only {$e->available} left of {$e->productName}.";
} catch (\Throwable $e) {
    // Genuinely unexpected -- log it and show a generic message
    error_log($e->getMessage());
    echo "Something went wrong. Please try again.";
}

Extending \RuntimeException (rather than the base \Exception) signals "this failure happens due to something at runtime, not a programmer error" — a convention that helps future readers (and you) understand intent at a glance. Notice the two-tier catch: a specific, recoverable case first, then a broad \Throwable safety net last.

6. Hands-on Exercise

Hands-on

Composer-ize last week's cart, with real error handling

Turn Week 3's classes into a proper Composer project and add exceptions for the failure cases you ignored.

Requirements:

  1. Run composer init, with a psr-4 autoload mapping from App\ to src/.
  2. Move last week's Product, DigitalProduct, Sellable, Loggable and Cart classes into src/Shop/, each with the correct namespace App\Shop; declaration.
  3. composer require nesbot/carbon and record each product's Carbon creation timestamp instead of the plain DateTimeImmutable from before.
  4. Define an InsufficientStockException extends \RuntimeException carrying the product name, requested quantity and available quantity.
  5. Update Cart::addItem() to accept a quantity, throw InsufficientStockException when requested quantity exceeds stock, and confirm a try/catch in your entry-point script prints a friendly message using the exception's properties.
Hint

Run composer dump-autoload any time a new class doesn't seem to be found — it's the most common Composer gotcha, and regenerating the autoloader map fixes it in seconds.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What's the actual difference between composer.json and composer.lock?

composer.json declares version constraints you're willing to accept (e.g. ^2.0). composer.lock records the exact versions Composer actually resolved and installed the last time dependencies were updated. Committing the lock file means every machine running composer install gets identical dependency versions, not just versions matching the same loose constraints.

Q2

How does Composer know that App\Shop\Product lives in src/Shop/Product.php?

From the "psr-4": { "App\\": "src/" } mapping in composer.json. PSR-4 says a namespace prefix maps onto a directory: strip the App\ prefix from the full class name, replace remaining \ with /, and look in src/ — so App\Shop\Product resolves to src/Shop/Product.php automatically.

Q3

What does finally guarantee that catch alone doesn't?

The finally block runs regardless of whether the try block succeeded, threw an exception that was caught, or threw an exception that propagated past every catch clause. It's the right place for cleanup — closing a file handle or database connection — that must happen no matter how the block exits.

Q4

Why define InsufficientStockException instead of throwing a generic \Exception with a message string?

A specific exception type lets calling code catch exactly this failure case without accidentally swallowing unrelated exceptions that also happen to be \Exception. It also carries structured data as real typed properties (productName, requested, available) that calling code can use programmatically, instead of having to parse a human-readable message string.