PHP::OOP_Mastery
Overview / Session 02 / Session 03
Session 03 of 10  ·  2 hrs + 15 min review

Constructor, $this
& Object Instantiation

⏱ 2-hour session 📋 15-min review ⚠ Requires Sessions 01–02 🗂 4 topics

1 — The __construct() Method

The constructor is a special method that PHP calls automatically the moment you write new ClassName(...). Its one job: put the new object into a valid, usable state before any other code touches it.

Think of it as the object's birth certificate — it records everything the object needs to know about itself from day one. If an object can't be born without certain data, that data goes in the constructor. No more "remember to call init() after creating the object" bugs.

PHP — Constructor basics
class Invoice
{
    private string    $number;
    private Customer  $customer;
    private float     $total;
    private DateTimeImmutable $createdAt;

    public function __construct(
        string   $number,
        Customer $customer,
        float    $total
    ) {
        if ($total < 0) {
            throw new InvalidArgumentException('Invoice total cannot be negative');
        }

        $this->number    = $number;
        $this->customer  = $customer;
        $this->total     = $total;
        $this->createdAt = new DateTimeImmutable();  // set internally
    }

    public function getTotal(): float     { return $this->total; }
    public function getNumber(): string   { return $this->number; }
    public function getCreatedAt(): DateTimeImmutable { return $this->createdAt; }
}

// Every Invoice is guaranteed to have a number, customer, total, and timestamp
$inv = new Invoice('INV-001', $customer, 299.99);

// This throws — negative total rejected at birth
$bad = new Invoice('INV-002', $customer, -50.0); // InvalidArgumentException

Constructor with default parameter values

Parameters with defaults are optional. PHP evaluates default values at call time, so you can't use a runtime expression like new DateTime() as a default — but you can use null and handle it in the body.

PHP — Optional constructor parameters
class Connection
{
    public function __construct(
        private string $host    = '127.0.0.1',
        private int    $port    = 3306,
        private string $charset = 'utf8mb4'
    ) {}
}

$default = new Connection();                     // all defaults
$custom  = new Connection('db.example.com', 5432); // custom host+port

No constructor at all

If you don't declare __construct(), PHP provides an invisible no-argument constructor. Calling new ClassName() still works — properties just won't be initialized until you assign them. As covered in Session 2, reading an uninitialized typed property throws an error, so for classes with typed properties you should almost always write an explicit constructor.

2 — $this: The Object Talking to Itself

Inside any non-static method, $this is a reference to the current object — the specific instance the method was called on. It's how an object accesses its own properties and calls its own methods.

PHP — $this in action
class Circle
{
    public function __construct(private float $radius) {}

    public function area(): float
    {
        // $this->radius refers to THIS circle's radius
        return M_PI * $this->radius ** 2;
    }

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

    public function isLargerThan(Circle $other): bool
    {
        // $this is the object the method was called on
        // $other is a different Circle passed as argument
        return $this->radius > $other->radius;
    }
}

$big   = new Circle(10.0);
$small = new Circle(3.0);

echo $big->area();                    // 314.16…
echo $small->area();                  // 28.27… — different object, different result
var_dump($big->isLargerThan($small)); // bool(true)

$this inside vs outside a method

