Class is a blueprint; object is the cookie cut from it.
Properties hold data; methods define behaviour — all inside the class.
Constructor __construct() runs immediately on 'new' to set valid state.
Use 'new ClassName()' to stamp out independent objects.
Visibility (public, protected, private) controls who touches what.
Assigning an object copies the reference, not the value — use clone for a true copy.
Plain-English First
Think of a class like a cookie cutter and an object like the actual cookie. The cutter defines the shape — it's the blueprint. Every cookie you press out is a separate object made from that same blueprint. You can make a hundred cookies, each with different icing, but they all share the same shape because they came from the same cutter. In PHP, a class is that cutter, and every time you use 'new', you're pressing out a fresh cookie.
Every serious PHP application you've ever used — Laravel, WordPress, Symfony — is built on one foundational idea: objects. Not arrays. Not loose functions scattered across files. Objects. The reason experienced developers reach for OOP isn't because it sounds fancy; it's because real-world problems naturally map to things that have both data and behaviour. A user doesn't just have a name — a user can also log in, update their profile, and reset their password. Bundling that data and those actions together is exactly what classes let you do.
Before OOP, PHP code tended to sprawl. You'd have a users.php with fifty functions, half of them needing the same $db variable passed around, half of them accidentally sharing global state. Bugs were hard to trace because data lived everywhere. Classes solve this by giving each concept in your application its own fenced-off space — its own properties to hold data and its own methods to act on it. Change the internals of a class without breaking anything outside it. That's the deal.
By the end of this article you'll understand not just how to define a class and instantiate an object, but why the constructor exists, what visibility keywords actually protect, how to tell a class method from an instance method, and the patterns senior developers use daily. You'll also walk away knowing the mistakes that trip up 80% of beginners so you can skip straight past them.
Defining a Class: Blueprint Before You Build Anything
A class is a template. It describes what a thing looks like (its properties) and what a thing can do (its methods). Nothing actually exists in memory until you instantiate it with 'new'. This is the most important mental model shift: writing a class doesn't create a user, it defines what a user is.
Properties are variables that belong to the class. Methods are functions that belong to the class. Both live inside the class body, and both can be marked as public, protected, or private — more on that shortly.
The constructor is a special method named __construct(). PHP calls it automatically the moment you use 'new ClassName()'. Its job is to set the object up in a valid state. If you're building a BankAccount, the constructor should insist on an opening balance. You shouldn't be able to create a BankAccount that starts in an undefined, broken state — the constructor is your gatekeeper.
Notice in the example below how $this refers to the specific object being worked with. It's the object saying 'my own property'. Every object has its own copy of properties, which is why two BankAccount objects can have different balances without interfering with each other.
BankAccount.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php
classBankAccount
{
// Properties — data the object holds// 'private' means ONLY code inside this class can touch these directlyprivate string $ownerName;
private float $balance;
// The constructor runs automatically when you write: new BankAccount(...)// It guarantees the object starts in a valid, known statepublicfunction__construct(string $ownerName, float $openingBalance)
{
if ($openingBalance < 0) {
// Throw early — never let broken data into your objectthrownewInvalidArgumentException('Opening balance cannot be negative.');
}
$this->ownerName = $ownerName; // $this means THIS specific object
$this->balance = $openingBalance;
}
// A method that changes internal statepublicfunctiondeposit(float $amount): void
{
if ($amount <= 0) {
thrownewInvalidArgumentException('Deposit amount must be positive.');
}
$this->balance += $amount; // Update THIS object's own balance
}
// A method that reads and returns statepublicfunctiongetBalance(): float
{
return $this->balance;
}
publicfunctiongetSummary(): string
{
return"{$this->ownerName}'s account — Balance: £{$this->balance}";
}
}
// --- Instantiation: pressing the cookie cutter ---
$aliceAccount = new BankAccount('Alice', 500.00); // Creates one object
$bobAccount = new BankAccount('Bob', 250.00); // Creates a SEPARATE object// Alice deposits — only her object changes
$aliceAccount->deposit(150.00);
echo $aliceAccount->getSummary() . PHP_EOL;
echo $bobAccount->getSummary() . PHP_EOL;
// Demonstrate the constructor guardtry {
$brokenAccount = newBankAccount('Eve', -100);
} catch (InvalidArgumentException $e) {
echo'Caught: ' . $e->getMessage() . PHP_EOL;
}
Output
Alice's account — Balance: £650
Bob's account — Balance: £250
Caught: Opening balance cannot be negative.
Pro Tip: Validate in the Constructor, Not Everywhere Else
If you validate data at the point of object creation, every method inside the class can trust that the data is already clean. You write the validation once instead of checking it in every deposit(), withdraw(), and transfer() method separately.
Production Insight
In production, missing constructor validation leads to zombie objects — instances in an invalid state that silently corrupt data. Always enforce constraints at birth.
A common pattern: use a named constructor (static factory) that validates before calling the real constructor, especially when initialisation requires external lookups.
Rule: if an object can be constructed in an invalid state, someone will eventually do it.
Key Takeaway
A class is a zero-cost blueprint.
Constructor is your gatekeeper — validate all required data there.
Use 'new' to stamp out as many independent objects as you need.
Constructor Design Decision Guide
IfObject requires data to function
→
UseMake those parameters required in __construct()
IfSome parameters have sensible defaults
→
UseUse constructor promotion with defaults (PHP 8+) or set defaults in constructor body
UseUse a static factory method to encapsulate the lookup; keep constructor lightweight
IfObject can be created empty and filled later
→
UseKeep constructor parameterless but ensure all setters validate
Visibility Keywords: public, protected, and private Actually Enforced
Visibility is the mechanism that lets you separate what an object exposes to the world from what it keeps to itself. Most beginners mark everything public because it's easier. That's a trap — it means any code anywhere in your codebase can reach in and mangle your object's state without going through your methods.
Think of it like a car dashboard. The steering wheel and pedals are public — they're designed to be used by the driver. The engine internals are private — you're not meant to reach in and adjust the fuel injectors directly while driving. That encapsulation is what makes the car safe to use.
public means anyone, anywhere can access it. protected means only this class and any class that extends it can access it (useful for inheritance). private means only code inside this exact class can access it.
The real-world pattern most PHP developers use: make all properties private, then expose only what outside code genuinely needs through carefully designed public methods. This is called encapsulation and it's one of OOP's four pillars. The payoff is that you can completely rewrite how a class stores its data internally without breaking any code that uses the class — as long as the public methods keep working the same way.
UserProfile.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
classUserProfile
{
private string $email; // Never exposed directly — change format internally anytime
private string $passwordHash; // Must NEVER be public
private int $loginCount = 0; // Internal bookkeeping onlypublicfunction__construct(string $email, string $plainTextPassword)
{
$this->setEmail($email); // Reuse validation logic via a private method// Hash immediately — plain text NEVER gets stored on the object
$this->passwordHash = password_hash($plainTextPassword, PASSWORD_BCRYPT);
}
// Public setter — validates before accepting data (the only door in)publicfunctionupdateEmail(string $newEmail): void
{
$this->setEmail($newEmail); // Centralised validation
}
// Public getter — returns a safe view of internal datapublicfunctiongetEmail(): string
{
return $this->email;
}
// Public behaviour method — records the action internallypublicfunctionrecordLogin(): void
{
$this->loginCount++;
}
publicfunctiongetLoginCount(): int
{
return $this->loginCount;
}
// Private — internal helper, NOT part of the public API// Outside code has no business calling this directlyprivatefunctionsetEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
thrownewInvalidArgumentException("Invalid email address: {$email}");
}
$this->email = strtolower(trim($email)); // Normalise on the way in
}
}
$user = newUserProfile(' Alice@Example.COM ', 'hunter2');
$user->recordLogin();
$user->recordLogin();
echo $user->getEmail() . PHP_EOL; // Normalised automaticallyecho $user->getLoginCount() . PHP_EOL;
// This line would cause a Fatal Error — uncomment to see it// echo $user->passwordHash; // Cannot access private propertytry {
$user->updateEmail('not-an-email');
} catch (InvalidArgumentException $e) {
echo'Caught: ' . $e->getMessage() . PHP_EOL;
}
Output
alice@example.com
2
Caught: Invalid email address: not-an-email
Watch Out: Public Properties Break Encapsulation Silently
If $passwordHash were public, nothing stops a developer six months from now writing $user->passwordHash = 'letmein' and unknowingly bypassing your hashing logic entirely. The bug won't throw an error — it'll just silently corrupt data. Private properties prevent this class of mistake at the language level.
Production Insight
Making properties public to 'save time' creates technical debt that compounds. Every direct property access outside the class is a hidden coupling — you can't rename the property without hunting down every usage.
In production, the cost of a public property bug often exceeds the time saved by not writing getters by a factor of 100x.
Rule: default to private. Only promote to protected when subclassing demands it. Public properties are a code smell.
Key Takeaway
public = exposed.
protected = family only.
private = sealed.
Default to private. Expose through tested public methods. Your future self will thank you.
Visibility Level Decision Guide
IfWill external code need to read this value?
→
UseKeep property private, expose via a public getter method
IfWill external code need to change this value?
→
UseKeep property private, expose via a public setter with validation
IfWill a subclass need access to this property or method?
→
UseUse protected instead of private
IfNo external code should ever touch this
→
UseKeep it private — even subclasses don't need to know
Static Methods and Properties: When the Class Itself Needs to Know Things
Every object you've seen so far has its own independent copy of its properties. That's usually what you want. But sometimes a piece of data or behaviour belongs to the class itself — not to any one instance of it. That's what static is for.
A classic example is a counter tracking how many objects of a class have been created. You can't store that on any single object because no single object knows about the others. The class needs to hold it centrally.
Another common use case is factory methods — static methods that construct and return a new instance with a specific configuration. Laravel and many modern PHP frameworks use this pattern heavily: User::create([...]), Carbon::now(), Response::json(...).
Access static members with the :: operator (called the scope resolution operator), not ->. Inside the class, use self:: to refer to the class itself rather than $this. Using $this inside a static method is a fatal error because there is no 'this' — no object is involved.
Use static sparingly. Overusing it leads you back toward procedural code with global state. The sweet spot is factory methods and genuine class-level metadata like the counter below.
DatabaseConnection.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
classDatabaseConnection
{
// Static property — belongs to the CLASS, shared across all instancesprivatestatic int $connectionCount = 0;
private static ?self $primaryInstance = null; // For the singleton patternprivate string $dsn;
private bool $isConnected = false;
// Private constructor — forces use of the factory method belowprivatefunction__construct(string $dsn)
{
$this->dsn = $dsn;
self::$connectionCount++; // self:: targets the class, not an instance
}
// Static factory method — the only way to create an instance from outside// This pattern lets you add caching, logging, or validation in one placepublicstaticfunctioncreate(string $dsn): self
{
return new self($dsn); // 'new self()' creates an instance of THIS class
}
// Singleton factory — returns the same instance every time (common for DB connections)publicstaticfunctiongetPrimaryConnection(string $dsn): self
{
if (self::$primaryInstance === null) {
self::$primaryInstance = newself($dsn);
}
return self::$primaryInstance; // Returns the SAME object on subsequent calls
}
publicfunctionconnect(): void
{
// Simulate connection — real code would use PDO here
$this->isConnected = true;
echo"Connected to: {$this->dsn}" . PHP_EOL;
}
// Static method — usable without any instance at allpublicstaticfunctiongetTotalConnections(): int
{
return self::$connectionCount;
}
}
// Factory method in action — clean, readable, can validate internally
$readReplica = DatabaseConnection::create('mysql:host=replica1;dbname=shop');
$writeReplica = DatabaseConnection::create('mysql:host=primary;dbname=shop');
$readReplica->connect();
$writeReplica->connect();
// Access static data via the class name — no object neededecho'Total connections created: ' . DatabaseConnection::getTotalConnections() . PHP_EOL;
// Singleton — both variables point to the EXACT same object
$connA = DatabaseConnection::getPrimaryConnection('mysql:host=primary;dbname=shop');
$connB = DatabaseConnection::getPrimaryConnection('mysql:host=primary;dbname=shop');
echo'Same instance? ' . ($connA === $connB ? 'Yes' : 'No') . PHP_EOL;
Output
Connected to: mysql:host=replica1;dbname=shop
Connected to: mysql:host=primary;dbname=shop
Total connections created: 2
Same instance? Yes
Interview Gold: self:: vs $this — Know the Difference Cold
$this refers to the current object instance at runtime. self:: refers to the class in which the code is written at compile time. There's also static:: (late static binding) which resolves to whatever class was actually called at runtime — crucial when you have static methods in an inheritance chain. If an interviewer asks about this, mention late static binding and watch their eyes light up.
Production Insight
Static methods seem convenient but they're notoriously hard to mock in tests. If you scatter static calls throughout your code, you'll end up with test suites that rely on fragile workarounds.
A production issue: a static $connectionCache got corrupted under high concurrency because static state is shared across all requests in the same PHP-FPM process.
Rule: limit static to factory methods and pure utility functions that don't hold mutable state.
Key Takeaway
static = class level, not instance level.
Use self:: for static members, $this for instance members.
Never mix them — static method + $this = fatal error.
Limit static use to factories and pure utilities.
Static vs Instance Decision Guide
IfMethod reads/writes instance properties
→
UseMust be an instance method — never use static
IfMethod creates and returns a new instance
→
UseStatic factory method is a good fit
IfMethod is a utility that takes inputs and returns output, no side effects
→
UseStatic is fine, but consider making it a standalone function if FP style
IfNeed to track cross-instance state (e.g., total connections)
→
UseStatic property is appropriate, but beware of mutable state in shared memory
Object Cloning and Comparison: Two Gotchas That Bite in Production
Objects in PHP are passed by reference-like handles. This trips up developers who come from a JavaScript or Python background and also those who've only worked with PHP primitives. When you assign an object to a new variable, you don't get a copy — both variables point at the same object. Change it through one variable and the other sees the change too.
To get a true independent copy, you use the clone keyword. PHP then calls the magic __clone() method on the new copy if you've defined one — use that to deep-clone any nested objects the class holds, because clone is shallow by default.
Comparison has its own wrinkle. == checks if two objects have the same class and same property values. === checks if both variables point to the exact same instance in memory. This matters in tests and in any logic where identity (not just equality) matters.
This section pulls together everything from the article — you'll see a class with a constructor, private properties, a public API, and now cloning all working together. Think of this as the capstone example.
ShoppingCart.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php
classCartItem
{
publicfunction__construct(
publicreadonly string $sku,
public int $quantity
) {}
}
classShoppingCart
{
privatearray $items = [];
private string $currency;
publicfunction__construct(string $currency = 'GBP')
{
$this->currency = $currency;
}
publicfunctionaddItem(CartItem $item): void
{
// Store the item — note: this stores a REFERENCE to the CartItem object
$this->items[$item->sku] = $item;
}
publicfunctiongetItemCount(): int
{
returnarray_sum(array_column(
array_map(fn($i) => ['qty' => $i->quantity], $this->items),
'qty'
));
}
// __clone is called automatically after PHP does the shallow copy// Without this, $cart->items would still point to the SAME CartItem objectspublicfunction__clone()
{
$clonedItems = [];
foreach ($this->items as $sku => $item) {
// Deep clone each nested object so the copy is truly independent
$clonedItems[$sku] = clone $item;
}
$this->items = $clonedItems;
}
}
$originalCart = newShoppingCart('GBP');
$originalCart->addItem(newCartItem('TSHIRT-RED-M', 2));
$originalCart->addItem(newCartItem('MUG-FORGE', 1));
// Without clone — BOTH variables point to the same object
$sameCart = $originalCart;
$sameCart->addItem(new CartItem('HOODIE-BLUE-L', 3)); // This modifies $originalCart too!
echo 'Same-reference cart items: ' . $originalCart->getItemCount() . PHP_EOL; // 6, not 3// With clone — completely independent copy
$giftCart = clone $originalCart; // __clone() fires, deep-copies the items array
$giftCart->addItem(newCartItem('GIFT-WRAP', 1));
echo 'Original cart items: ' . $originalCart->getItemCount() . PHP_EOL; // Unchanged
echo 'Gift cart items: ' . $giftCart->getItemCount() . PHP_EOL; // Has the extra item// Comparison demo
$anotherRef = $originalCart; // Same instance
$clonedCopy = clone $originalCart; // Different instance, same valuesecho PHP_EOL;
echo '$anotherRef == $originalCart: ' . var_export($anotherRef == $originalCart, true) . PHP_EOL; // true
echo '$anotherRef === $originalCart: ' . var_export($anotherRef === $originalCart, true) . PHP_EOL; // true
echo '$clonedCopy == $originalCart: ' . var_export($clonedCopy == $originalCart, true) . PHP_EOL; // true
echo '$clonedCopy === $originalCart: ' . var_export($clonedCopy === $originalCart, true) . PHP_EOL; // false!
Output
Same-reference cart items: 6
Original cart items: 6
Gift cart items: 7
$anotherRef == $originalCart: true
$anotherRef === $originalCart: true
$clonedCopy == $originalCart: true
$clonedCopy === $originalCart: false
Watch Out: clone Is Shallow by Default
If your class holds other objects as properties and you don't define __clone(), cloning gives you a new outer object but the nested objects are still shared. Mutate a nested object on the clone and you've mutated the original too. Always implement __clone() when your class contains object properties.
Production Insight
Ghost mutations from shallow clones are notoriously hard to debug. The original and clone appear separate in logs, but nested objects change together. In a complex order system, this could corrupt pricing or inventory counts.
Profiling tip: use debug_zval_refs() on suspected objects to see reference counts. If you see refcount > 1 and no intentional sharing, you've got a shallow clone problem.
Rule: if your class has an array of objects or any object-type property, write __clone() to deep-copy them.
IfClass has only scalar properties (string, int, float, bool)
→
UseUse clone as-is — shallow copy is sufficient
IfClass has object-type properties (including arrays of objects)
→
UseImplement __clone() and deep-copy each object property
IfClass has properties that should remain shared (e.g., logger)
→
UseIn __clone(), manually reassign those properties to the same reference
IfNeed to validate clones have different identity in tests
→
UseUse assertNotSame($original, $clone) and assertInstanceOf()
Inheritance in PHP: Extending Classes and Method Overriding
Inheritance lets you create a new class based on an existing one. The child class (subclass) inherits all public and protected properties and methods from the parent class (superclass). You can then add new properties and methods, or override existing ones to change behaviour.
PHP supports single inheritance — a class can extend only one parent class. But a parent class can have many children. This is the classic 'is-a' relationship: a Truck is a Vehicle, a Circle is a Shape.
The child class uses the extends keyword. Inside the child, you call parent::method() to invoke the parent's version of a method. Overriding methods must have compatible signatures — PHP enforces this at compile time.
A common mistake is forgetting to call the parent constructor if the parent has mandatory setup logic. A child class must explicitly call parent::__construct() if the parent's constructor is defined and does important work.
Use inheritance when the child class genuinely is a more specific version of the parent. If the relationship is more about sharing behaviour than identity, favour composition or traits instead.
VehicleInheritance.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<?php
abstractclassVehicle
{
protected string $make;
protected string $model;
protected int $year;
publicfunction__construct(string $make, string $model, int $year)
{
$this->make = $make;
$this->model = $model;
$this->year = $year;
}
abstractpublicfunctiongetFuelType(): string;
publicfunctiongetDescription(): string
{
return"{$this->year} {$this->make} {$this->model}";
}
}
classCarextendsVehicle
{
private int $doors;
publicfunction__construct(string $make, string $model, int $year, int $doors)
{
parent::__construct($make, $model, $year);
$this->doors = $doors;
}
publicfunctiongetFuelType(): string
{
return'Petrol';
}
publicfunctiongetDoors(): int
{
return $this->doors;
}
}
classElectricCarextendsCar
{
private int $batteryCapacity; // kWhpublicfunction__construct(string $make, string $model, int $year, int $doors, int $batteryCapacity)
{
parent::__construct($make, $model, $year, $doors);
$this->batteryCapacity = $batteryCapacity;
}
// Override fuel type — electric doesn't use petrolpublicfunctiongetFuelType(): string
{
return'Electric';
}
publicfunctiongetBatteryRange(): int
{
// Rough estimate: 6 km per kWhreturn $this->batteryCapacity * 6;
}
}
$tesla = newElectricCar('Tesla', 'Model 3', 2024, 4, 75);
echo $tesla->getDescription() . PHP_EOL; // Inherited from Vehicle
echo 'Fuel type: ' . $tesla->getFuelType() . PHP_EOL; // Overridden
echo 'Doors: ' . $tesla->getDoors() . PHP_EOL; // Inherited from Car
echo 'Range: ' . $tesla->getBatteryRange() . ' km' . PHP_EOL; // ElectricCar specific
Output
2024 Tesla Model 3
Fuel type: Electric
Doors: 4
Range: 450 km
Mental Model: Inheritance Is 'is-a', Not 'has-a'
A child class must be a specialised version of the parent (Dog extends Animal).
If you're thinking 'this new class needs the same methods as that class', consider composition: pass the behaviour in via dependency injection.
PHP's single inheritance means you get only one shot at the parent. Choose wisely.
Favour composition over inheritance — it's less brittle and easier to test.
Production Insight
Deep inheritance hierarchies (more than 3 levels) become extremely hard to maintain. A change in the top-most class can silently break behaviour in all descendants — this is the 'fragile base class problem'.
Rule of thumb: keep inheritance hierarchies flat. If you need more polymorphism, use interfaces and composition.
In production, shallow hierarchies with clear contracts (abstract methods) are far easier to reason about than deep, mixed hierarchies.
Key Takeaway
Inheritance is 'is-a'.
Always call parent::__construct() if the parent has one.
Override methods to specialise behaviour.
Keep hierarchies flat — 3 levels max.
When in doubt, compose instead of inherit.
Inheritance vs Composition Decision Guide
IfDoes the subclass truly 'is-a' kind of the parent?
→
UseInheritance is appropriate
IfDo you want to reuse behaviour without the subclass identity?
→
UseUse composition (inject a collaborator) or PHP traits
IfDo you need to override more than 2-3 methods of the parent?
→
UsePossibly a sign of poor abstraction — reconsider the hierarchy
IfIs the parent class concrete (not abstract)?
→
UsePrefer abstract base classes or interfaces for polymorphic contracts
● Production incidentPOST-MORTEMseverity: high
Fatal Error: Using $this in Static Context Took Down a Deployment
Symptom
Your API returns 500 errors intermittently with the message: 'Fatal error: Uncaught Error: Using $this when not in object context'.
Assumption
Using static means you can still reference instance properties via $this — it's just a shortcut for calling the method without an object instance.
Root cause
A static method belongs to the class, not to any instance. When called as ClassName::method(), there is no $this. Any $this reference inside a static method raises an immediate fatal error.
Fix
1. Remove the static keyword if the method uses $this. 2. Or refactor the method to use only static properties and self::. 3. Add a static analyzer rule (e.g., PHPStan level 6) to catch $this in static methods before deployment.
Key lesson
Never use $this inside a static method — PHP will kill the request.
When you see static, you should not see $this anywhere in that method chain.
Add static analysis to your CI pipeline to catch this before it hits production.
Production debug guideThree frequent object-related failures and the commands to diagnose them fast.3 entries
Symptom · 01
Call to a member function getName() on null
→
Fix
Check the variable type with var_dump($object) at the fail point. Ensure the object was instantiated (e.g., $user = new User() before calling methods). Review constructor logic — any condition that might skip assignment?
Symptom · 02
Using $this when not in object context
→
Fix
Identify which method is called statically. Search for :: in the call stack. Remove static from the method definition or replace $this with self:: for static properties/methods.
Symptom · 03
Cloning an object does not produce an independent copy (ghost mutations)
→
Fix
Check if the class has object-type properties. If yes, add a __clone() method that deep-clones each nested object. Use debug_zval_refs() to check reference counts before and after clone.
★ Quick Debugging Commands for PHP Object IssuesKeep these commands handy when you suspect constructor failures, cloning problems, or static/instance confusion.
Object property is null unexpectedly−
Immediate action
Check constructor assignment order and early returns.
Commands
var_dump($object->properties) // See all current values
print_r(get_object_vars($object)) // List all accessible properties
Fix now
Add a guard clause in the constructor to ensure required parameters are passed.
Modifying one object changes another+
Immediate action
Identify if the modification is via assignment or clone. Use debug_zval_refs to see reference count.
Use clone when assigning, and implement __clone() for deep copy if nested objects exist.
Fatal error: Using $this when not in object context+
Immediate action
Find the static method call and examine the method definition for $this.
Commands
php -l filename.php // Syntax check the file
grep -n 'static function' filename.php // List all static methods
Fix now
Remove the static keyword from the method if it uses $this, or replace $this with self::.
Procedural vs OOP PHP
Aspect
Procedural PHP (functions)
OOP PHP (classes and objects)
Data + behaviour bundling
Separate — arrays passed between functions
Together — properties and methods on one object
State management
Global variables or function parameters
Encapsulated in object properties
Code reuse
Copy-paste or include files
Instantiate new objects; use inheritance
Validation location
Scattered — each function must check inputs
Centralised in constructor and setters
Testability
Hard — functions depend on global state
Easy — inject dependencies, mock objects
Access control
None — all data is accessible everywhere
public / protected / private enforced by PHP
Typical Laravel route handler
Rare — used only for tiny utility scripts
Standard — controllers, models, services are all classes
Key takeaways
1
A class is a blueprint that costs nothing in memory
objects are the real things created from it with 'new'. Write the blueprint once, stamp out as many objects as you need.
2
The constructor is your gatekeeper
validate and assign all required data there so every method inside the class can trust the object is always in a valid state.
3
Mark properties private by default
expose only what outside code genuinely needs through public methods. Encapsulation is what lets you refactor internals without breaking callers.
4
Assigning an object to a variable does NOT copy it
both variables point to the same instance. Use clone when you need independence, and define __clone() when the class holds nested objects.
5
Inheritance is 'is-a'. Always call parent::__construct(). Keep hierarchies shallow
deep inheritance is a maintenance nightmare.
Common mistakes to avoid
4 patterns
×
Forgetting that object assignment copies the reference, not the object
Symptom
You modify what you think is a copy but the original changes too, causing ghost mutations that are painful to debug.
Fix
Use the clone keyword when you need an independent copy, and implement __clone() to deep-clone any nested object properties.
×
Making all properties public to avoid writing getters
Symptom
External code starts depending on internal property names, so renaming or restructuring the property breaks code all over the project.
Fix
Mark properties private, expose only what outside code actually needs via specific public methods, and name those methods after the intent (getFormattedPrice()) not the storage (getPriceInPence()).
×
Using $this inside a static method
Symptom
PHP throws a Fatal error: Using $this when not in object context.
Fix
Static methods have no object instance, so use self:: to access static properties or methods. If you find yourself needing $this, the method shouldn't be static in the first place.
×
Forgetting to call parent::__construct() in a child class
Symptom
Parent properties remain uninitialised, leading to 'Typed property must not be accessed before initialization' errors or null values.
Fix
In the child constructor, always call parent::__construct($args) before any child-specific initialisation.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is the difference between a class and an object in PHP, and can you...
Q02JUNIOR
Explain the three visibility modifiers in PHP — public, protected, and p...
Q03SENIOR
What is the difference between self:: and static:: in a PHP class, and i...
Q04SENIOR
How does PHP handle object cloning? What is the difference between shall...
Q01 of 04JUNIOR
What is the difference between a class and an object in PHP, and can you give a real-world analogy to illustrate it?
ANSWER
A class is a blueprint — it defines properties and methods that describe a type of thing. An object is a concrete instance created from that blueprint. Think of a cookie cutter (class) and the cookies you stamp out (objects). Every cookie has the same shape but can have different icing. In code, each object has its own independent copy of the properties, so changing one object's state doesn't affect others.
Q02 of 04JUNIOR
Explain the three visibility modifiers in PHP — public, protected, and private — and describe a concrete scenario where you'd choose private over public for a property.
ANSWER
public = accessible from anywhere. protected = accessible from the class and its subclasses. private = accessible only from the class itself. Choose private for properties that store sensitive internal state — e.g., a password hash. If you make it public, any caller can bypass your hashing logic by directly setting $user->passwordHash = 'letmein'. Private enforces that all changes go through your defined methods, keeping the object in a valid state.
Q03 of 04SENIOR
What is the difference between self:: and static:: in a PHP class, and in what scenario would using self:: give you the wrong result when inheritance is involved?
ANSWER
self:: resolves to the class where the code is written at compile time. static:: (late static binding) resolves to the class that was actually called at runtime. This matters when you have a static method in a parent class that is called from a child class. If the parent uses self::, it will always refer to the parent class, even if the child overrides the method. Static:: makes it behave polymorphically — the child's version is used if it exists. Example: parent::getInstance() in a factory method should use static:: to return an instance of the subclass that called it.
Q04 of 04SENIOR
How does PHP handle object cloning? What is the difference between shallow and deep copy?
ANSWER
PHP's clone keyword creates a shallow copy of the object. The new object gets its own copy of scalar properties, but any object-type properties are still references to the same objects. To get a true independent copy (deep copy), you must define a __clone() method that manually clones each nested object. Without it, mutating a nested object on the clone will affect the original. Always implement __clone() when your class holds objects.
01
What is the difference between a class and an object in PHP, and can you give a real-world analogy to illustrate it?
JUNIOR
02
Explain the three visibility modifiers in PHP — public, protected, and private — and describe a concrete scenario where you'd choose private over public for a property.
JUNIOR
03
What is the difference between self:: and static:: in a PHP class, and in what scenario would using self:: give you the wrong result when inheritance is involved?
SENIOR
04
How does PHP handle object cloning? What is the difference between shallow and deep copy?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between a class and an object in PHP?
A class is the blueprint or template — it defines what properties and methods something has. An object is a specific instance created from that blueprint using the 'new' keyword. You define a class once but can create as many objects from it as you like, each with its own independent property values.
Was this helpful?
02
When should I use a static method instead of a regular method in PHP?
Use a static method when the behaviour belongs to the class itself rather than to any particular instance. Common use cases are factory methods (User::create()), singleton accessors, and utility functions that don't need to read or write any object properties. If the method touches $this or any instance property, it should not be static.
Was this helpful?
03
Why does changing an object through one variable affect another variable pointing to the same object?
PHP objects are accessed through handles, so assigning an object to a new variable gives both variables a handle to the exact same object in memory — not a copy. To get a genuinely independent copy you must use the clone keyword. If the class holds other objects as properties, you should also implement __clone() to deep-clone those nested objects, because the default shallow clone still shares them.
Was this helpful?
04
Do I always need to call parent::__construct() in a child class?
If the parent constructor does important work (e.g., setting properties, validating data, connecting to a database), you must call parent::__construct() explicitly in the child constructor. PHP does not call it automatically. If the parent constructor has required parameters, you must pass them from the child. Omitting the call can lead to uninitialised properties and runtime errors.
Was this helpful?
05
What is the difference between == and === when comparing objects in PHP?
The == operator checks if two objects have the same class and the same property values. The === operator checks if both variables reference the exact same instance in memory. Use == when you care about value equality (e.g., two different User objects with the same ID should be considered equal). Use === when you need to confirm identity (e.g., caching: is the object already in memory?).