Object-Oriented PHP
Reviewed & published by Brayan K
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.
Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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
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";Notice $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";3️⃣ 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 getterThe 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";Now 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
?>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
?>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";Common Errors (and the fix)
- "Cannot access private property User::$balance" — you tried to read or write a private property from outside the class (e.g. $acc->balance). Private means in-class only. Add a public method — a getter like getBalance() — and call that instead.
- "Undefined variable $name" inside a method — you forgot $this->. A bare $name is 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 $this inside a static method. Static methods belong to the class, not an object, so there's no $this. Use self:: 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 readonly property 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 |
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
?>🎉 Lesson Complete!
- ✅ A class is a blueprint; new builds independent objects from it
- ✅ Typed properties with public/private/protected control who can touch each value
- ✅ __construct() runs on new, and $this refers to the current object inside methods
- ✅ Constructor promotion declares and assigns a property in one line
- ✅ static members + self::/static:: live on the class; const and readonly lock values down
Practice quiz
What is the relationship between a class and an object?
- They are the same thing
- An object is a blueprint for a class
- A class is a blueprint; an object is built from it
- A class is one instance of an object
Answer: A class is a blueprint; an object is built from it. A class is the blueprint, and each object (instance) is a thing built from it with new.
Which keyword builds an object from a class?
- new
- create
- build
- make
Answer: new. new builds an object (instance) from a class, e.g. new Dog().
Which operator reaches a property or method on an object?
- ::
- .
- =>
- ->
Answer: ->. The arrow operator -> accesses an object's properties and methods, e.g. $rex->name.
What is the name of the special method that runs automatically on new?
- __init
- __construct
- __new
- __start
Answer: __construct. __construct() is the constructor; it runs the instant you write new.
What does $this refer to inside a method?
- The specific object the method is running on
- The class itself
- A global variable
- The parent class
Answer: The specific object the method is running on. $this is a reference to the particular object the method is being called on.
What does private visibility mean for a property?
- Anyone can read and write it
- It can never be set
- Only code inside the same class can access it
- It is shared across all objects
Answer: Only code inside the same class can access it. private restricts access to code inside the same class; outsiders use methods like getters.
What does constructor property promotion let you do?
- Make all properties public
- Declare a property and assign it in one step in the constructor signature
- Skip the constructor entirely
- Make a property readonly only
Answer: Declare a property and assign it in one step in the constructor signature. Putting a visibility keyword in the constructor's parameter list declares the property and assigns it.
How do you reach a static member from inside its own class?
- $this->
- new
- ->
- self::
Answer: self::. Inside the class you use self:: (e.g. self::$count) to reach static members and constants.
What is true of a readonly property (PHP 8.1)?
- It can never be set
- It can be set once, then never changed
- It is always public
- It is shared across objects
Answer: It can be set once, then never changed. A readonly property is assigned once (usually in the constructor) and locked thereafter.
What does a static property belong to?
- Each individual object
- The parent class only
- The class itself, shared by all
- A global scope
Answer: The class itself, shared by all. A static property has one shared copy on the class, reachable with Class::member.
Continue this course
- Previous: Sessions & Cookies
- Next: Enums (PHP 8.1) — Model a fixed set of values with backed enums, methods, and constants
- Quick reference: PHP cheat sheet › OOP
Frequently asked questions
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.
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.
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.
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.
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.