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.
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.
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.
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.
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 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.
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)
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.
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:
==— returnstrueif both objects are of the same class and all their properties are equal in value===— returnstrueonly if both variables point to the exact same object in memory
$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.
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.
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?