VS Vikesh Singh

Courses PHP & Laravel Lesson 4

PHP & LARAVEL · LESSON 4 OF 18

Object-Oriented PHP: Classes, Objects, and Why They Help

· 5 min read

Everything so far has been procedural: variables, functions you call directly, arrays you loop over by hand.

Everything so far has been procedural: variables, functions you call directly, arrays you loop over by hand. That works fine for small scripts, but once an application has dozens of files and the data you work with has real shape and real behavior attached to it, procedural code starts falling apart under its own weight. Object-oriented programming (OOP) is PHP's (and Laravel's) answer to that problem, and this lesson is the last piece of plain-PHP foundation before we move into Laravel itself in lesson 6.

Classes And Objects

A class is a blueprint; an object is a specific thing built from that blueprint. If User is a class, $tonyStark might be one specific object (an "instance") of that class, with its own name, email, and age, distinct from every other User instance:

class User
{
    public string $name;
    public string $email;
    private int $age;

    public function __construct(string $name, string $email, int $age)
    {
        $this->name = $name;
        $this->email = $email;
        $this->age = $age;
    }

    public function isAdult(): bool
    {
        return $this->age >= 18;
    }
}

$tonyStark = new User("Tony Stark", "tony@starkindustries.com", 48);
echo $tonyStark->name;        // Tony Stark
echo $tonyStark->isAdult();   // 1 (true)

$this inside a class method refers to "the specific object this method was called on," which is the whole point of OOP: the same isAdult() code runs for every User object, but $this->age refers to that particular object's own age each time. The __construct method is special; PHP calls it automatically whenever you use new ClassName(...), which is exactly where you put setup logic that every object of this class needs.

Notice the three different visibility keywords: public, private. public properties and methods can be accessed from anywhere, including outside the class entirely ($tonyStark->name works because name is public). private properties and methods can only be accessed from inside the class itself; trying $tonyStark->age from outside would throw an error, because age was declared private. This is not bureaucracy for its own sake. It is how you protect an object's internal rules. If age were public, nothing would stop other code from setting it to -5 or "banana". Keeping it private and only exposing it through a controlled method (like isAdult(), or a dedicated getter) means the class itself decides what counts as valid, in one place, instead of trusting every piece of code anywhere in the application to behave correctly.

Constructor Property Promotion

PHP 8 lets you skip the repetitive assignment lines in the constructor entirely using constructor property promotion, which you will see constantly in real Laravel code (it is the same pattern used in the project's own CLAUDE.md conventions, for what it's worth):

class Trade
{
    public function __construct(
        public string $symbol,
        public float $entryPrice,
        public float $exitPrice,
        public int $quantity,
    ) {}

    public function profit(): float
    {
        return ($this->exitPrice - $this->entryPrice) * $this->quantity;
    }
}

$trade = new Trade("NIFTY", 19500.0, 19650.0, 50);
echo $trade->profit(); // 7500

Declaring public string $symbol directly in the constructor's parameter list does two things at once: it declares the property on the class and assigns it from the matching constructor argument, with no $this->symbol = $symbol; line needed. This is not just shorter to type; it removes an entire category of bug where someone adds a constructor parameter and forgets to assign it to a property, leaving a property permanently null.

Inheritance Between Classes

Inheritance lets one class build on another, reusing its behavior and adding or overriding pieces of it. This is where the "why does this help" question really gets answered:

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

    public function describe(): string
    {
        return "This shape has an area of " . $this->area();
    }
}

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

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

$circle = new Circle(5.0);
echo $circle->describe(); // This shape has an area of 78.53975

abstract class Shape cannot be instantiated directly with new Shape(); it exists purely to be extended. It declares area() as an abstract method, meaning every class that extends Shape is forced, by the language itself, to provide its own implementation of area(), or PHP refuses to run. Meanwhile describe() is written once, on Shape, and every subclass gets it for free, automatically using whichever area() that specific subclass provides. This is the core value of OOP in a sentence: shared behavior written once, specific behavior supplied per class, with the language enforcing the contract between them.

Interfaces And Contracts

Interfaces take this one step further by describing only the contract, with no shared implementation at all: "any class that implements Comparable must have a compareTo() method," full stop, with each implementing class free to define it however it needs. You will meet interfaces constantly in Laravel itself; many of the framework's own classes are swappable specifically because code depends on an interface rather than a specific class.

A real mistake worth naming directly: reaching for inheritance (extends) when composition (one class simply holding an instance of another as a property) would be simpler and more flexible. Deep inheritance chains, four or five classes extending each other, get brittle fast, because changing a method three levels up can break something two levels down in a way that is hard to trace. If you catch yourself writing class SpecialTradeWithDiscount extends TradeWithFees extends Trade, stop and ask whether a Trade that simply has a $discount property and a $fee property would do the same job with far less structural risk.

Try it yourself: write an abstract class PaymentMethod with an abstract charge(float $amount): bool method and a concrete describe() method that calls it, then write two subclasses, CreditCard and UpiPayment, each implementing charge() differently, and call describe() on an instance of each.