$this only exists inside an instance method. It is not available in static methods (there's no object yet), and trying to use it outside a class context causes a fatal error. If PHP says "Using $this when not in object context", a static method is the usual culprit.

Passing $this to other objects

Because $this is just the current object, you can pass it as an argument to another method or object. This is the basis of the Observer and Strategy design patterns.

PHP — Passing $this to a collaborator
class Order
{
    private array $items = [];

    public function addItem(Product $p): void
    {
        $this->items[] = $p;
    }

    public function process(OrderProcessor $processor): void
    {
        $processor->handle($this);  // hand "myself" to the processor
    }

    public function getItems(): array { return $this->items; }
}

3 — Constructor Promotion (PHP 8.0+)

PHP 8.0 introduced constructor promotion — a syntax shortcut that collapses the "declare property + set in constructor" boilerplate into a single line. It's now the standard style in modern PHP codebases (Laravel, Symfony, all use it).

PHP — Before and after promotion
// ── PHP 7 style (verbose) ──────────────────────────────────
class Address
{
    private string $street;
    private string $city;
    private string $postcode;
    private string $country;

    public function __construct(
        string $street,
        string $city,
        string $postcode,
        string $country
    ) {
        $this->street   = $street;
        $this->city     = $city;
        $this->postcode = $postcode;
        $this->country  = $country;
    }
}

// ── PHP 8 style (promoted, identical behaviour) ───────────
class Address
{
    public function __construct(
        private string $street,
        private string $city,
        private string $postcode,
        private string $country
    ) {}   // properties declared AND assigned in one step
}

Adding the visibility modifier (private, protected, public) in front of a constructor parameter tells PHP: "declare a property with this name and visibility, and assign this parameter to it." PHP does the rest.

Mixing promoted and regular parameters

You can mix promoted and non-promoted parameters in the same constructor. Non-promoted parameters are just regular function arguments — they don't become properties.

PHP — Mixed promoted + non-promoted
class Discount
{
    public readonly float $amount;   // declared separately (for clarity)

    public function __construct(
        private string $code,       // promoted → becomes $this->code
        float $pct,                  // NOT promoted — just a parameter
        private bool   $active = true // promoted with default
    ) {
        // $pct is a parameter — we decide how to use it ourselves
        $this->amount = min(max($pct, 0.0), 100.0); // clamp 0–100
    }

    public function isActive(): bool  { return $this->active; }
    public function getCode(): string { return $this->code; }
}

$d = new Discount('SUMMER20', 20.0);
echo $d->amount;    // 20
echo $d->getCode(); // SUMMER20

Promoted + readonly (the cleanest immutable value object)

PHP — Immutable value object in 3 lines
class Money
{
    public function __construct(
        public readonly float  $amount,
        public readonly string $currency
    ) {}
}

$price = new Money(29.99, 'EUR');
echo "{$price->amount} {$price->currency}"; // 29.99 EUR
$price->amount = 0;  // ✗ Fatal: Cannot modify readonly

4 — Object Cloning, Identity & the Destructor

Deep vs shallow clone

PHP's clone keyword performs a shallow copy: scalar properties (string, int, float, bool) are copied by value, but object properties are still copied as handles — they still point to the same nested object. To get a true deep copy, implement the __clone() magic method.

PHP — Shallow vs deep clone
class UserProfile
{
    public function __construct(
        public string   $name,
        public Address  $address  // an object property
    ) {}

    // Called automatically when clone is used
    public function __clone(): void
    {
        // Clone the nested object too — now it's truly independent
        $this->address = clone $this->address;
    }
}

$alice = new UserProfile('Alice', new Address('1 Main St', 'London', 'E1 1AA', 'GB'));
$bob   = clone $alice;

$bob->name             = 'Bob';         // only $bob changes
$bob->address->street = '99 Park Ave'; // only $bob's address changes (deep clone)

echo $alice->name;             // Alice — unchanged
echo $alice->address->street; // 1 Main St — unchanged

Object identity: === vs ==

With objects, PHP's comparison operators behave differently from scalars:

  • == — returns true if both objects are of the same class and all their properties are equal in value
  • === — returns true only if both variables point to the exact same object in memory
PHP — Object comparison
$m1 = new Money(10.0, 'EUR');
$m2 = new Money(10.0, 'EUR');
$m3 = $m1;

var_dump($m1 == $m2);  // bool(true) — same class, same values
var_dump($m1 === $m2); // bool(false) — different instances
var_dump($m1 === $m3); // bool(true) — SAME instance

The destructor __destruct()

Just as __construct() runs at birth, __destruct() runs when an object is destroyed — when it goes out of scope or the script ends. This is the right place to release resources: close a file handle, flush a log buffer, or disconnect from a socket.

PHP — Destructor example
class FileLogger
{
    private $handle;

    public function __construct(string $path)
    {
        $this->handle = fopen($path, 'a');
        if ($this->handle === false) {
            throw new RuntimeException("Cannot open log: {$path}");
        }
    }

    public function log(string $message): void
    {
        fwrite($this->handle, date('[Y-m-d H:i:s] ') . $message . "\n");
    }

    public function __destruct(): void
    {
        if (is_resource($this->handle)) {
            fclose($this->handle);  // guaranteed cleanup
        }
    }
}

// File is automatically closed when $logger goes out of scope
{
    $logger = new FileLogger('/var/log/app.log');
    $logger->log('Application started');
}   // __destruct() called here — file handle closed

Putting it all together: a fully featured value object

Here is a pattern you'll use constantly in real projects. A value object holds data, validates it at construction, and is immutable. Operations that need a modified value return a new instance rather than mutating the existing one.

PHP — Immutable Email value object
final class Email
{
    private readonly string $value;

    public function __construct(string $email)
    {
        $normalized = strtolower(trim($email));

        if (! filter_var($normalized, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(
                "'{$email}' is not a valid email address"
            );
        }

        $this->value = $normalized;
    }

    public function toString(): string
    {
        return $this->value;
    }

    public function domain(): string
    {
        return substr($this->value, strpos($this->value, '@') + 1);
    }

    public function equals(Email $other): bool
    {
        return $this->value === $other->value;
    }
}

$email = new Email('  Alice@Example.COM  ');
echo $email->toString(); // alice@example.com (normalized)
echo $email->domain();   // example.com

new Email('not-an-email');  // ✗ InvalidArgumentException

Notice the final keyword on the class declaration. Marking a value object final prevents it from being extended, which could accidentally break its immutability guarantees. You'll learn more about inheritance and final in Session 04.

15-Minute Review — Session 03

Switch the sidebar timer to review mode. Answer without scrolling up.

Q1 — When is __construct() automatically called?

Q2 — What does constructor promotion do?

Q3 — clone $obj in PHP performs a:

Q4 — $obj1 === $obj2 returns true when:

Q5 — Where is $this not available?

✓ Key concepts checklist

Next: Inheritance & Method Overriding →