PHP Classes Objects

A class is a blueprint that describes what data (properties) and behavior (methods) something should have, and an object is a concrete instance built from that blueprint. Classes and objects are the foundation of object-oriented programming (OOP) in PHP, letting you model real-world things — a Car, a BankAccount, a User — as self-contained units instead of scattering related variables and functions everywhere. Once you understand classes and objects, every other OOP feature (inheritance, interfaces, traits) builds directly on top of them.

Overview: How Classes and Objects Work

Think of a class as a cookie cutter and objects as the cookies. The class Car defines that every car has a make, a model, and a year, and that every car can describe() itself. The class itself never drives anywhere — you must create an instance of it with the new keyword before you have something you can actually use.

A class bundles two kinds of members:

  • Properties — variables attached to the class that hold an object’s state (e.g. $make, $balance).
  • Methods — functions attached to the class that define an object’s behavior (e.g. describe(), deposit()).

Each property and method has a visibility that controls where it can be accessed from:

  • public — accessible from anywhere, including outside the class.
  • protected — accessible from inside the class and any class that extends it.
  • private — accessible only from inside the exact class that declares it.

Inside a method, the special variable $this refers to the specific object the method was called on. It’s how a method reaches the object’s own properties ($this->balance) rather than some unrelated variable.

Internally, PHP’s Zend Engine represents every object as a zend_object structure stored in an internal object table, and PHP variables that “hold” an object actually hold a lightweight handle (like a reference) pointing at that structure. This detail matters in practice — it’s why assigning one object variable to another does not create a second, independent object (more on that in Common Mistakes below). Each object also keeps a reference count so PHP’s garbage collector knows when it’s safe to free the memory once nothing points to the object anymore.

Syntax

<?php
class ClassName
{
    // Property declarations (with optional type and default)
    public string $property = 'default';
    private int $count = 0;

    // The constructor runs automatically when the object is created
    public function __construct(string $property)
    {
        $this->property = $property;
    }

    // A regular method
    public function methodName(): string
    {
        return $this->property;
    }
}

$object = new ClassName('example');   // create an instance
echo $object->methodName();           // call a method
echo $object->property;               // read a public property
Part Meaning
class ClassName { ... } Declares a new class named ClassName. By convention class names use PascalCase.
public string $property A typed property with a visibility modifier and, optionally, a default value.
__construct() A magic method PHP calls automatically when you write new ClassName(...).
$this Inside a method, refers to the current object instance.
new ClassName(...) Creates (instantiates) an object from the class, passing arguments to the constructor.
-> The object operator, used to access a property or call a method on an instance.

Examples

Example 1: A Simple Class

<?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}";
    }
}

$car = new Car('Toyota', 'Corolla', 2023);
echo $car->describe();
echo PHP_EOL;
echo "Make: {$car->make}";

Output:

2023 Toyota Corolla
Make: Toyota

The constructor runs the moment new Car(...) executes, storing each argument onto the new object via $this. Afterward, $car holds a fully-formed Car object whose public properties can be read directly with ->, and whose describe() method can compute a value using that same object’s data.

Example 2: Encapsulation with a Bank Account

<?php

class BankAccount
{
    private float $balance = 0.0;

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

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

    public function withdraw(float $amount): void
    {
        if ($amount > $this->balance) {
            throw new RuntimeException('Insufficient funds.');
        }
        $this->balance -= $amount;
    }

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

$account = new BankAccount('ACC-1001', 'Priya Shah', 500.0);
$account->deposit(250.0);
$account->withdraw(100.0);

echo "Owner account: {$account->accountNumber}\n";
echo "Balance: " . number_format($account->getBalance(), 2);

Output:

Owner account: ACC-1001
Balance: 650.00

This example uses constructor property promotion (PHP 8+): declaring visibility directly on constructor parameters like public readonly string $accountNumber automatically creates and assigns that property, saving boilerplate. The $balance property is private, so outside code cannot set it directly — it can only change through deposit() and withdraw(), which enforce the rules (no negative deposits, no overdrafts). This controlled access is called encapsulation, and it’s one of the biggest practical benefits of OOP.

Example 3: Inheritance and Polymorphism

<?php

class Employee
{
    public function __construct(
        protected string $name,
        protected float $baseSalary
    ) {}

    public function calculatePay(): float
    {
        return $this->baseSalary;
    }

    public function summary(): string
    {
        return sprintf('%s earns %.2f', $this->name, $this->calculatePay());
    }
}

class Manager extends Employee
{
    public function __construct(
        string $name,
        float $baseSalary,
        private float $bonus
    ) {
        parent::__construct($name, $baseSalary);
    }

