PHP Classes and Objects — $this Error in Static Context
Fatal error: Using $this in static context causes 500 errors.
20+ years shipping production PHP systems at scale. Everything here is grounded in real deployments.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- 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.
PHP classes and objects are the language's implementation of object-oriented programming (OOP), a paradigm that bundles data (properties) and behavior (methods) into reusable, encapsulated units. A class is a blueprint—you define it once with class User { ... }—and then instantiate it into objects ($user = new ) that hold their own state.User()
This exists because procedural PHP with global functions and arrays becomes unmanageable beyond a few thousand lines; OOP lets you model real-world entities (users, orders, payments) with clear boundaries and predictable behavior. In the PHP ecosystem, classes are mandatory for modern frameworks like Laravel, Symfony, and WordPress plugin development, but they're overkill for simple scripts—if you're just processing a form submission, a function is fine.
The core mechanics are straightforward but have sharp edges. Visibility keywords (public, protected, private) are actually enforced at runtime—unlike JavaScript or Python where they're conventions—so $user->password throws an error if $password is private.
Static methods and properties (public static $count) belong to the class itself, not instances, which is where the infamous $this error bites: inside a static method, there's no $this because no object context exists. Inheritance via extends lets you override methods, but PHP's single-inheritance model means you'll reach for traits or interfaces for cross-cutting concerns.
Cloning with clone does a shallow copy by default, so nested objects share references—a production bug that silently corrupts data until you explicitly implement _. Comparison with _clone()== checks property values, while === checks object identity (same instance), a distinction that causes subtle logic errors when caching or comparing entities.
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.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
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.
What PHP Classes and Objects Actually Are
PHP classes are blueprints for objects — they define properties and methods that instances will hold. Objects are runtime instances of a class, each with its own memory space for property values. The core mechanic: a class declares structure; the new keyword materializes it into a live object.
When you call a method on an object, PHP automatically injects $this as a reference to that specific instance. This is how methods access an object's own properties and other methods. Static methods, declared with the static keyword, belong to the class itself, not any instance — they cannot use $this because there is no object context. Calling a non-static method statically (e.g., ClassName::method()) triggers a deprecation warning in PHP 7 and an error in PHP 8, because $this is undefined.
Use classes and objects when you need to model entities with state and behavior — users, orders, HTTP requests. In real systems, this is the foundation for encapsulation, dependency injection, and testable code. Without objects, you end up with global state and procedural spaghetti that breaks under any non-trivial load.
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 . PHP calls it automatically the moment you use 'new __construct()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.
<?php class BankAccount { // Properties — data the object holds // 'private' means ONLY code inside this class can touch these directly private string $ownerName; private float $balance; // The constructor runs automatically when you write: new BankAccount(...) // It guarantees the object starts in a valid, known state public function __construct(string $ownerName, float $openingBalance) { if ($openingBalance < 0) { // Throw early — never let broken data into your object throw new InvalidArgumentException('Opening balance cannot be negative.'); } $this->ownerName = $ownerName; // $this means THIS specific object $this->balance = $openingBalance; } // A method that changes internal state public function deposit(float $amount): void { if ($amount <= 0) { throw new InvalidArgumentException('Deposit amount must be positive.'); } $this->balance += $amount; // Update THIS object's own balance } // A method that reads and returns state public function getBalance(): float { return $this->balance; } public function getSummary(): 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 guard try { $brokenAccount = new BankAccount('Eve', -100); } catch (InvalidArgumentException $e) { echo 'Caught: ' . $e->getMessage() . PHP_EOL; }
deposit(), withdraw(), and transfer() method separately.__construct()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.
<?php class UserProfile { private string $email; // Never exposed directly — change format internally anytime private string $passwordHash; // Must NEVER be public private int $loginCount = 0; // Internal bookkeeping only public function __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) public function updateEmail(string $newEmail): void { $this->setEmail($newEmail); // Centralised validation } // Public getter — returns a safe view of internal data public function getEmail(): string { return $this->email; } // Public behaviour method — records the action internally public function recordLogin(): void { $this->loginCount++; } public function getLoginCount(): int { return $this->loginCount; } // Private — internal helper, NOT part of the public API // Outside code has no business calling this directly private function setEmail(string $email): void { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Invalid email address: {$email}"); } $this->email = strtolower(trim($email)); // Normalise on the way in } } $user = new UserProfile(' Alice@Example.COM ', 'hunter2'); $user->recordLogin(); $user->recordLogin(); echo $user->getEmail() . PHP_EOL; // Normalised automatically echo $user->getLoginCount() . PHP_EOL; // This line would cause a Fatal Error — uncomment to see it // echo $user->passwordHash; // Cannot access private property try { $user->updateEmail('not-an-email'); } catch (InvalidArgumentException $e) { echo 'Caught: ' . $e->getMessage() . PHP_EOL; }
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.
<?php class DatabaseConnection { // Static property — belongs to the CLASS, shared across all instances private static int $connectionCount = 0; private static ?self $primaryInstance = null; // For the singleton pattern private string $dsn; private bool $isConnected = false; // Private constructor — forces use of the factory method below private function __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 place public static function create(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) public static function getPrimaryConnection(string $dsn): self { if (self::$primaryInstance === null) { self::$primaryInstance = new self($dsn); } return self::$primaryInstance; // Returns the SAME object on subsequent calls } public function connect(): 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 all public static function getTotalConnections(): 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 needed echo '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;
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 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.__clone()
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.
<?php class CartItem { public function __construct( public readonly string $sku, public int $quantity ) {} } class ShoppingCart { private array $items = []; private string $currency; public function __construct(string $currency = 'GBP') { $this->currency = $currency; } public function addItem(CartItem $item): void { // Store the item — note: this stores a REFERENCE to the CartItem object $this->items[$item->sku] = $item; } public function getItemCount(): int { return array_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 objects public function __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 = new ShoppingCart('GBP'); $originalCart->addItem(new CartItem('TSHIRT-RED-M', 2)); $originalCart->addItem(new CartItem('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(new CartItem('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 values echo 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!
__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.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.__clone() to deep-copy them.__clone() is your deep-copy hook.__clone() and deep-copy each object property__clone(), manually reassign those properties to the same referenceInheritance 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:: if the parent's constructor is defined and does important work.__construct()
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.
<?php abstract class Vehicle { protected string $make; protected string $model; protected int $year; public function __construct(string $make, string $model, int $year) { $this->make = $make; $this->model = $model; $this->year = $year; } abstract public function getFuelType(): string; public function getDescription(): string { return "{$this->year} {$this->make} {$this->model}"; } } class Car extends Vehicle { private int $doors; public function __construct(string $make, string $model, int $year, int $doors) { parent::__construct($make, $model, $year); $this->doors = $doors; } public function getFuelType(): string { return 'Petrol'; } public function getDoors(): int { return $this->doors; } } class ElectricCar extends Car { private int $batteryCapacity; // kWh public function __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 petrol public function getFuelType(): string { return 'Electric'; } public function getBatteryRange(): int { // Rough estimate: 6 km per kWh return $this->batteryCapacity * 6; } } $tesla = new ElectricCar('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
- 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.
__construct() if the parent has one.Interfaces: The Contract That Saves Your Weekend
Here's a truth that hits hard after a 3AM rollback: production doesn't care about your intents, only your contracts. PHP interfaces enforce a specific set of required methods across unrelated classes. No ambiguity, no "well, we thought it worked."
Why this matters: You define an interface when multiple classes must implement the same behavior, but they implement it differently. Your payment gateway, for example, might have a PayPalStrategy and a StripeStrategy. Both must implement charge(float $amount) and refund(string $transactionId). The interface ensures that when your boss says "add a new processor," you literally cannot forget those methods.
The contract is enforced at compile-time. If a class says implements PaymentGateway but doesn't define charge(), PHP throws a fatal error before your code reaches staging. Not a warning. Not a log. A hard stop.
Inheritance is about sharing implementation. Interfaces are about sharing capability. Use them when you need to guarantee behavior without dictating how it's done.
// io.thecodeforge interface PaymentGateway { public function charge(float $amount): string; // returns transaction ID public function refund(string $transactionId): bool; } class StripeStrategy implements PaymentGateway { public function charge(float $amount): string { // Stripe API call return 'txn_' . uniqid(); } public function refund(string $transactionId): bool { return true; // assume success } } // Forgetting charge() throws: // Fatal error: Class PayPalStrategy contains 1 abstract method class PayPalStrategy implements PaymentGateway { public function refund(string $transactionId): bool { return true; } }
Abstract Classes: When You Want Partial Answers
An abstract class is the middle ground between a concrete class and an interface. It contains some implemented methods and some placeholders (abstract methods) that child classes must fill. Think of it as a partially-written blueprint with critical gaps your team must complete.
Why reach for this? When you have shared logic across related classes but need to force specific implementations for certain behaviors. Your DataExporter base class might have the export() method fully written, but it requires getData() and formatFile() to be defined by each subclass (CSV, PDF, JSON). The abstract class handles the boilerplate; the subclass handles the unique parts.
In PHP 8.x, abstract classes can have typed properties, named arguments, and full constructor promotion. They're not legacy—they're tactical.
Important: You cannot instantiate an abstract class directly. If someone writes $exporter = new DataExporter(), PHP will throw a fatal error. That's the feature, not a bug. You're forcing your team to think about specialization before execution.
// io.thecodeforge abstract class DataExporter { protected array $data; public function __construct(array $data) { $this->data = $data; } // Shared logic, fully implemented public function export(): string { $formatted = $this->formatFile(); file_put_contents($this->getFilename(), $formatted); return $this->getFilename(); } // Must be implemented by child classes abstract protected function formatFile(): string; abstract protected function getFilename(): string; } class CsvExporter extends DataExporter { protected function formatFile(): string { $lines = []; foreach ($this->data as $row) { $lines[] = implode(',', $row); } return implode("\n", $lines); } protected function getFilename(): string { return 'export_' . date('Ymd') . '.csv'; } } $exporter = new CsvExporter([['name', 'email']]); echo $exporter->export();
Traits: Reuse Without Inheritance Hell
PHP single-inheritance model means a class can only extend one parent. That's a hard limit—your ReportGenerator can't extend both PdfRenderer and EmailSender. Traits are the escape hatch: they let you compose behavior into a class without constructing a fragile inheritance pyramid.
Think of a trait as a copy-paste that PHP manages for you. When a class uses a trait, PHP copies the trait's methods directly into the class at compile-time. No diamond problem, no fragile base class syndrome. Just reusable code that lives in its own file.
In PHP 8.x, traits support abstract methods, properties, and even other traits. Use them for cross-cutting concerns like logging, timestamp management, or caching logic that multiple unrelated classes need.
Watch the recall trap: If two traits define the same method, PHP throws a fatal error unless you resolve the conflict with insteadof or as. You'll discover this immediately in CI, not in production.
// io.thecodeforge trait LoggerTrait { private string $logFile; public function log(string $message): void { $timestamp = date('Y-m-d H:i:s'); file_put_contents( $this->logFile ?? '/var/log/app.log', "[$timestamp] $message" . PHP_EOL, FILE_APPEND ); } abstract public function getIdentifier(): string; // forces class to define } trait TimestampTrait { public function getTimestamp(): string { return date('c'); } } class OrderProcessor { use LoggerTrait, TimestampTrait; public function getIdentifier(): string { return 'order_' . uniqid(); } public function process(): void { $this->log('Processing started'); // ... $this->log('Processing completed at ' . $this->getTimestamp()); } } $processor = new OrderProcessor(); $processor->process();
Fatal Error: Using $this in Static Context Took Down a Deployment
- 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.
User() before calling methods). Review constructor logic — any condition that might skip assignment?__clone() method that deep-clones each nested object. Use debug_zval_refs() to check reference counts before and after clone.var_dump($object->properties) // See all current valuesprint_r(get_object_vars($object)) // List all accessible propertiesdebug_zval_refs($object1) // Shows reference countspl_object_id($object1) === spl_object_id($object2) ? 'same' : 'different'__clone() for deep copy if nested objects exist.php -l filename.php // Syntax check the filegrep -n 'static function' filename.php // List all static methods| 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 |
| File | Command / Code | Purpose |
|---|---|---|
| BankAccount.php | class BankAccount | Defining a Class |
| UserProfile.php | class UserProfile | Visibility Keywords |
| DatabaseConnection.php | class DatabaseConnection | Static Methods and Properties |
| ShoppingCart.php | class CartItem | Object Cloning and Comparison |
| VehicleInheritance.php | abstract class Vehicle | Inheritance in PHP |
| PaymentGatewayInterface.php | interface PaymentGateway | Interfaces |
| DataExporter.php | abstract class DataExporter | Abstract Classes |
| LoggerTrait.php | trait LoggerTrait | Traits |
Key takeaways
__clone() when the class holds nested objects.__construct(). Keep hierarchies shallowCommon mistakes to avoid
4 patternsForgetting that object assignment copies the reference, not the object
__clone() to deep-clone any nested object properties.Making all properties public to avoid writing getters
Using $this inside a static method
Forgetting to call parent::__construct() in a child class
Interview Questions on This Topic
What is the difference between a class and an object in PHP, and can you give a real-world analogy to illustrate it?
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.
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?
How does PHP handle object cloning? What is the difference between shallow and deep copy?
__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.Frequently Asked Questions
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.
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.
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 to deep-clone those nested objects, because the default shallow clone still shares them.__clone()
If the parent constructor does important work (e.g., setting properties, validating data, connecting to a database), you must call parent:: 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.__construct()
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?).
20+ years shipping production PHP systems at scale. Everything here is grounded in real deployments.
That's OOP in PHP. Mark it forged?
7 min read · try the examples if you haven't