Lesson 11 • Expert
Object-Oriented PHP 🏗️
By the end of this lesson you'll be able to model real things as classes — bundling their data and behaviour into objects with constructors, visibility, static members, constants, and readonly properties — the foundation of every modern PHP application.
What You'll Learn in This Lesson
- Define a class with typed properties and methods, and build objects with new
- Use public, private, and protected to control who can touch each property
- Initialise objects with a __construct() constructor and the $this keyword
- Cut boilerplate with PHP 8 constructor property promotion
- Share data and helpers across a class with static properties, static methods, and self::/static::
- Lock down values with class constants and readonly properties
$this, function, or typed parameters look unfamiliar, revisit the Introduction to PHP and the functions lesson first.php file.php. The Output panel under each example shows exactly what to expect. (OOP needs PHP 8.1+ for readonly properties.)1️⃣ Classes, Properties & Objects
Up to now your data has lived in loose variables. Object-oriented programming (OOP) bundles related data and the code that works on it into one unit. A class is the blueprint: it lists the properties (the data each object holds) and the methods (the things it can do). An object is a single thing built from that blueprint with the new keyword. You reach inside an object with the arrow operator ->. Typing each property (string $name) tells PHP — and your editor — exactly what kind of value belongs there.
<?php
// A class is a BLUEPRINT. It describes what every object made from it
// will hold (properties) and what it can do (methods). Nothing happens
// until you build an actual object from it with 'new'.
class Dog
{
// Typed properties with visibility:
// public -> readable/writable from anywhere
// private -> only inside THIS class
public string $name; // the dog's name
public int $age; // the dog's age in years
}
// 'new' builds an OBJECT (an instance) from the blueprint.
$rex = new Dog(); // $rex is now a Dog object
// Reach inside the object with -> to set its properties.
$rex->name = "Rex";
$rex->age = 3;
echo "Name: {$rex->name}\n"; // {$rex->name} pulls the value back out
echo "Age: {$rex->age}\n";
// Each object is independent — a second Dog has its own data.
$buddy = new Dog();
$buddy->name = "Buddy";
$buddy->age = 7;
echo "Name: {$buddy->name}\n";
echo "Age: {$buddy->age}\n";Name: Rex
Age: 3
Name: Buddy
Age: 7Notice $rex and $buddy are completely separate — each carries its own name and age. That independence is the whole point: one blueprint, many self-contained objects.
2️⃣ The Constructor, $this & Methods
Setting every property by hand is tedious. A constructor — the special method named __construct() — runs automatically the instant you write new, so an object is born ready to use. Inside any method, $this means "this particular object", so $this->name = $name stores the passed-in value on the object. A method is just a function that lives in the class and can use $this.
<?php
// Setting every property by hand (like the last example) is tedious and
// easy to forget. A CONSTRUCTOR fixes that: __construct() runs automatically
// the moment you write 'new', so an object is fully built in one line.
class Dog
{
public string $name;
public int $age;
// $this means "this particular object". $this->name is the property
// on the object being built; $name is the value passed in.
public function __construct(string $name, int $age)
{
$this->name = $name; // store the argument on the object
$this->age = $age;
}
// A METHOD is a function that belongs to the class and can use $this.
public function describe(): string
{
return "{$this->name} is {$this->age} years old";
}
}
// The arguments go straight into __construct — no manual setup needed.
$rex = new Dog("Rex", 3);
$buddy = new Dog("Buddy", 7);
echo $rex->describe() . "\n"; // calls the method with ->
echo $buddy->describe() . "\n";Rex is 3 years old
Buddy is 7 years old3️⃣ Visibility & Constructor Promotion
Each property has a visibility that decides who may touch it. public means anyone, anywhere; private means only code inside the same class; protected is like private but also visible to classes that extend it. Hiding data behind private lets a class guarantee it stays valid — outsiders go through methods (a getter like getBalance()) instead of poking the value directly. PHP 8's constructor property promotion declares the property and assigns the argument in one move: put the visibility keyword right in the constructor's parameter list and PHP does the $this->x = $x for you.
<?php
// PHP 8 CONSTRUCTOR PROPERTY PROMOTION: declare the property AND assign it
// in one step by putting the visibility keyword in the constructor signature.
// PHP creates the property and runs $this->x = $x for you. Same result,
// far less boilerplate.
class BankAccount
{
// 'private' here both DECLARES $balance and assigns the argument to it.
// 'protected' would let subclasses see it; 'public' anyone.
public function __construct(
public string $owner, // promoted public property
private float $balance = 0, // promoted private property, default 0
) {}
public function deposit(float $amount): void
{
$this->balance += $amount; // private is reachable from INSIDE
}
public function getBalance(): float
{
return $this->balance; // a 'getter' exposes it safely
}
}
$acc = new BankAccount("Alice", 100);
$acc->deposit(50);
echo "Owner: {$acc->owner}\n"; // public -> read directly
echo "Balance: {$acc->getBalance()}\n"; // private -> via the getterOwner: Alice
Balance: 150The whole class is just a constructor and two short methods, yet $balance can never be set to a nonsense value from outside — that's encapsulation, the real payoff of private.
4️⃣ Static Members & Class Constants
Most properties belong to an object. A static property or method belongs to the class itself — there's one shared copy and you reach it with Class::member, no new needed. From inside the class you write self:: to mean "this class" (static:: is its close cousin that respects subclasses — handy once you use inheritance). A class constant (const MAX = 100;) is a fixed value attached to the class that can never change, written in UPPER_CASE by convention.
<?php
// STATIC members belong to the CLASS itself, not to any one object.
// There is exactly one copy, shared by everything. Use Class::member to
// reach it — no 'new' required.
class Counter
{
// A class CONSTANT never changes. By convention it's UPPER_CASE.
const MAX = 100;
// A static PROPERTY is shared across every object of the class.
public static int $count = 0;
public function __construct()
{
self::$count++; // self:: = "this class" — bumps the shared count
}
// A static METHOD is called on the class, not an instance.
public static function howMany(): string
{
// self::MAX reads the constant; self::$count reads the static property.
return self::$count . " of " . self::MAX . " created";
}
}
new Counter(); // each 'new' runs the constructor, so $count goes up
new Counter();
new Counter();
echo Counter::howMany() . "\n"; // Class::method() — no object needed
echo "MAX is " . Counter::MAX . "\n";3 of 100 created
MAX is 100Now you try. The class below is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
<?php
// 🎯 YOUR TURN — build a Book class, then run it.
// Fill in each blank marked ___ using the 👉 hints.
class Book
{
// 1) Declare two PUBLIC typed properties: a string $title and an int $pages
___ string $title; // 👉 add the visibility keyword: public
public ___ $pages; // 👉 add the type: int
// 2) Finish the constructor so it stores both arguments on the object
public function __construct(string $title, int $pages)
{
$this->title = ___; // 👉 the argument to save here is $title
___->pages = $pages; // 👉 the object refers to itself as $this
}
public function summary(): string
{
return "{$this->title} — {$this->pages} pages";
}
}
$b = new Book("PHP Basics", 240);
echo $b->summary() . "\n";
// ✅ Expected output:
// PHP Basics — 240 pages
?>PHP Basics — 240 pagespublic keyword, the int type, the $title argument, and the $this reference, then run it. You should see one summary line.One more — this time the blanks are about static members. Fill them in so the shared counter works.
<?php
// 🎯 YOUR TURN — finish the static counter, then run it.
// Replace each ___ using the 👉 hints.
class Robot
{
public static int $built = 0; // shared across ALL robots
public function __construct()
{
// 1) Increase the SHARED counter by one.
___::$built++; // 👉 "this class" keyword is self
}
}
new Robot();
new Robot();
// 2) Read the shared count off the CLASS, not an object.
echo Robot::___ . " robots built\n"; // 👉 the static property is $built
// ✅ Expected output:
// 2 robots built
?>2 robots builtself to bump the shared count and $built to read it off the class. Two robots are built, so the count is 2.5️⃣ Readonly Properties
Some values should be set once and then frozen — an id, a created-at timestamp, a primary key. PHP 8.1's readonly keyword does exactly that: the property can be assigned a single time (almost always inside the constructor) and any later attempt to change it throws an error. It pairs perfectly with constructor promotion, giving you tamper-proof objects with almost no code.
<?php
// A READONLY property (PHP 8.1) can be set ONCE — usually in the constructor —
// then never changed again. Perfect for an ID or anything that must stay fixed.
class User
{
public function __construct(
public readonly int $id, // set once, then locked
public string $name, // a normal, changeable property
) {}
}
$u = new User(1, "Alice");
echo "ID: {$u->id}\n";
echo "Name: {$u->name}\n";
$u->name = "Alicia"; // fine — $name is a normal property
echo "Renamed to: {$u->name}\n";
// $u->id = 2; // would crash: "Cannot modify readonly property User::\$id"
echo "ID is still: {$u->id}\n";ID: 1
Name: Alice
Renamed to: Alicia
ID is still: 1Common Errors (and the fix)
- "Cannot access private property User::$balance" — you tried to read or write a
privateproperty from outside the class (e.g.$acc->balance). Private means in-class only. Add a public method — a getter likegetBalance()— and call that instead. - "Undefined variable $name" inside a method — you forgot
$this->. A bare$nameis a local variable; the property is$this->name. Inside any non-static method, reach properties through$this. - "Using $this when not in object context" — you used
$thisinside astaticmethod. Static methods belong to the class, not an object, so there's no$this. Useself::for static members, or make the method non-static if it needs object data. - "Too few arguments to function __construct(), 0 passed" — you wrote
new Dog()but the constructor requires arguments. Pass them:new Dog("Rex", 3), or give the parameters defaults (int $age = 0). - "Cannot modify readonly property User::$id" — you tried to reassign a
readonlyproperty after it was first set. That's the whole point of readonly: set it once in the constructor and never again. If it must change, don't mark it readonly.
Pro Tips
- 💡 Start
private, widen only when needed. It's easy to make a property more visible later; locking down a public property others already use is painful. - 💡 Reach for constructor promotion by default. It removes the declare-then-assign boilerplate and is the modern, idiomatic style.
- 💡 Use constants and readonly for "facts that never change." They document intent and let PHP catch accidental edits for you.
📋 Quick Reference — PHP OOP
| Syntax | Example | What It Does |
|---|---|---|
| class | class Dog { } | Define a blueprint |
| new | new Dog("Rex", 3) | Build an object (instance) |
| public/private/protected | private int $age; | Set who can access a property |
| __construct | function __construct(...) | Runs automatically on new |
| $this-> | $this->name | This object's property/method |
| static + self:: | self::$count | Class-level data, one shared copy |
| const | const MAX = 100; | A fixed value on the class |
| readonly | readonly int $id | Set once, then locked |
Frequently Asked Questions
Q: What is the difference between a class and an object?
A class is the blueprint and an object is a thing built from it. The class Dog describes that every dog has a name and an age and can bark; each time you write new Dog() you get a separate object with its own name and age. One class can produce as many independent objects as you like — changing one object never affects the others.
Q: When should a property be public, private, or protected?
Default to private and only widen it when you have a reason. private hides a property so only code inside the same class can touch it, which lets the class guarantee its own data stays valid (you expose it through methods like getBalance() instead). protected is like private but also visible to child classes that extend it. public means anyone, anywhere can read and write it — convenient but it removes the safety the other two give you.
Q: What does $this actually mean?
$this is a reference to the specific object a method is running on. When you call $rex->describe(), inside describe() $this IS $rex, so $this->name reads Rex's name; call $buddy->describe() and the same code sees Buddy. You can only use $this inside a non-static method, because static methods belong to the class and have no particular object to point at.
Q: What is constructor property promotion and why use it?
It is PHP 8 shorthand that lets you declare a property and assign its constructor argument in a single line, by writing the visibility keyword (public/private/protected) right in the constructor's parameter list. Instead of declaring private float $balance, listing it as a parameter, and writing $this->balance = $balance, you write just private float $balance in the signature. It is the same behaviour with far less boilerplate, which is why modern PHP code uses it everywhere.
Q: When should I use static instead of a normal property or method?
Use static when the data or behaviour belongs to the class as a whole rather than to one object — for example a shared counter of how many objects exist, or a utility method that does not need any object's data. You reach static members with Class::member (or self:: from inside the class) and they exist without ever calling new. If a method needs $this, it cannot be static.
Mini-Challenge: Rectangle Class
No code is filled in this time — just a brief and an outline. Write the class yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is exactly the write-run-check loop you'll use on every real class.
<?php
// 🎯 MINI-CHALLENGE: A Rectangle class.
// No code is filled in — work from the steps below, then run it.
//
// 1. Declare a class called Rectangle
// 2. Give it a constructor using PROPERTY PROMOTION that takes
// public float $width and public float $height
// 3. Add a method area(): float that returns $this->width * $this->height
// 4. Add a method describe(): string that returns
// "Wx H = AREA" e.g. "4x3 = 12"
// 5. Build new Rectangle(4, 3) and echo its describe() on its own line
//
// Tip: inside double quotes, "{$this->width}x{$this->height}" reads both
// properties; call $this->area() to get the area.
//
// ✅ Expected output:
// 4x3 = 12
// your code here
?>$width and $height, add area() and describe() methods, then echo describe(). It should print one line.🎉 Lesson Complete!
- ✅ A class is a blueprint;
newbuilds independent objects from it - ✅ Typed properties with
public/private/protectedcontrol who can touch each value - ✅
__construct()runs onnew, and$thisrefers to the current object inside methods - ✅ Constructor promotion declares and assigns a property in one line
- ✅
staticmembers +self::/static::live on the class;constandreadonlylock values down - ✅ Next lesson: Database Integration — connect your classes to a MySQL database and store objects for real
Sign up for free to track which lessons you've completed and get learning reminders.