OOP and Classes
Object-oriented programming (OOP) is a paradigm that organizes code around objects rather than functions and procedures. PHP has robust OOP support, and most modern PHP frameworks and libraries are built entirely with classes and objects. In the previous lesson on arrays and objects, you got a brief introduction. Now it is time to go deeper into the full power of PHP's object-oriented features.
What You'll Learn
- How to define classes with typed properties
- Constructors and constructor promotion
- Visibility modifiers: public, private, and protected
- Inheritance and method overriding
- Abstract classes and interfaces
- Static methods and properties
- Readonly properties in PHP 8
Classes and Objects
A class is a blueprint for creating objects. It defines properties (data) and methods (behavior):
<?php
class Product {
public string $name;
public float $price;
public int $quantity;
public function __construct(string $name, float $price, int $quantity = 0) {
$this->name = $name;
$this->price = $price;
$this->quantity = $quantity;
}
public function totalValue(): float {
return $this->price * $this->quantity;
}
public function describe(): string {
return "{$this->name}: \${$this->price} x {$this->quantity} = \${$this->totalValue()}";
}
}
$laptop = new Product("Laptop", 999.99, 5);
$mouse = new Product("Mouse", 29.99, 50);
echo $laptop->describe() . "\n";
echo $mouse->describe() . "\n";
echo "Laptop stock value: $" . $laptop->totalValue() . "\n";
The __construct method is a special method called automatically when you create a new object with new. Inside the class, $this refers to the current object instance.
Constructor Promotion
PHP 8 introduced constructor promotion, which lets you declare and assign properties in a single line. This eliminates a lot of boilerplate code:
<?php
class User {
// Constructor promotion: declare and assign in one step
public function __construct(
public readonly string $name,
public string $email,
private string $password,
public string $role = 'user'
) {}
public function getInfo(): string {
return "{$this->name} ({$this->email}) - Role: {$this->role}";
}
public function verifyPassword(string $attempt): bool {
return $this->password === $attempt;
}
}
$admin = new User("Alice", "alice@example.com", "secret123", "admin");
$user = new User("Bob", "bob@example.com", "pass456");
echo $admin->getInfo() . "\n";
echo $user->getInfo() . "\n";
// Readonly properties cannot be changed after construction
// $admin->name = "Eve"; // This would throw an error
echo "Password check: " . ($admin->verifyPassword("secret123") ? "Valid" : "Invalid") . "\n";
echo "Password check: " . ($admin->verifyPassword("wrong") ? "Valid" : "Invalid") . "\n";
With constructor promotion, the public, private, or protected keyword before a constructor parameter automatically creates and assigns a class property. The readonly modifier (PHP 8.1) makes the property immutable after construction.
Visibility Modifiers
Visibility controls who can access properties and methods. PHP has three levels:
- public -- accessible from anywhere
- protected -- accessible within the class and its children
- private -- accessible only within the class itself
<?php
class BankAccount {
private float $balance;
private array $transactions = [];
public function __construct(
public readonly string $accountNumber,
public readonly string $owner,
float $initialBalance = 0.0
) {
$this->balance = $initialBalance;
$this->logTransaction("Account opened", $initialBalance);
}
public function deposit(float $amount): void {
if ($amount <= 0) {
throw new InvalidArgumentException("Deposit must be positive");
}
$this->balance += $amount;
$this->logTransaction("Deposit", $amount);
}
public function withdraw(float $amount): void {
if ($amount <= 0) {
throw new InvalidArgumentException("Withdrawal must be positive");
}
if ($amount > $this->balance) {
throw new RuntimeException("Insufficient funds");
}
$this->balance -= $amount;
$this->logTransaction("Withdrawal", -$amount);
}
public function getBalance(): float {
return $this->balance;
}
private function logTransaction(string $type, float $amount): void {
$this->transactions[] = [
'type' => $type,
'amount' => $amount,
'date' => date('Y-m-d H:i:s'),
];
}
public function getTransactionCount(): int {
return count($this->transactions);
}
}
$account = new BankAccount("ACC-001", "Alice", 1000.00);
$account->deposit(500.00);
$account->withdraw(200.00);
echo "Owner: {$account->owner}\n";
echo "Balance: $" . $account->getBalance() . "\n";
echo "Transactions: " . $account->getTransactionCount() . "\n";
// $account->balance; // Error: private property
// $account->logTransaction(...); // Error: private method
Notice that $balance and $transactions are private, so outside code cannot directly change the balance. The class controls access through public methods like deposit() and withdraw(), ensuring business rules are enforced.
Inheritance
Inheritance lets a child class extend a parent class, reusing and overriding behavior:
<?php
class Shape {
public function __construct(
protected string $color = "red"
) {}
public function getColor(): string {
return $this->color;
}
public function area(): float {
return 0.0;
}
public function describe(): string {
return "A {$this->color} shape with area " . round($this->area(), 2);
}
}
class Circle extends Shape {
public function __construct(
private float $radius,
string $color = "red"
) {
parent::__construct($color);
}
public function area(): float {
return M_PI * $this->radius ** 2;
}
public function describe(): string {
return "A {$this->color} circle (r={$this->radius}) with area " . round($this->area(), 2);
}
}
class Rectangle extends Shape {
public function __construct(
private float $width,
private float $height,
string $color = "blue"
) {
parent::__construct($color);
}
public function area(): float {
return $this->width * $this->height;
}
public function describe(): string {
return "A {$this->color} rectangle ({$this->width}x{$this->height}) with area " . round($this->area(), 2);
}
}
$shapes = [
new Circle(5.0, "green"),
new Rectangle(4.0, 6.0),
new Circle(3.0),
];
foreach ($shapes as $shape) {
echo $shape->describe() . "\n";
}
The extends keyword creates a child class. The child inherits all public and protected members of the parent. Use parent::__construct() to call the parent's constructor. Overriding a method replaces the parent's implementation in the child.
Abstract Classes and Interfaces
Abstract classes define methods that child classes must implement. Interfaces define a contract that implementing classes must fulfill:
<?php
interface Printable {
public function toString(): string;
}
abstract class Animal implements Printable {
public function __construct(
protected string $name,
protected int $age
) {}
// Abstract method: children MUST implement this
abstract public function speak(): string;
// Concrete method: children inherit this
public function describe(): string {
return "{$this->name} is {$this->age} years old";
}
public function toString(): string {
return "{$this->name} says: {$this->speak()}";
}
}
class Dog extends Animal {
public function speak(): string {
return "Woof!";
}
public function fetch(string $item): string {
return "{$this->name} fetches the {$item}!";
}
}
class Cat extends Animal {
public function speak(): string {
return "Meow!";
}
public function purr(): string {
return "{$this->name} purrs contentedly.";
}
}
$dog = new Dog("Rex", 3);
$cat = new Cat("Whiskers", 5);
echo $dog->describe() . "\n";
echo $dog->toString() . "\n";
echo $dog->fetch("ball") . "\n";
echo $cat->describe() . "\n";
echo $cat->toString() . "\n";
echo $cat->purr() . "\n";
// You cannot instantiate an abstract class:
// $animal = new Animal("Test", 1); // Error!
Use abstract classes when you want to share common code between related classes. Use interfaces when you want unrelated classes to share a common contract.
Static Methods and Properties
Static members belong to the class itself, not to individual instances:
<?php
class Counter {
private static int $count = 0;
public static function increment(): void {
self::$count++;
}
public static function getCount(): int {
return self::$count;
}
public static function reset(): void {
self::$count = 0;
}
}
Counter::increment();
Counter::increment();
Counter::increment();
echo "Count: " . Counter::getCount() . "\n";
Counter::reset();
echo "After reset: " . Counter::getCount() . "\n";
// Practical example: a factory method
class DatabaseConnection {
private static ?DatabaseConnection $instance = null;
private function __construct(
private string $host,
private string $database
) {}
public static function create(string $host, string $database): self {
if (self::$instance === null) {
self::$instance = new self($host, $database);
}
return self::$instance;
}
public function getInfo(): string {
return "Connected to {$this->database} on {$this->host}";
}
}
$db = DatabaseConnection::create("localhost", "myapp");
echo $db->getInfo() . "\n";
Static methods are called with ClassName::method() instead of $object->method(). Use self:: to reference static members within the class. The Singleton pattern shown above is a classic use of static properties.
Key Takeaways
- Classes define blueprints for objects with properties and methods
- Constructor promotion (PHP 8) reduces boilerplate code significantly
- Use visibility modifiers to enforce encapsulation:
public,protected,private - Inheritance with
extendsenables code reuse and polymorphism - Abstract classes provide partial implementations; interfaces define contracts
- Static members belong to the class, not to individual instances
- The
readonlymodifier (PHP 8.1) creates immutable properties
Next Steps
You now understand classes, constructors, visibility modifiers, inheritance, abstract classes, interfaces, and static members -- the full OOP toolkit that powers modern PHP.
Well-structured classes are great, but what happens when something goes wrong? A database connection fails, a file is missing, user input is invalid. Without a plan for handling failures, even beautifully designed classes can crash your application. Error handling teaches you to use try/catch/finally blocks, throw and create custom exceptions, and build robust code that fails gracefully instead of catastrophically. These patterns are essential before you start working with databases, files, and external services.
Continue to Error Handling -->
Pro Tip: Follow the Single Responsibility Principle -- each class should have one reason to change. If a class is doing too many things (handling HTTP requests, querying the database, and formatting output), split it into smaller focused classes. This makes your code easier to test, understand, and maintain. Start with simple classes and add complexity only when needed.