    public function calculatePay(): float
    {
        return parent::calculatePay() + $this->bonus;
    }
}

$staff = [
    new Employee('Alex Chen', 4000.0),
    new Manager('Jordan Lee', 6000.0, 1500.0),
];

foreach ($staff as $person) {
    echo $person->summary() . PHP_EOL;
}

Output:

Alex Chen earns 4000.00
Jordan Lee earns 7500.00

Manager extends Employee, inheriting its protected properties and its summary() method. Manager overrides calculatePay() to add a bonus on top of the base salary calculated by parent::calculatePay(). Because summary() calls $this->calculatePay() rather than a hard-coded formula, each object in the $staff array automatically uses its own version of calculatePay() — this is polymorphism: the same method call produces different behavior depending on the actual object.

Under the Hood: How PHP Creates Objects

When PHP executes new ClassName(...), several things happen in sequence:

  • PHP looks up the class definition (compiling the file that declares it, if it hasn’t already).
  • The Zend Engine allocates a new zend_object structure and, using the class’s property table, initializes each property to its declared default (or leaves typed properties without a default “uninitialized” until first assigned).
  • PHP calls __construct() on the new object, if one is defined, passing along whatever arguments you supplied.
  • The expression new ClassName(...) evaluates to a handle referencing that object, which gets stored in your variable.

Because PHP variables store a handle rather than the object’s raw data, assigning $b = $a when $a is an object copies the handle, not the object — both variables end up pointing at the exact same object in memory. Each object also carries a reference count; PHP’s garbage collector frees the object’s memory once no variable references it anymore, which is why you rarely need to manage object lifetimes manually.

Common Mistakes

Mistake 1: Assuming Objects Are Copied on Assignment

<?php

class Counter
{
    public int $value = 0;
}

$a = new Counter();
$b = $a; // Mistake: this does NOT create a separate copy of the object
$b->value = 10;

echo $a->value;

Output:

10

Many beginners expect $a->value to still be 0, since $b = $a “looks like” a copy the way it would for a plain number or string. But objects are assigned by handle, so $a and $b both refer to the same underlying Counter object — changing one changes the other. If you genuinely need an independent copy, use the clone keyword:

<?php

$a = new Counter();
$b = clone $a;
$b->value = 10;

echo $a->value; // 0
echo $b->value; // 10

Mistake 2: Forgetting to Use $this Inside Methods

<?php

class Product
{
    public string $name = '';

    public function __construct(string $name)
    {
        $name = $name; // Mistake: reassigns the local parameter, never touches the property
    }
}

$product = new Product('Keyboard');
echo "Name: '{$product->name}'";

Output:

Name: ''

Inside the constructor, $name refers to the parameter, a completely separate variable from the object’s $name property. Writing $name = $name; just reassigns the parameter to itself and silently does nothing useful — the property keeps its default value. You must explicitly write to $this->name to store the value on the object:

<?php

class Product
{
    public string $name = '';

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

$product = new Product('Keyboard');
echo "Name: '{$product->name}'";

Best Practices

  • Prefer private or protected properties with public methods over exposing raw public properties, so you can validate changes and change internal representation later without breaking callers.
  • Always declare property types (string, int, ?User, etc.) — they catch bugs early and document what a class expects.
  • Use constructor property promotion for simple data-holding classes to cut boilerplate, but switch to explicit assignment in __construct() once you need validation logic.
  • Favor composition (“has-a”) over inheritance (“is-a”) when a relationship isn’t a strict specialization — deep inheritance chains get hard to reason about.
  • Use readonly properties (PHP 8.1+) for values that should never change after construction, such as IDs or timestamps.
  • Name classes as singular nouns in PascalCase (Invoice, not invoices or ProcessInvoice).
  • Keep methods focused on one responsibility — if a method needs a long comment to explain what it does, it likely should be split up.

Practice Exercises

  • Create a Rectangle class with width and height properties, a constructor, and methods area() and perimeter(). Instantiate two rectangles and print both calculations for each.
  • Write a Person class with a private $age property and a public method haveBirthday() that increments it by one, plus a getAge() method to read it. Confirm that $age cannot be modified directly from outside the class.
  • Extend the Employee/Manager example from this lesson by adding an Intern class that extends Employee and overrides calculatePay() to always return a fixed stipend, regardless of baseSalary. Loop over an array containing one of each class and print each summary().

Summary

  • A class is a blueprint; an object is an instance of that blueprint created with new.
  • Properties hold an object’s state; methods define its behavior; $this refers to the current instance inside a method.
  • Visibility keywords (public, protected, private) control encapsulation — hiding internal state behind controlled methods.
  • __construct() runs automatically on object creation, and constructor property promotion can shorten it considerably.
  • Inheritance (extends) lets a subclass reuse and override a parent class’s members, enabling polymorphism.
  • PHP objects are assigned by handle, not by value — use clone when you need an independent copy.
  • Always write to $this->property, not a bare local variable, when you mean to change the object’s state.