PHP OOP Introduction

Object-oriented programming (OOP) is a way of structuring code around objects — self-contained bundles of data and the behavior that operates on that data — rather than a long list of loose functions and variables. PHP has supported OOP since PHP 4 and modernized it heavily through PHP 5, 7, and 8. Once you understand classes and objects, you can model real-world things (users, orders, shapes, bank accounts) as code that is easier to organize, reuse, and maintain than purely procedural scripts.

Overview / How It Works

In procedural PHP, you write functions and pass data between them: calculateArea($width, $height). In OOP, you define a class — a blueprint — that describes what data (properties) and behavior (methods) something has. You then create objects (also called instances) from that blueprint using the new keyword. Each object has its own copy of the properties defined by the class, even though all objects of that class share the same method code.

Internally, when you write new Car(...), the Zend Engine (PHP’s runtime) allocates a new object handle and a property table for that instance, then calls the class’s constructor (__construct) to initialize it. Objects in PHP are always handled by reference-like handles: when you assign an object to another variable or pass it to a function, both variables point to the same object in memory — unlike arrays or scalars, which PHP copies by value. This matters because modifying an object through one variable is visible through every other variable referencing it, unless you explicitly clone it.

The four foundational pillars of OOP are:

  • Encapsulation — bundling data and the methods that act on it, and controlling access with visibility keywords (public, protected, private).
  • Inheritance — letting one class (a child) reuse and extend the behavior of another class (a parent) with extends.
  • Polymorphism — different classes responding to the same method call in their own way, so calling code doesn’t need to know the exact class it’s working with.
  • Abstraction — hiding implementation detail behind a simpler interface, often via abstract classes or interfaces.

This lesson focuses on the fundamentals — classes, objects, properties, methods, constructors, encapsulation, and basic inheritance — which is everything you need to start writing real object-oriented PHP.

Syntax

class ClassName
{
    // Properties (state)
    public string $propertyName;
    private int $anotherProperty = 0;

    // Constructor: runs automatically when the object is created
    public function __construct(string $propertyName)
    {
        $this->propertyName = $propertyName;
    }

    // Methods (behavior)
    public function doSomething(): string
    {
        return $this->propertyName;
    }
}

$object = new ClassName("value");
echo $object->doSomething();
Piece Meaning
class ClassName { ... } Defines the blueprint. By convention, class names use PascalCase.
public, protected, private Visibility keywords controlling who can access a property or method.
__construct() A special “magic” method PHP calls automatically when you write new ClassName(...).
$this Inside a method, refers to the current object instance.
new ClassName(...) Creates (instantiates) a new object from the class.
-> The object operator, used to access properties and methods on an instance, e.g. $object->doSomething().

Examples

Example 1: A basic class with a constructor

<?php
class Car {
    public string $make;
    public string $model;
    public int $year;

    public function __construct(string $make, string $model, int $year) {
        $this->make = $make;
        $this->model = $model;
        $this->year = $year;
    }

    public function describe(): string {
        return "{$this->year} {$this->make} {$this->model}";
    }
}

$car1 = new Car("Toyota", "Corolla", 2023);
$car2 = new Car("Honda", "Civic", 2022);

echo $car1->describe() . "\n";
echo $car2->describe() . "\n";

Output:

2023 Toyota Corolla
2022 Honda Civic

Here, Car is the blueprint and $car1 / $car2 are two independent objects. Each stores its own make, model, and year, even though both were built from the exact same class definition and method code.

Example 2: Encapsulation with constructor promotion

<?php
class BankAccount {
    private float $balance;

    public function __construct(
        private readonly string $owner,
        float $openingBalance = 0.0
    ) {
        $this->balance = $openingBalance;
    }

    public function deposit(float $amount): void {
        if ($amount <= 0) {
            throw new InvalidArgumentException("Deposit must be positive.");
        }
        $this->balance += $amount;
    }

    public function withdraw(float $amount): bool {
        if ($amount > $this->balance) {
            return false;
        }
        $this->balance -= $amount;
        return true;
    }

    public function getBalance(): float {
        return $this->balance;
    }
}

$account = new BankAccount("Alice", 100.0);
$account->deposit(50.0);
$account->withdraw(30.0);

echo $account->getBalance() . "\n";
echo $account->withdraw(1000.0) ? "Approved\n" : "Denied\n";

Output:

120
Denied

The balance property is private, so no code outside BankAccount can modify it directly — it can only change through deposit() and withdraw(), which enforce rules (no negative deposits, no overdrafts). This is encapsulation: the class controls how its own state changes. Notice the constructor also uses constructor promotion — declaring private readonly string $owner directly in the parameter list automatically creates and assigns that property, a shorthand available since PHP 8.0 (with readonly since PHP 8.1).

Example 3: Inheritance and polymorphism

<?php
abstract class Shape {
    abstract public function area(): float;

    public function describe(): string {
        return static::class . " has area " . round($this->area(), 2);
    }
}

