Method Overriding in PHP: Customizing Inherited Behavior
Learning Objectives
- Master PHP programming concepts
- Write clean, maintainable code
- Apply best practices
- Build dynamic applications
Customizing Inherited Behavior
Welcome to our deep dive into method overriding in PHP! Method overriding is a fundamental concept in object-oriented programming that allows child classes to provide specialized implementations of methods that are already defined in their parent classes.
Why Method Overriding Matters: Method overriding is essential for creating flexible, extensible applications. In WordPress development, overriding methods allows you to customize themes and plugins without modifying core files, making your code more maintainable and update-resistant.
The Art of Method Overriding
Method overriding is like inheriting a family recipe and then adding your own twist to it. The original recipe (parent method) provides a foundation, but your version (overridden method) tailors it to your specific taste while keeping the core essence intact.
Basic Mechanism of Method Overriding
When a child class inherits from a parent class, it can provide its own implementation of a method that already exists in the parent class. When the method is called on an instance of the child class, PHP uses the child's version instead of the parent's.
Simple Method Overriding Example
// Parent class
class Vehicle {
protected $make;
protected $model;
public function __construct($make, $model) {
$this->make = $make;
$this->model = $model;
}
public function getInfo() {
return "Vehicle: {$this->make} {$this->model}";
}
public function start() {
return "The vehicle is starting the engine...";
}
}
// Child class
class Car extends Vehicle {
protected $doors;
public function __construct($make, $model, $doors) {
parent::__construct($make, $model);
$this->doors = $doors;
}
// Override the getInfo method
public function getInfo() {
return "Car: {$this->make} {$this->model} with {$this->doors} doors";
}
}
// Create instances
$vehicle = new Vehicle("Generic", "Transport");
$car = new Car("Toyota", "Corolla", 4);
// Output
echo $vehicle->getInfo(); // Output: "Vehicle: Generic Transport"
echo "
";
echo $car->getInfo(); // Output: "Car: Toyota Corolla with 4 doors"
echo "
";
echo $car->start(); // Output: "The vehicle is starting the engine..." (inherited, not overridden)
In this example, the Car class overrides the getInfo() method to include information about doors, but inherits the start() method unchanged from the Vehicle class.
The Recipe Analogy
Think of method overriding like cooking:
- The parent class (Grandmother) has a cookie recipe with specific ingredients and instructions.
- The child class (You) inherits this recipe but wants to modify it slightly – perhaps adding chocolate chips or changing the baking time.
- You keep the core essence of the recipe (method signature) but customize the implementation to suit your specific needs.
- Anyone asking you for cookies gets your version of the recipe, not your grandmother's original.
This analogy highlights how overriding preserves the method's contract while allowing customization of behavior.
Rules and Requirements for Method Overriding
Method overriding in PHP must follow several important rules to maintain consistency and expected behavior:
Method Overriding Rules
| Rule | Description | Example |
|---|---|---|
| Same Method Name | The method in the child class must have the exact same name as the method in the parent class. | getInfo() in both parent and child |
| Compatible Parameter Count | The overriding method should accept at least the same number of parameters as the parent method. | parent: process($data)child: process($data, $options = []) |
| Type Compatibility | Parameter types must be compatible (same or less specific) and return types must be compatible (same or more specific). | Parameter: parent accepts Animal, child accepts Animal or any parent type.Return: parent returns Animal, child returns Animal or any child type. |
| Access Modifier | The overriding method can have the same or less restrictive access modifier than the parent method. | If parent is protected, child can be protected or public, but not private. |
| Static Consistency | If the parent method is static, the child method must also be static, and vice versa. | Cannot override a non-static method with a static method. |
Visibility Rules in Detail
One of the most important rules involves visibility (access modifiers). The overriding method can have the same or a less restrictive visibility than the parent method, but it cannot be more restrictive:
Visibility Override Example
class ParentClass {
// Private method - only accessible within this class
private function privateMethod() {
return "This is private";
}
// Protected method - accessible within this class and child classes
protected function protectedMethod() {
return "This is protected";
}
// Public method - accessible from anywhere
public function publicMethod() {
return "This is public";
}
// Public method that uses the private method
public function usePrivateMethod() {
return $this->privateMethod();
}
}
class ChildClass extends ParentClass {
// Cannot override private methods (they're not inherited)
// This is a new method, not an override
private function privateMethod() {
return "Child private method";
}
// Override protected with protected (same visibility)
protected function protectedMethod() {
return "Child protected method";
}
// Override public with public (same visibility)
public function publicMethod() {
return "Child public method";
}
// This would cause an error - cannot make visibility more restrictive
// protected function publicMethod() {
// return "This will cause an error";
// }
// This is valid - making visibility less restrictive
public function protectedMethod2() {
// Call the parent's protected method
return "Public wrapper: " . parent::protectedMethod();
}
}
$child = new ChildClass();
echo $child->publicMethod(); // "Child public method"
echo $child->usePrivateMethod(); // "This is private" (uses parent's privateMethod)
echo $child->protectedMethod2(); // "Public wrapper: Child protected method"
Type Declarations and Covariance/Contravariance
Since PHP 7.4, method overriding supports return type covariance and parameter type contravariance. These concepts allow more flexibility when overriding methods:
- Return Type Covariance: The overriding method can return a more specific type than the parent method.
- Parameter Type Contravariance: The overriding method can accept a less specific type than the parent method.
// Example of return type covariance
class Animal {}
class Dog extends Animal {}
class AnimalShelter {
public function getAnimal(): Animal {
return new Animal();
}
}
class DogShelter extends AnimalShelter {
// Return type is more specific (covariance)
public function getAnimal(): Dog {
return new Dog();
}
}
// Example of parameter type contravariance
class Feed {
public function feedAnimal(Dog $animal) {
// Feed only dogs
}
}
class SuperFeed extends Feed {
// Parameter type is less specific (contravariance)
public function feedAnimal(Animal $animal) {
// Can feed any animal
}
}
Extending Parent Methods with parent::
Often, you don't want to completely replace the parent method's behavior, but rather extend it. The parent:: keyword lets you call the parent's version of a method from within the overriding method.
Using parent:: to Extend Functionality
class Logger {
public function log($message) {
$timestamp = date('Y-m-d H:i:s');
return "[{$timestamp}] {$message}";
}
}
class DetailedLogger extends Logger {
protected $logLevel;
public function __construct($logLevel = 'INFO') {
$this->logLevel = $logLevel;
}
// Override with extension, not replacement
public function log($message) {
// First, get the parent's log format
$parentLog = parent::log($message);
// Then enhance it with additional information
return "{$parentLog} [Level: {$this->logLevel}]";
}
}
// Usage
$logger = new Logger();
$detailedLogger = new DetailedLogger('WARNING');
echo $logger->log("System started");
// Output: "[2025-04-28 10:15:22] System started"
echo "
";
echo $detailedLogger->log("Low memory detected");
// Output: "[2025-04-28 10:15:22] Low memory detected [Level: WARNING]"
Common Use Cases for parent::
- Constructors: Calling the parent constructor before adding child-specific initialization
- Adding functionality: Extending the parent's method with additional behaviors
- Pre/post processing: Running code before or after the parent's implementation
- Filtering results: Modifying the return value of the parent's method
- Validation: Adding additional validation before calling the parent method
Constructor Overriding Example
class Product {
protected $name;
protected $price;
public function __construct($name, $price) {
$this->name = $name;
$this->price = $price;
}
public function getDetails() {
return [
'name' => $this->name,
'price' => $this->price
];
}
}
class DigitalProduct extends Product {
protected $downloadUrl;
protected $fileSize;
public function __construct($name, $price, $downloadUrl, $fileSize) {
// Call parent constructor first
parent::__construct($name, $price);
// Then add child-specific properties
$this->downloadUrl = $downloadUrl;
$this->fileSize = $fileSize;
}
public function getDetails() {
// Get the parent's details
$details = parent::getDetails();
// Add child-specific details
$details['download_url'] = $this->downloadUrl;
$details['file_size'] = $this->fileSize;
$details['type'] = 'digital';
return $details;
}
}
$ebook = new DigitalProduct(
'PHP Mastery',
29.99,
'https://example.com/downloads/php-mastery.pdf',
'15MB'
);
print_r($ebook->getDetails());
/* Output:
Array
(
[name] => PHP Mastery
[price] => 29.99
[download_url] => https://example.com/downloads/php-mastery.pdf
[file_size] => 15MB
[type] => digital
)
*/
Best Practices for Calling parent::
- Always call
parent::__construct()when overriding constructors - Call
parent::at the beginning of the method for pre-processing (validation) - Call
parent::at the end of the method for post-processing (augmenting results) - Be careful with conditional calls to
parent::as it can make code harder to follow - Document why you're overriding and what parent functionality you're preserving
Method Overriding in the Real World
Example 1: WordPress Widgets
WordPress widgets use method overriding extensively. Each widget type extends the WP_Widget base class and overrides specific methods to customize behavior.
Custom WordPress Widget Example
class WP_Widget {
protected $id_base;
protected $name;
protected $options;
public function __construct($id_base, $name, $options = []) {
$this->id_base = $id_base;
$this->name = $name;
$this->options = $options;
}
// Default widget output method
public function widget($args, $instance) {
echo "Default widget output";
}
// Default form for widget settings
public function form($instance) {
echo "Default form";
}
// Default method to save widget settings
public function update($new_instance, $old_instance) {
return $new_instance;
}
}
class Latest_Posts_Widget extends WP_Widget {
public function __construct() {
// Call parent constructor with specific widget details
parent::__construct(
'latest_posts',
'Latest Posts Widget',
['description' => 'Display the most recent posts']
);
}
// Override widget output
public function widget($args, $instance) {
// Extract arguments provided by WordPress
extract($args);
// Get the widget settings
$title = !empty($instance['title']) ? $instance['title'] : 'Latest Posts';
$count = !empty($instance['count']) ? (int) $instance['count'] : 5;
// Widget start (from theme)
echo $before_widget;
echo $before_title . $title . $after_title;
// Widget custom content
echo "";
// Widget end (from theme)
echo $after_widget;
}
// Override settings form
public function form($instance) {
// Get saved settings or defaults
$title = !empty($instance['title']) ? $instance['title'] : 'Latest Posts';
$count = !empty($instance['count']) ? (int) $instance['count'] : 5;
// Output form fields
echo "";
echo "";
echo "";
echo "
";
echo "";
echo "";
echo "";
echo "
";
}
// Override update method
public function update($new_instance, $old_instance) {
// Start with parent's update method
$instance = parent::update($new_instance, $old_instance);
// Add our custom sanitization
$instance['title'] = sanitize_text_field($new_instance['title']);
$instance['count'] = max(1, min(10, (int) $new_instance['count']));
return $instance;
}
}
Example 2: WooCommerce Product Types
WooCommerce, the popular e-commerce plugin for WordPress, uses method overriding to handle different product types with varying pricing, shipping, and tax calculations.
Simplified WooCommerce Product Classes
abstract class WC_Product {
protected $id;
protected $name;
protected $price;
protected $stock_quantity;
public function __construct($id, $name, $price, $stock_quantity) {
$this->id = $id;
$this->name = $name;
$this->price = $price;
$this->stock_quantity = $stock_quantity;
}
// Base methods
public function get_price() {
return $this->price;
}
public function is_in_stock() {
return $this->stock_quantity > 0;
}
// Abstract method all product types must implement
abstract public function calculate_tax($tax_rate);
}
// Simple physical product
class WC_Product_Simple extends WC_Product {
protected $weight;
protected $dimensions;
public function __construct($id, $name, $price, $stock_quantity, $weight, $dimensions) {
parent::__construct($id, $name, $price, $stock_quantity);
$this->weight = $weight;
$this->dimensions = $dimensions;
}
// Override to include tax
public function get_price() {
$base_price = parent::get_price();
return $base_price + $this->calculate_tax(0.1); // 10% tax rate
}
public function calculate_tax($tax_rate) {
return $this->price * $tax_rate;
}
// Shipping-specific method
public function get_shipping_cost($zone) {
return $this->weight * $zone->rate_per_kg;
}
}
// Digital product (no shipping)
class WC_Product_Digital extends WC_Product {
protected $download_url;
protected $file_size;
public function __construct($id, $name, $price, $stock_quantity, $download_url, $file_size) {
parent::__construct($id, $name, $price, $stock_quantity);
$this->download_url = $download_url;
$this->file_size = $file_size;
}
// Digital products are always in stock
public function is_in_stock() {
return true;
}
// Different tax calculation for digital goods
public function calculate_tax($tax_rate) {
return $this->price * ($tax_rate * 0.5); // Half tax rate for digital goods
}
// Digital-specific method
public function get_download_link($order_id) {
return $this->download_url . "?order={$order_id}&time=" . time();
}
}
// Usage
$physical = new WC_Product_Simple(1, 'Coffee Mug', 14.99, 10, 0.5, ['height' => 10, 'width' => 8, 'depth' => 8]);
$digital = new WC_Product_Digital(2, 'E-book', 9.99, 0, 'https://example.com/downloads/ebook.pdf', '2.5MB');
echo "Physical product price: $" . $physical->get_price() . "
"; // Includes tax
echo "Digital product price: $" . $digital->get_price() . "
";
echo "Physical product in stock: " . ($physical->is_in_stock() ? 'Yes' : 'No') . "
";
echo "Digital product in stock: " . ($digital->is_in_stock() ? 'Yes' : 'No') . "
"; // Always Yes
Example 3: WordPress Theme Customizer
WordPress theme customization relies heavily on method overriding to customize how different control types are rendered.
WordPress Customizer Control Classes
class WP_Customize_Control {
protected $id;
protected $label;
protected $type;
protected $settings;
public function __construct($id, $label, $settings, $args = []) {
$this->id = $id;
$this->label = $label;
$this->settings = $settings;
$this->type = isset($args['type']) ? $args['type'] : 'text';
}
// Render the control's content
public function render_content() {
echo "{$this->label}";
echo "";
}
}
// Color picker control
class WP_Customize_Color_Control extends WP_Customize_Control {
// Override to render a color picker instead of a text input
public function render_content() {
// Call parent for the label
parent::render_content();
// Replace input with color picker
echo "";
echo "Select a color";
}
}
// Image upload control
class WP_Customize_Image_Control extends WP_Customize_Control {
// Completely override the render method
public function render_content() {
echo "{$this->label}";
echo "";
echo "
";
echo "";
echo "";
echo "";
echo "";
}
}
// Usage in a theme
function theme_customizer_setup($wp_customize) {
// Add a text setting
$wp_customize->add_setting('site_title_color', [
'default' => '#000000',
'transport' => 'refresh',
]);
// Add a color control
$wp_customize->add_control(new WP_Customize_Color_Control(
'site_title_color',
'Site Title Color',
'site_title_color',
['section' => 'title_tagline']
));
// Add an image setting
$wp_customize->add_setting('header_image', [
'default' => '',
'transport' => 'refresh',
]);
// Add an image control
$wp_customize->add_control(new WP_Customize_Image_Control(
'header_image',
'Header Image',
'header_image',
['section' => 'header_settings']
));
}
Advanced Topics in Method Overriding
Final Methods: Preventing Overrides
Sometimes, you want to prevent a method from being overridden in child classes. PHP provides the final keyword for this purpose.
Final Method Example
class PaymentProcessor {
public function processPayment($amount) {
$this->validateAmount($amount);
$this->logTransaction($amount);
return $this->sendPaymentRequest($amount);
}
// This method can be overridden
protected function validateAmount($amount) {
if ($amount <= 0) {
throw new Exception("Amount must be positive");
}
}
// Critical security method - cannot be overridden
final protected function logTransaction($amount) {
// Secure logging implementation
$timestamp = date('Y-m-d H:i:s');
$transaction_id = uniqid('txn_');
echo "LOGGED: [{$timestamp}] Transaction {$transaction_id} for \${$amount} initiated.
";
}
// This method should be overridden by specific payment providers
protected function sendPaymentRequest($amount) {
throw new Exception("Payment method not implemented");
}
}
class StripePaymentProcessor extends PaymentProcessor {
// Override validation to add Stripe-specific rules
protected function validateAmount($amount) {
// Call parent validation first
parent::validateAmount($amount);
// Add Stripe-specific validation
if ($amount < 0.50) {
throw new Exception("Stripe requires a minimum charge of $0.50");
}
}
// Cannot override final method
// This would cause a fatal error
// protected function logTransaction($amount) {
// // Custom implementation
// }
// Implement required method
protected function sendPaymentRequest($amount) {
// Stripe-specific implementation
echo "Processing \${$amount} payment through Stripe.
";
return "stripe_" . uniqid();
}
}
// Usage
$processor = new StripePaymentProcessor();
$processor->processPayment(25.99);
Private Methods and Overriding
It's important to understand that private methods are not inherited by child classes, so they cannot be overridden in the traditional sense. If a child class defines a method with the same name as a private method in the parent, it's creating a new method, not overriding the parent's method.
Private Methods vs. Overriding
class ParentClass {
public function publicMethod() {
echo "Public method in parent
";
$this->privateMethod();
}
private function privateMethod() {
echo "Private method in parent
";
}
}
class ChildClass extends ParentClass {
// This is NOT overriding - it's a completely separate method
private function privateMethod() {
echo "Private method in child
";
}
}
$parent = new ParentClass();
$child = new ChildClass();
$parent->publicMethod();
// Output:
// Public method in parent
// Private method in parent
$child->publicMethod();
// Output:
// Public method in parent
// Private method in parent (not child!)
Notice that when $child->publicMethod() is called, it still uses the parent's private method, not the child's. This is because the child's method with the same name is a completely different method that is not accessible to the inherited public method.
Design Considerations
- Use
finalmethods when:- A method implements critical security functionality
- Overriding would break the class's internal logic
- You want to enforce a specific algorithm
- Use
privatemethods when:- The functionality is purely internal to the class
- The method should not be part of the class's API
- You want to prevent any form of external access or modification
- Use
protectedmethods when:- You want child classes to be able to override behavior
- The method is part of the inheritance API but not the public API
- You're designing extension points for child classes
Common Method Overriding Patterns
Method overriding is used in several common design patterns. Understanding these patterns can help you structure your code more effectively.
The Template Method Pattern
The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It lets subclasses redefine certain steps without changing the algorithm's structure.
Template Method Pattern Example
abstract class ReportGenerator {
// The template method defines the algorithm structure
public function generateReport($data) {
$this->validateData($data);
$processedData = $this->processData($data);
$formattedReport = $this->formatReport($processedData);
$this->deliverReport($formattedReport);
return $formattedReport;
}
// Common validation logic
protected function validateData($data) {
if (empty($data)) {
throw new Exception("Data cannot be empty");
}
}
// Abstract methods that must be implemented by subclasses
abstract protected function processData($data);
abstract protected function formatReport($data);
// Hook method with default implementation
protected function deliverReport($report) {
echo "Report generated and ready for delivery.
";
}
}
class PDFReportGenerator extends ReportGenerator {
protected function processData($data) {
echo "Processing data for PDF report...
";
// PDF-specific data processing
return array_map(function($item) {
return strtoupper($item); // Convert to uppercase for PDF
}, $data);
}
protected function formatReport($data) {
echo "Formatting as PDF...
";
// PDF-specific formatting
return "PDF_REPORT: " . implode(", ", $data);
}
// Override hook method
protected function deliverReport($report) {
parent::deliverReport($report);
echo "Saving PDF to disk...
";
}
}
class CSVReportGenerator extends ReportGenerator {
protected function processData($data) {
echo "Processing data for CSV report...
";
// CSV-specific data processing
return $data; // No special processing for CSV
}
protected function formatReport($data) {
echo "Formatting as CSV...
";
// CSV-specific formatting
return "CSV_REPORT: " . implode(",", $data);
}
// Override hook method
protected function deliverReport($report) {
parent::deliverReport($report);
echo "Emailing CSV report...
";
}
}
// Usage
$data = ['sales' => 1000, 'expenses' => 700, 'profit' => 300];
$pdfGenerator = new PDFReportGenerator();
$pdfReport = $pdfGenerator->generateReport($data);
echo "Result: {$pdfReport}
";
$csvGenerator = new CSVReportGenerator();
$csvReport = $csvGenerator->generateReport($data);
echo "Result: {$csvReport}
";
The Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
Strategy Pattern Example
// Strategy interface
interface PaymentStrategy {
public function pay($amount);
public function getPaymentDetails();
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
private $cardNumber;
private $expiryDate;
private $cvv;
public function __construct($cardNumber, $expiryDate, $cvv) {
$this->cardNumber = $cardNumber;
$this->expiryDate = $expiryDate;
$this->cvv = $cvv;
}
public function pay($amount) {
// Credit card payment processing logic
echo "Processing credit card payment of ${$amount}...
";
return "cc_transaction_" . rand(10000, 99999);
}
public function getPaymentDetails() {
return "Credit Card: " . substr($this->cardNumber, -4);
}
}
class PayPalPayment implements PaymentStrategy {
private $email;
private $password;
public function __construct($email, $password) {
$this->email = $email;
$this->password = $password;
}
public function pay($amount) {
// PayPal payment processing logic
echo "Processing PayPal payment of ${$amount}...
";
return "pp_transaction_" . rand(10000, 99999);
}
public function getPaymentDetails() {
return "PayPal: " . $this->email;
}
}
// Context class
class ShoppingCart {
private $paymentStrategy;
private $items = [];
public function setPaymentStrategy(PaymentStrategy $paymentStrategy) {
$this->paymentStrategy = $paymentStrategy;
}
public function addItem($item, $price) {
$this->items[$item] = $price;
}
public function calculateTotal() {
return array_sum($this->items);
}
public function checkout() {
$amount = $this->calculateTotal();
if (!$this->paymentStrategy) {
throw new Exception("Payment method not set");
}
echo "Checking out with " . $this->paymentStrategy->getPaymentDetails() . "
";
echo "Cart total: ${$amount}
";
$transactionId = $this->paymentStrategy->pay($amount);
echo "Transaction complete: {$transactionId}
";
}
}
// Usage
$cart = new ShoppingCart();
$cart->addItem("PHP Book", 29.99);
$cart->addItem("Hosting (1 month)", 9.99);
// Pay with credit card
$cart->setPaymentStrategy(new CreditCardPayment("1234567890123456", "12/26", "123"));
$cart->checkout();
echo "
";
// Pay with PayPal
$cart->setPaymentStrategy(new PayPalPayment("user@example.com", "password"));
$cart->checkout();
The Factory Method Pattern
The Factory Method pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. It lets a class defer instantiation to subclasses.
Factory Method Pattern Example
// Product interface
interface Logger {
public function log($message);
}
// Concrete products
class FileLogger implements Logger {
private $filePath;
public function __construct($filePath) {
$this->filePath = $filePath;
echo "Creating FileLogger to {$filePath}
";
}
public function log($message) {
echo "FileLogger: Writing message to {$this->filePath}: {$message}
";
}
}
class DatabaseLogger implements Logger {
private $connection;
public function __construct($connection) {
$this->connection = $connection;
echo "Creating DatabaseLogger with connection {$connection}
";
}
public function log($message) {
echo "DatabaseLogger: Saving message to database ({$this->connection}): {$message}
";
}
}
// Creator (abstract factory)
abstract class LoggerFactory {
// The factory method
abstract protected function createLogger();
// Template method that uses the factory method
public function logMessage($message) {
$logger = $this->createLogger();
$logger->log($message);
}
}
// Concrete creators
class FileLoggerFactory extends LoggerFactory {
protected function createLogger() {
return new FileLogger("/var/log/app.log");
}
}
class DatabaseLoggerFactory extends LoggerFactory {
protected function createLogger() {
return new DatabaseLogger("mysql://localhost/logs");
}
}
// Client code
function clientCode(LoggerFactory $factory) {
$factory->logMessage("System started");
$factory->logMessage("User logged in");
}
// Usage
echo "Using FileLoggerFactory:
";
clientCode(new FileLoggerFactory());
echo "
Using DatabaseLoggerFactory:
";
clientCode(new DatabaseLoggerFactory());
Best Practices for Method Overriding
Follow the Liskov Substitution Principle
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. When overriding methods, make sure your subclass behavior doesn't violate the expectations set by the parent class.
Bad Example
class Rectangle {
protected $width;
protected $height;
public function setWidth($width) {
$this->width = $width;
}
public function setHeight($height) {
$this->height = $height;
}
public function getArea() {
return $this->width * $this->height;
}
}
// Violates Liskov Substitution Principle
class Square extends Rectangle {
// Override to maintain square properties
public function setWidth($width) {
$this->width = $width;
$this->height = $width; // Side effect!
}
// Override to maintain square properties
public function setHeight($height) {
$this->width = $height; // Side effect!
$this->height = $height;
}
}
// This code breaks with Square
function resizeRectangle(Rectangle $rectangle) {
$rectangle->setWidth(5);
$rectangle->setHeight(4);
// With Rectangle: 5 * 4 = 20
// With Square: 4 * 4 = 16 (Unexpected!)
return $rectangle->getArea();
}
$rectangle = new Rectangle();
$area1 = resizeRectangle($rectangle);
echo "Rectangle area: {$area1}
"; // 20
$square = new Square();
$area2 = resizeRectangle($square);
echo "Square area: {$area2}
"; // 16 (Unexpected!)
Document Your Overrides
Always use PHPDoc comments to document your method overrides. This helps other developers understand your intentions and any differences from the parent method.
class ParentClass {
/**
* Process the given data.
*
* @param array $data The data to process
* @return array Processed data
*/
public function processData(array $data) {
// Processing logic
return $data;
}
}
class ChildClass extends ParentClass {
/**
* {@inheritdoc}
*
* Overrides parent method to add additional validation
* and format the output as JSON strings.
*
* @param array $data The data to process
* @return array Processed data with JSON-encoded values
*/
public function processData(array $data) {
// Call parent implementation
$processed = parent::processData($data);
// Add additional processing
foreach ($processed as &$value) {
$value = json_encode($value);
}
return $processed;
}
}
Be Consistent with Method Signatures
Keep method signatures consistent to avoid unexpected behavior. Use type declarations to enforce type compatibility.
class ParentClass {
/**
* Find a user by ID.
*
* @param int $id User ID
* @return array|null User data or null if not found
*/
public function findUser(int $id): ?array {
// Implementation
}
}
class ChildClass extends ParentClass {
/**
* {@inheritdoc}
*/
public function findUser(int $id): ?array {
// Implementation with same signature
}
}
Consider Performance Implications
Method overriding can have performance implications, especially when calling parent methods. Be mindful of performance-critical sections.
Performance-Aware Overriding
class CacheableContent {
protected $cache = [];
public function getData($key) {
if (isset($this->cache[$key])) {
return $this->cache[$key];
}
$data = $this->fetchData($key);
$this->cache[$key] = $data;
return $data;
}
protected function fetchData($key) {
// Expensive operation
sleep(1); // Simulate slow operation
return "Original data for {$key}";
}
}
class EnhancedCacheableContent extends CacheableContent {
private $enhancedCache = [];
// Override with enhanced caching
public function getData($key) {
// Enhanced caching layer
$cacheKey = "enhanced_{$key}";
if (isset($this->enhancedCache[$cacheKey])) {
return $this->enhancedCache[$cacheKey];
}
// Don't call parent::getData() which would use another cache layer
// Instead, call fetchData directly if needed
if (!isset($this->cache[$key])) {
$this->cache[$key] = $this->fetchData($key);
}
// Enhance the data
$enhancedData = $this->enhance($this->cache[$key]);
$this->enhancedCache[$cacheKey] = $enhancedData;
return $enhancedData;
}
protected function enhance($data) {
return "Enhanced: " . $data;
}
}
Use Method Overriding for Extension, Not Modification
Method overriding is best used to extend functionality, not to drastically change behavior. If you need to change behavior substantially, consider composition over inheritance.
Good Use of Overriding
class BasicAuthenticator {
public function authenticate($username, $password) {
// Basic validation
if (empty($username) || empty($password)) {
return false;
}
// Check credentials
$valid = $this->checkCredentials($username, $password);
if ($valid) {
$this->logSuccess($username);
} else {
$this->logFailure($username);
}
return $valid;
}
protected function checkCredentials($username, $password) {
// Simple check (in a real app, this would check a database)
return ($username === 'admin' && $password === 'password');
}
protected function logSuccess($username) {
echo "User {$username} logged in successfully
";
}
protected function logFailure($username) {
echo "Failed login attempt for user {$username}
";
}
}
class EnhancedAuthenticator extends BasicAuthenticator {
private $maxAttempts = 3;
private $attemptCount = [];
// Override to add rate limiting
protected function logFailure($username) {
parent::logFailure($username);
// Add rate limiting logic
if (!isset($this->attemptCount[$username])) {
$this->attemptCount[$username] = 0;
}
$this->attemptCount[$username]++;
if ($this->attemptCount[$username] >= $this->maxAttempts) {
echo "Account {$username} locked due to too many failed attempts
";
}
}
// Override to add secure credential checking
protected function checkCredentials($username, $password) {
// Check if account is locked
if (isset($this->attemptCount[$username]) &&
$this->attemptCount[$username] >= $this->maxAttempts) {
return false;
}
// Use parent method for actual credential checking
return parent::checkCredentials($username, $password);
}
}
// Usage
$auth = new EnhancedAuthenticator();
// Test with wrong password
$auth->authenticate('admin', 'wrong1');
$auth->authenticate('admin', 'wrong2');
$auth->authenticate('admin', 'wrong3');
// This attempt fails because account is locked
$auth->authenticate('admin', 'password');
Homework: Extend Your Previous Class with Inheritance
Assignment: Method Overriding Practice
For this assignment, you'll take the class you created in the previous session and extend it with proper method overriding. The goal is to practice creating child classes that override parent methods in various ways.
Requirements:
- Start with the simple class you created in the previous homework, or use the example class below.
- Create at least two child classes that extend your original class.
- In each child class, override at least two methods from the parent class.
- Use different overriding approaches:
- Complete replacement (new implementation)
- Extension with parent:: call at the beginning
- Extension with parent:: call at the end
- Conditional parent:: call
- Include proper documentation (PHPDoc comments) for each overridden method.
- Create a test script that demonstrates the different behaviors of the parent and child classes.
Example Starting Class:
/**
* Product class representing a basic product in an e-commerce system.
*/
class Product {
protected $name;
protected $price;
protected $sku;
protected $description;
/**
* Create a new product instance.
*
* @param string $name Product name
* @param float $price Product price
* @param string $sku Stock keeping unit
* @param string $description Product description
*/
public function __construct($name, $price, $sku, $description = '') {
$this->name = $name;
$this->price = $price;
$this->sku = $sku;
$this->description = $description;
}
/**
* Get the product details as an array.
*
* @return array
*/
public function getDetails() {
return [
'name' => $this->name,
'price' => $this->price,
'sku' => $this->sku,
'description' => $this->description
];
}
/**
* Calculate the final price including tax.
*
* @param float $taxRate The tax rate (0.1 for 10%)
* @return float
*/
public function calculatePriceWithTax($taxRate = 0.1) {
return $this->price * (1 + $taxRate);
}
/**
* Get formatted product information.
*
* @return string
*/
public function getFormattedInfo() {
return "Product: {$this->name} (SKU: {$this->sku}) - \${$this->price}";
}
/**
* Check if the product is in a specific price range.
*
* @param float $minPrice Minimum price
* @param float $maxPrice Maximum price
* @return bool
*/
public function isInPriceRange($minPrice, $maxPrice) {
return $this->price >= $minPrice && $this->price <= $maxPrice;
}
}
Starter Code for Child Classes:
/**
* DigitalProduct extends the basic Product with digital-specific features.
*/
class DigitalProduct extends Product {
protected $downloadUrl;
protected $fileSizeInMB;
/**
* Create a new digital product instance.
*
* @param string $name Product name
* @param float $price Product price
* @param string $sku Stock keeping unit
* @param string $description Product description
* @param string $downloadUrl Download URL
* @param float $fileSizeInMB File size in megabytes
*/
public function __construct($name, $price, $sku, $description, $downloadUrl, $fileSizeInMB) {
// Call parent constructor
parent::__construct($name, $price, $sku, $description);
// Set digital-specific properties
$this->downloadUrl = $downloadUrl;
$this->fileSizeInMB = $fileSizeInMB;
}
/**
* {@inheritdoc}
*
* Override to include digital product specific details.
*/
public function getDetails() {
// TODO: Override this method to add digital-specific details
}
/**
* {@inheritdoc}
*
* Override to use a different tax rate for digital products.
*/
public function calculatePriceWithTax($taxRate = 0.05) {
// TODO: Override this method to use digital-specific tax calculation
}
}
/**
* PhysicalProduct extends the basic Product with physical-specific features.
*/
class PhysicalProduct extends Product {
protected $weight;
protected $dimensions;
protected $inStock;
/**
* Create a new physical product instance.
*
* @param string $name Product name
* @param float $price Product price
* @param string $sku Stock keeping unit
* @param string $description Product description
* @param float $weight Weight in kg
* @param array $dimensions Array with width, height, depth
* @param int $inStock Number of items in stock
*/
public function __construct($name, $price, $sku, $description, $weight, $dimensions, $inStock) {
// TODO: Call parent constructor
// TODO: Set physical-specific properties
}
/**
* {@inheritdoc}
*
* Override to include physical product specific details.
*/
public function getFormattedInfo() {
// TODO: Override this method to add physical-specific formatting
}
/**
* {@inheritdoc}
*
* Override to also check stock availability.
*/
public function isInPriceRange($minPrice, $maxPrice) {
// TODO: Override this method to include stock check
}
/**
* Calculate shipping cost based on weight and distance.
*
* @param float $distance Shipping distance in miles
* @return float
*/
public function calculateShippingCost($distance) {
// Base rate + weight factor + distance factor
return 5 + ($this->weight * 2) + ($distance * 0.1);
}
}
Test Script Template:
// Include your class files
// require_once 'Product.php';
// require_once 'DigitalProduct.php';
// require_once 'PhysicalProduct.php';
// Create instances
$basicProduct = new Product('Basic Widget', 19.99, 'BW-001', 'A simple widget');
$ebook = new DigitalProduct('PHP Mastery', 29.99, 'E-BOOK-123', 'Learn PHP from scratch',
'https://example.com/downloads/php-mastery.pdf', 15.5);
$chair = new PhysicalProduct('Office Chair', 149.99, 'CHAIR-456', 'Ergonomic office chair',
12.5, ['width' => 60, 'height' => 120, 'depth' => 65], 5);
// Test method overriding
echo "Product Details Comparison:
";
echo "";
print_r($basicProduct->getDetails());
echo "
";
print_r($ebook->getDetails());
echo "
";
print_r($chair->getDetails());
echo "
";
echo "Price with Tax Comparison:
";
echo "Basic product: $" . number_format($basicProduct->calculatePriceWithTax(), 2) . "
";
echo "Digital product: $" . number_format($ebook->calculatePriceWithTax(), 2) . "
";
echo "Physical product: $" . number_format($chair->calculatePriceWithTax(), 2) . "
";
echo "Formatted Info Comparison:
";
echo $basicProduct->getFormattedInfo() . "
";
echo $ebook->getFormattedInfo() . "
";
echo $chair->getFormattedInfo() . "
";
echo "Price Range Check Comparison:
";
$minPrice = 20;
$maxPrice = 100;
echo "Basic product in range {$minPrice}-{$maxPrice}: " .
($basicProduct->isInPriceRange($minPrice, $maxPrice) ? 'Yes' : 'No') . "
";
echo "Digital product in range {$minPrice}-{$maxPrice}: " .
($ebook->isInPriceRange($minPrice, $maxPrice) ? 'Yes' : 'No') . "
";
echo "Physical product in range {$minPrice}-{$maxPrice}: " .
($chair->isInPriceRange($minPrice, $maxPrice) ? 'Yes' : 'No') . "
";
// Test product-specific methods
echo "Product-Specific Methods:
";
echo "Physical product shipping cost (100 miles): $" .
number_format($chair->calculateShippingCost(100), 2) . "
";
Bonus Challenges:
- Add a third child class
SubscriptionProductwith a recurring billing feature - Add a method to the parent class that calculates a discount, then override it in each child class with different discount strategies
- Add error checking and exception handling to the overridden methods
- Implement a
finalmethod in the parent class and explain why it shouldn't be overridden
Key Takeaways
Method Overriding Fundamentals
Method overriding allows child classes to provide specific implementations of methods defined in parent classes, customizing inherited behavior while maintaining the same method signature.
Rules and Requirements
Overriding methods must follow specific rules: same name, compatible parameters, compatible return types, and visibility that is the same or less restrictive than the parent method.
parent:: Keyword
The parent:: keyword allows child classes to extend parent behavior rather than completely replace it, enabling you to call the parent method and then add additional functionality.
Design Patterns
Method overriding is a key component in several design patterns like Template Method, Strategy, and Factory Method, enabling flexible and extensible code structures.
Best Practices
Follow the Liskov Substitution Principle, document your overrides with PHPDoc comments, maintain consistent method signatures, and use overriding for extension rather than drastic modification.