Method Overriding
What Is Method Overriding? In simple terms: When a child class inherits a method but needs it to work differently, it can override it: it declares a method with the same name, and that version replace
What Is Method Overriding?
In simple terms: When a child class inherits a method but needs it to work differently, it can override it: it declares a method with the same name, and that version replaces the parent's version for objects of the child class. A sale product inherits everything from a product, but its price() method returns a discounted price.
Technically: A method in a child class with the same name as an inherited public or protected method overrides it. PHP decides at runtime which version to call based on the actual class of the object (dynamic dispatch). The overriding method must have a compatible signature, may not reduce visibility, and can call the original implementation with parent::methodName().
A First Example
<?php
class Product
{
public function __construct(protected string $name, protected float $basePrice) {}
public function price(): float
{
return $this->basePrice;
}
public function label(): string
{
return $this->name . ': ₹' . number_format($this->price(), 2);
}
}
class SaleProduct extends Product
{
public function __construct(string $name, float $basePrice, private float $discountPercent)
{
parent::__construct($name, $basePrice);
}
public function price(): float
{
return round(parent::price() * (1 - $this->discountPercent / 100), 2);
}
}
$products = [
new Product('Gel Pen', 20),
new SaleProduct('Desk Lamp', 1299, 20),
];
foreach ($products as $product) {
echo $product->label() . "\n";
}
Output
Gel Pen: ₹20.00
Desk Lamp: ₹1,039.20
How It Works
SaleProductoverridesprice(). For aSaleProductobject, every call toprice()uses the child's version.label()is not overridden. It is defined inProductand calls$this->price(). Because$thisis aSaleProductfor the lamp, the overridden method runs — the parent's code automatically uses the child's behavior.parent::price()calls the parent's implementation, so the child extends the original calculation instead of duplicating it.
This combination — a general method in the parent that calls methods the child can override — is a powerful way to customize behavior without copying code.
Extending vs Replacing
An override can replace the parent's behavior completely or extend it by calling parent:::
<?php
class Logger
{
public function log(string $message): void
{
echo "[LOG] $message\n";
}
}
class TimestampLogger extends Logger
{
public function log(string $message): void
{
parent::log('2026-10-15 10:00 ' . $message); // extend
}
}
class SilentLogger extends Logger
{
public function log(string $message): void
{
// replace: do nothing (useful in tests)
}
}
(new TimestampLogger())->log('Order 501 placed');
(new SilentLogger())->log('This is not printed');
echo "Done\n";
Output
[LOG] 2026-10-15 10:00 Order 501 placed
Done
parent:: can be called anywhere inside the overriding method — before, after, or in the middle of the child's own logic.
Signature Compatibility Rules
An overriding method must be usable wherever the parent method is used. This is the Liskov Substitution Principle: a child object must be able to replace a parent object without breaking the code that uses it. PHP enforces it with these rules:
| Aspect | Rule | Example |
|---|---|---|
| Visibility | Same or wider (protected → public is allowed; public → protected is not) | — |
| Number of parameters | May add parameters only if they are optional | log(string $m) → log(string $m, array $context = []) |
| Parameter types | Same or wider (contravariance) | int → int|float |
| Return type | Same or narrower (covariance) | object → Product, iterable → array |
| Static / non-static | Must match | — |
Example: Covariant Return Types
<?php
class Product
{
public function __construct(public string $name) {}
}
class ProductRepository
{
public function find(int $id): ?object
{
return null;
}
}
class CachedProductRepository extends ProductRepository
{
public function find(int $id): ?Product // narrower return type: allowed
{
return new Product('Gel Pen');
}
}
echo (new CachedProductRepository())->find(1)?->name . "\n";
Output
Gel Pen
The parent promises "an object or null"; the child promises something more specific — "a Product or null" — which still keeps the parent's promise.
Incompatible Overrides Are Fatal Errors
class Product
{
public function price(): float { return 20.0; }
}
class BrokenProduct extends Product
{
public function price(): string { return 'twenty'; }
}
PHP Fatal error: Declaration of BrokenProduct::price(): string must be compatible with Product::price(): float
Reducing visibility is also rejected:
PHP Fatal error: Access level to BrokenProduct::price() must be public (as in class Product)
These checks happen when the class is loaded, so incompatible overrides are discovered immediately, not when the method is called.
The #[\Override] Attribute (PHP 8.3+)
A typo in an overriding method's name silently creates a new method instead of overriding the old one. The #[\Override] attribute tells PHP that a method is meant to override a parent method:
<?php
class Report
{
protected function title(): string
{
return 'Report';
}
}
class SalesReport extends Report
{
#[\Override]
protected function titel(): string // typo
{
return 'Sales Report';
}
}
Output
PHP Fatal error: SalesReport::titel() has #[\Override] attribute, but no matching parent method exists
Without the attribute, the typo would go unnoticed: title() would still return "Report". The attribute also protects against a parent method being renamed or removed later. It works for methods from parent classes and from implemented interfaces.
Overriding the Constructor
Constructors are an exception to the signature rules: a child constructor may have completely different parameters (as SaleProduct above shows). Remember to call parent::__construct() when the parent needs initialization (see PHP Inheritance).
What Cannot Be Overridden
| Member | Reason |
|---|---|
final methods | Explicitly locked by the parent (see The final Keyword) |
private methods | Not visible to the child; a child method with the same name is a separate, unrelated method |
<?php
class ParentClass
{
public function run(): string
{
return $this->step();
}
private function step(): string
{
return 'parent step';
}
}
class ChildClass extends ParentClass
{
public function step(): string // a new method, not an override
{
return 'child step';
}
}
echo (new ChildClass())->run() . "\n";
Output
parent step
run() is defined in ParentClass and calls the private step() of ParentClass. The child's step() does not replace it. Use protected for methods that subclasses are meant to override.
Practical Example: Customizing a Notification Workflow
<?php
class Notification
{
public function __construct(protected string $recipient, protected string $message) {}
public function send(): string
{
return sprintf('[%s] to %s: %s', $this->channel(), $this->formatRecipient(), $this->formatMessage());
}
protected function channel(): string
{
return 'EMAIL';
}
protected function formatRecipient(): string
{
return $this->recipient;
}
protected function formatMessage(): string
{
return $this->message;
}
}
class SmsNotification extends Notification
{
#[\Override]
protected function channel(): string
{
return 'SMS';
}
#[\Override]
protected function formatRecipient(): string
{
return '+91 ' . substr($this->recipient, 0, 5) . ' ' . substr($this->recipient, 5);
}
#[\Override]
protected function formatMessage(): string
{
$message = parent::formatMessage();
return mb_strlen($message) > 40 ? mb_substr($message, 0, 37) . '...' : $message;
}
}
echo (new Notification('asha@example.com', 'Your order #501 has been shipped and will arrive on Friday.'))->send() . "\n";
echo (new SmsNotification('9876543210', 'Your order #501 has been shipped and will arrive on Friday.'))->send() . "\n";
Output
[EMAIL] to asha@example.com: Your order #501 has been shipped and will arrive on Friday.
[SMS] to +91 98765 43210: Your order #501 has been shipped and ...
Step-by-Step Explanation
send()defines the overall format once, in the parent.- The parent exposes three protected extension points:
channel(),formatRecipient(), andformatMessage(). SmsNotificationoverrides all three: a different channel name, phone number formatting, and message shortening for SMS length limits (the first 37 characters plus...). The shortening extends the parent's formatting viaparent::formatMessage().#[\Override]documents each override and guards against typos.
Common Mistakes
Misspelling the Method Name
Effect: A new method is created; the parent's version still runs.
Fix: Use #[\Override] (PHP 8.3+).
Changing the Signature Incompatibly
Effect: Fatal "must be compatible" errors.
Fix: Keep parameter types the same or wider, return types the same or narrower, and only add optional parameters.
Forgetting to Call the Parent Method
Effect: Important parent behavior (validation, logging, initialization) is lost.
Fix: Call parent::method() when extending rather than replacing.
Trying to Override Private Methods
Effect: The parent keeps calling its own private method.
Fix: Make intended extension points protected.
Overrides That Break Expectations
Effect: A child that throws exceptions where the parent never did, or returns surprising results, breaks code written for the parent.
Fix: Ensure overrides honor the parent method's contract.
Best Practices
- Override methods to specialize behavior while keeping the parent's contract.
- Use
parent::method()to extend instead of duplicating parent logic. - Mark overrides with
#[\Override]in PHP 8.3+. - Design parent classes with small,
protectedextension points. - Keep signatures compatible: wider parameters, narrower return types.
Official reference: https://www.php.net/manual/en/language.oop5.inheritance.php