class Circle extends Shape {
    public function __construct(private float $radius) {}

    public function area(): float {
        return M_PI * $this->radius ** 2;
    }
}

class Rectangle extends Shape {
    public function __construct(private float $width, private float $height) {}

    public function area(): float {
        return $this->width * $this->height;
    }
}

$shapes = [new Circle(3), new Rectangle(4, 5)];

foreach ($shapes as $shape) {
    echo $shape->describe() . "\n";
}

Output:

Circle has area 28.27
Rectangle has area 20

Shape is an abstract class: it can’t be instantiated directly (new Shape() would error) and it forces every subclass to implement area(). Both Circle and Rectangle extend Shape and inherit its describe() method for free, but each supplies its own area() logic. Calling describe() on either object runs the same shared code, yet produces different results because it internally calls $this->area(), which resolves to whichever subclass is actually being used — this is polymorphism.

How It Works Step by Step

  • PHP parses the class keyword and registers the class definition (its properties, methods, and parent) in memory — this happens once, when the script loads, not each time you create an object.
  • When you call new ClassName(...), PHP allocates a fresh object and immediately invokes __construct() on it, passing along whatever arguments you gave new.
  • Inside any method, $this is bound to the specific object the method was called on, so $this->balance in BankAccount::deposit() always refers to the balance of the exact object you called deposit() on.
  • When a method is called on an object whose class extends another (like Circle extends Shape), PHP looks for the method on the object’s own class first, then walks up the parent chain until it finds a match — this lookup is what lets a child override a parent’s method.
  • Because objects are handled by reference internally, assigning $b = $a; for two object variables makes both point to the same underlying object — modifying one through $b->property = ... is visible through $a too.

Common Mistakes

Mistake 1: Forgetting $this-> inside a method

class Counter {
    private int $count = 0;

    public function increment() {
        $count++; // Wrong: creates/increments a local variable, not the property
    }

    public function getCount(): int {
        return $this->count;
    }
}

Without $this->, $count is just an ordinary local variable inside the method that disappears when the method ends — the object’s count property never changes, so getCount() always returns 0. Properties must always be accessed through $this->propertyName from inside the class.

<?php
class Counter {
    private int $count = 0;

    public function increment(): void {
        $this->count++;
    }

    public function getCount(): int {
        return $this->count;
    }
}

$counter = new Counter();
$counter->increment();
$counter->increment();
echo $counter->getCount() . "\n";

Output:

2

Mistake 2: Reaching into a private property from outside the class

class Wallet {
    private float $balance = 0.0;
}

$wallet = new Wallet();
echo $wallet->balance; // Fatal error: Cannot access private property Wallet::$balance

Marking a property private means only code inside that same class can read or write it directly. Trying to access it from outside — even just to read it — causes a fatal error. The fix is to expose a public method (a “getter”) that returns the value in a controlled way.

<?php
class Wallet {
    private float $balance = 0.0;

    public function getBalance(): float {
        return $this->balance;
    }
}

$wallet = new Wallet();
echo $wallet->getBalance() . "\n";

Output:

0

Best Practices

  • Default to private properties and expose access through public methods — only widen visibility to protected or public when you have a real reason to.
  • Use constructor promotion (public function __construct(private string $name) {}) to cut boilerplate for simple properties, but keep it readable — don’t cram a dozen unrelated parameters into one constructor.
  • Always type your properties, parameters, and return values (string, int, float, ?Foo, etc.) — it catches bugs earlier and documents intent.
  • Use readonly properties (PHP 8.1+) for values that should never change after construction, like an ID or a creation timestamp.
  • Favor composition (an object holding another object) over deep inheritance chains — inheritance should model a genuine “is-a” relationship, not just a convenient place to reuse code.
  • Name classes as singular nouns in PascalCase (Invoice, not invoices or ProcessInvoice), and methods as verbs in camelCase (calculateTotal()).

Practice Exercises

  • Create a Book class with private properties title, author, and price, a constructor to set them, and a public method summary(): string that returns a formatted string like "The Hobbit by J.R.R. Tolkien ($12.99)".
  • Write an abstract class Employee with an abstract method calculatePay(): float, then create two subclasses, SalariedEmployee and HourlyEmployee, each implementing calculatePay() differently. Loop over an array containing one of each and print each one’s pay.
  • Build a Stack class with a private array property and public methods push($item): void, pop(): mixed, and isEmpty(): bool. Push three values on, pop one off, and print what remains.

Summary

  • A class is a blueprint; an object is a specific instance created from it with new.
  • Properties hold an object’s state; methods define its behavior; $this refers to the current instance inside a method.
  • __construct() runs automatically when an object is created, and constructor promotion can declare and assign properties in one step.
  • Visibility keywords (public, protected, private) enforce encapsulation by controlling what outside code can access.
  • extends lets a child class reuse and override a parent’s behavior (inheritance); calling the same method on different subclasses can yield different results (polymorphism).
  • Objects are handled by reference internally — copying a variable copies the reference, not the object’s data.