1. Classes & Properties
A class bundles related data (properties) and behavior (methods) into one unit. PHP 8 lets you type-hint properties directly, the same way you type-hint function parameters:
<?php
declare(strict_types=1);
class Product
{
public string $name;
public float $price;
public bool $inStock = true; // default value
public function formattedPrice(): string
{
return '$' . number_format($this->price, 2);
}
}
$keyboard = new Product();
$keyboard->name = "Mechanical Keyboard";
$keyboard->price = 89.99;
echo $keyboard->formattedPrice(); // "$89.99"
$this inside a method refers to the specific object the method was
called on — $keyboard->formattedPrice() runs with
$this === $keyboard. Every property access from outside the class uses
->, PHP's object operator (not the dot used in many other languages).
2. Constructors & Visibility
Setting every property manually after new is error-prone — a
constructor lets you require values up front. PHP 8's
constructor property promotion collapses the common
declare-property-then-assign-in-constructor pattern into one line per property:
<?php
declare(strict_types=1);
class Product
{
public function __construct(
public string $name,
public float $price,
private int $stockCount = 0,
) {}
public function isInStock(): bool
{
return $this->stockCount > 0;
}
}
$keyboard = new Product("Mechanical Keyboard", 89.99, 5);
echo $keyboard->name; // "Mechanical Keyboard" -- public, accessible outside
echo $keyboard->isInStock(); // true
// $keyboard->stockCount; // Error: Cannot access private property
Visibility modifiers control where a property or method can be accessed from:
public— accessible from anywhere, including outside the classprotected— accessible from within the class and its subclassesprivate— accessible only from within the declaring class itself
private, widen when you need to
Starting properties as private and only exposing what callers actually need (through methods, or by widening deliberately) keeps a class's internal representation free to change without breaking code elsewhere — a smaller, more honest public surface is easier to reason about and to refactor safely.
3. Inheritance & Interfaces
A class can extend exactly one parent class, inheriting its public
and protected members and optionally overriding methods:
<?php
declare(strict_types=1);
class DigitalProduct extends Product
{
public function __construct(
string $name,
float $price,
public string $downloadUrl,
) {
parent::__construct($name, $price, stockCount: PHP_INT_MAX); // never out of stock
}
}
An interface declares a contract — method signatures with no implementation — that any implementing class must fulfill. Unlike single inheritance, a class can implement multiple interfaces:
<?php
declare(strict_types=1);
interface Discountable
{
public function applyDiscount(float $percentOff): float;
}
class Product implements Discountable
{
public function __construct(public string $name, public float $price) {}
public function applyDiscount(float $percentOff): float
{
return $this->price * (1 - $percentOff / 100);
}
}
function printSalePrice(Discountable $item, float $percentOff): void
{
echo number_format($item->applyDiscount($percentOff), 2);
}
Typing the parameter as Discountable rather than Product
means printSalePrice() works with any class that implements the
interface — this is the basis of dependency injection, which shows up constantly
once Laravel enters the picture in Week 9.
4. Traits
PHP has no multiple inheritance — a class can only extend one parent.
A trait is a way to share method implementations across unrelated
classes without inheritance, by literally copying the trait's methods into the
class at compile time:
<?php
declare(strict_types=1);
trait Timestamps
{
private ?\DateTimeImmutable $createdAt = null;
public function markCreated(): void
{
$this->createdAt = new \DateTimeImmutable();
}
public function createdAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
}
class Product
{
use Timestamps;
public function __construct(public string $name) {}
}
class Order
{
use Timestamps;
public function __construct(public int $id) {}
}
$product = new Product("Keyboard");
$product->markCreated();
echo $product->createdAt()->format('Y-m-d'); // today's date
Product and Order share no inheritance relationship, but
both get the exact same markCreated()/createdAt()
behavior by using the same trait — Laravel's Eloquent models use this pattern
heavily (its own SoftDeletes trait works exactly this way).
5. Namespaces & PSR-4
A namespace is a prefix that groups related classes and avoids
name collisions — two libraries can both define a Logger class as
long as they live in different namespaces:
<?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__ . '/Shop/Product.php';
use App\Shop\Product;
$item = new Product("Keyboard", 89.99);
// Or, without the 'use' import: new \App\Shop\Product(...)
Manually require-ing every file doesn't scale. PSR-4
is the community convention — implemented by Composer's autoloader, covered in
full next week — that maps a namespace prefix directly onto a directory structure,
so App\Shop\Product resolves to src/Shop/Product.php
automatically, with no manual require calls at all.
6. Hands-on Exercise
Model a small shopping cart with OOP
Apply classes, interfaces and a trait to a self-contained cart domain.
Requirements:
- Define a
Sellableinterface with aprice(): floatmethod. - Create a
Productclass implementingSellable, using constructor property promotion forname,priceandprivate stockCount. - Create a
DigitalProductclass that extendsProductand adds adownloadUrlproperty, callingparent::__construct(). - Write a
Loggabletrait with alog(string $message): voidmethod that echoes a timestamped message, anduseit in both classes. - Create a
Cartclass with an array ofSellableitems, anaddItem(Sellable $item): voidmethod, and atotal(): floatmethod that sums every item'sprice(). - Put everything under the
namespace App\Shop;and confirm the script runs correctly with a mix ofProductandDigitalProductitems in the cart.
Typing Cart::addItem(Sellable $item) against the interface, not the concrete Product class, is exactly what lets the same method accept both Product and DigitalProduct without any special-casing.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What does constructor property promotion actually save you from writing?
What does constructor property promotion actually save you from writing?
The separate property declaration and the $this->name = $name; assignment line inside the constructor body. Prefixing a constructor parameter with a visibility modifier (public string $name) declares the property and assigns it from the argument in one step — pure boilerplate reduction with no behavior difference from writing it out longhand.
Q2
Why type a function parameter as an interface (Discountable) instead of a concrete class (Product)?
Why type a function parameter as an interface (Discountable) instead of a concrete class (Product)?
Typing against the interface means the function accepts any class that implements it, not just Product — a new class implementing Discountable six months later works with that function automatically, no changes required. This is the core idea behind dependency injection, which every Laravel controller and service you'll write from Week 9 onward leans on.
Q3
Why reach for a trait instead of a shared base class?
Why reach for a trait instead of a shared base class?
PHP classes can only extend one parent, but a class can use several traits. When two otherwise-unrelated classes (like Product and Order) need identical behavior but shouldn't be forced into an artificial shared parent class, a trait shares that implementation without imposing an inheritance relationship between them.
Q4
What problem do namespaces solve that plain class names alone don't?
What problem do namespaces solve that plain class names alone don't?
Name collisions. Without namespaces, only one class named Logger (or Product, or User) could exist across your entire codebase and every installed library combined. A namespace prefix (App\Shop\Product vs. a library's Vendor\Package\Product) lets identically-named classes coexist safely, and — via PSR-4 — maps cleanly onto a predictable directory structure.