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:
# 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:
{
"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:
<?php
declare(strict_types=1);
namespace App\Shop;
class Product
{
public function __construct(public string $name, public float $price) {}
}
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use App\Shop\Product;
$item = new Product("Keyboard", 89.99);
echo $item->name;
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:
composer require nesbot/carbon
<?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/.
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:
<?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:
<?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"
);
}
}
<?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
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:
- Run
composer init, with apsr-4autoload mapping fromApp\tosrc/. - Move last week's
Product,DigitalProduct,Sellable,LoggableandCartclasses intosrc/Shop/, each with the correctnamespace App\Shop;declaration. composer require nesbot/carbonand record each product'sCarboncreation timestamp instead of the plainDateTimeImmutablefrom before.- Define an
InsufficientStockException extends \RuntimeExceptioncarrying the product name, requested quantity and available quantity. - Update
Cart::addItem()to accept a quantity, throwInsufficientStockExceptionwhen requested quantity exceeds stock, and confirm atry/catchin your entry-point script prints a friendly message using the exception's properties.
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?
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?
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?
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?
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.