Skip to content
beginner Phase 5 · PHP OOP

PHP Inheritance: Single Inheritance and Method Overriding

Master PHP inheritance, method overriding, parent:: calls, Liskov Substitution Principle, and Magento's class hierarchy.

1h
0 problems
Topic Progress 0%

Single Inheritance and Method Overriding

Basic Inheritance

<?php
namespace Vendor\Module\Model;

// Parent class
class Product
{
    public function __construct(
        protected string $name,
        protected float $price
    ) {}

    public function getName(): string
    {
        return $this->name;
    }

    public function getPrice(): float
    {
        return $this->price;
    }

    public function getFormattedPrice(): string
    {
        return '\$' . number_format($this->price, 2);
    }

    public function toArray(): array
    {
        return [
            'name' => $this->name,
            'price' => $this->price
        ];
    }
}

// Child class
class DigitalProduct extends Product
{
    public function __construct(
        string $name,
        float $price,
        private string $downloadUrl
    ) {
        parent::__construct($name, $price);  // Call parent constructor
    }

    // Override parent method
    public function getFormattedPrice(): string
    {
        return parent::getFormattedPrice() . ' (digital)';
    }

    // Override toArray to add own data
    public function toArray(): array
    {
        return array_merge(parent::toArray(), [
            'download_url' => $this->downloadUrl
        ]);
    }

    // New method specific to DigitalProduct
    public function getDownloadUrl(): string
    {
        return $this->downloadUrl;
    }
}

// Usage
$product = new DigitalProduct('E-Book', 9.99, 'https://example.com/download/123');
echo $product->getName();           // 'E-Book' (inherited)
echo $product->getFormattedPrice(); // '$9.99 (digital)' (overridden)
echo $product->getDownloadUrl();    // 'https://...' (own method)
print_r($product->toArray());       // ['name' => 'E-Book', 'price' => 9.99, 'download_url' => '...']

parent:: Keyword

<?php
class Logger
{
    public function log(string $message): void
    {
        echo "LOG: $message\n";
    }
}

class DatabaseLogger extends Logger
{
    public function log(string $message): void
    {
        parent::log($message);  // Call parent's log method first
        $this->saveToDb($message);  // Then add own behavior
    }

    private function saveToDb(string $message): void
    {
        echo "Saved to database: $message\n";
    }
}

class FileLogger extends Logger
{
    public function log(string $message): void
    {
        parent::log($message);
        file_put_contents('/var/log/app.log', $message . "\n", FILE_APPEND);
    }
}

// Chain of responsibility
$dbLogger = new DatabaseLogger();
$dbLogger->log('User login');
// Output:
// LOG: User login
// Saved to database: User login

Protected vs Public Methods

<?php
class AbstractBlock
{
    // Protected - can be overridden by children
    protected function _toHtml(): string
    {
        return '<div>' . $this->renderContent() . '</div>';
    }

    // Private - cannot be overridden (not visible to children)
    private function escapeHtml(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }

    // Public - final API
    public function toHtml(): string
    {
        return $this->_toHtml();
    }
}

class CustomBlock extends AbstractBlock
{
    protected function _toHtml(): string
    {
        return '<section>' . parent::_toHtml() . '</section>';
    }

    // WRONG: Can't override private method
    // private function escapeHtml(): string {} // Fatal error if parent has private
}

Key Takeaway

PHP supports single inheritance (extends). Use parent:: to call overridden methods. Protected methods can be overridden; private methods cannot. Always call parent constructor when overriding __construct.

Liskov Substitution Principle (LSP)

What is LSP?

LSP states that objects of a child class should be usable in place of objects of the parent class without breaking the program.

<?php
// LSP COMPLIANT
class Rectangle
{
    public function __construct(
        protected float $width,
        protected float $height
    ) {}

    public function area(): float
    {
        return $this->width * $this->height;
    }

    public function getWidth(): float
    {
        return $this->width;
    }

    public function getHeight(): float
    {
        return $this->height;
    }
}

class Square extends Rectangle
{
    public function __construct(float $side)
    {
        parent::__construct($side, $side);
    }

    // Square maintains the invariant: width === height
    public function setWidth(float $width): void
    {
        $this->width = $width;
        $this->height = $width;  // Keep square shape
    }

    public function setHeight(float $height): void
    {
        $this->width = $height;  // Keep square shape
        $this->height = $height;
    }
}

// This function works with any Rectangle (including Square)
function printArea(Rectangle $rect): void
{
    echo "Area: " . $rect->area() . "\n";
}

$rect = new Rectangle(5, 10);
printArea($rect);  // Area: 50

$square = new Square(7);
printArea($square);  // Area: 49 (works correctly)

LSP Violation Example

<?php
// LSP VIOLATION - Don't do this!
class Bird
{
    public function fly(): string
    {
        return 'Flying';
    }
}

class Penguin extends Bird
{
    // Violates LSP - penguins can't fly!
    public function fly(): string
    {
        throw new \Exception('Penguins cannot fly');
    }
}

// This breaks when using Penguin
function makeBirdFly(Bird $bird): void
{
    echo $bird->fly();  // Works for Bird, breaks for Penguin
}

// BETTER: Use separate interfaces
interface Flyable
{
    public function fly(): string;
}

interface Swimmable
{
    public function swim(): string;
}

class Eagle implements Flyable
{
    public function fly(): string { return 'Flying high'; }
}

class Penguin implements Swimmable
{
    public function swim(): string { return 'Swimming deep'; }
}

Magento Inheritance Hierarchy

Object (Magento\Framework\Object)
  |
  +-- AbstractModel (Magento\Framework\Model\AbstractModel)
        |
        +-- Product (Magento\Catalog\Model\Product)
        +-- Order (Magento\Sales\Model\Order)
        +-- Category (Magento\Catalog\Model\Category)

AbstractBlock (Magento\Framework\View\Element\AbstractBlock)
  |
  +-- Template (Magento\Framework\View\Element\Template)
  +-- Text (Magento\Framework\View\Element\Text)
  +-- Form (Magento\Framework\View\Element\Form)
<?php
// Magento model inheritance
namespace Magento\Catalog\Model\ResourceModel\Product;

class Collection extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
    protected function _construct()
    {
        $this->_init(
            \Magento\Catalog\Model\Product::class,
            \Magento\Catalog\Model\ResourceModel\Product::class
        );
    }
}

Key Takeaway

LSP requires child classes to be substitutable for parent classes. If a child class breaks parent behavior (like Penguin not flying), you have an LSP violation. Use composition or interfaces instead.

Inheritance vs Composition

Inheritance (IS-A relationship)

<?php
// Inheritance: Product IS-A TaxableItem
class TaxableItem
{
    public function __construct(
        protected float $price,
        protected float $taxRate
    ) {}

    public function getPriceWithTax(): float
    {
        return $this->price * (1 + $this->taxRate);
    }
}

class Product extends TaxableItem
{
    public function __construct(
        float $price,
        float $taxRate,
        private string $name
    ) {
        parent::__construct($price, $taxRate);
    }
}

Composition (HAS-A relationship)

<?php
// Composition: Product HAS-A TaxCalculator
class TaxCalculator
{
    public function calculate(float $price, float $taxRate): float
    {
        return $price * (1 + $taxRate);
    }
}

class PriceFormatter
{
    public function format(float $price): string
    {
        return '\$' . number_format($price, 2);
    }
}

class Product
{
    private TaxCalculator $taxCalculator;
    private PriceFormatter $priceFormatter;

    public function __construct(
        private string $name,
        private float $price,
        private float $taxRate
    ) {
        $this->taxCalculator = new TaxCalculator();
        $this->priceFormatter = new PriceFormatter();
    }

    public function getPriceWithTax(): float
    {
        return $this->taxCalculator->calculate($this->price, $this->taxRate);
    }

    public function getFormattedPrice(): string
    {
        return $this->priceFormatter->format($this->price);
    }
}

When to Use Each

Scenario Use Inheritance Use Composition
Relationship IS-A (Product is a TaxableItem) HAS-A (Product has a TaxCalculator)
Shared behavior Child reuses parent's code Delegate to helper objects
Flexibility Fixed at compile time Can swap implementations
Testing Harder to mock parent Easy to mock collaborators
Multiple behaviors Not possible (single inheritance) Can compose multiple behaviors

Magento: Composition with DI

<?php
// Magento prefers composition over inheritance
namespace Vendor\Module\Service;

class ProductPriceService
{
    // HAS-A relationships via DI
    public function __construct(
        private TaxCalculator $taxCalculator,
        private PriceFormatter $priceFormatter,
        private DiscountCalculator $discountCalculator,
        private CurrencyConverter $currencyConverter
    ) {}

    public function getFinalPrice(Product $product, Customer $customer): string
    {
        $price = $product->getPrice();
        $discount = $this->discountCalculator->calculate($price, $customer);
        $taxedPrice = $this->taxCalculator->calculate($price - $discount, $product->getTaxRate());
        $formatted = $this->priceFormatter->format($taxedPrice);
        return $formatted;
    }
}

Composition with Strategy Pattern

<?php
interface PricingStrategy
{
    public function calculate(float $price, float $discount = 0): float;
}

class RegularPricing implements PricingStrategy
{
    public function calculate(float $price, float $discount = 0): float
    {
        return $price - $discount;
    }
}

class MembershipPricing implements PricingStrategy
{
    public function __construct(
        private float $memberDiscount
    ) {}

    public function calculate(float $price, float $discount = 0): float
    {
        return $price - $discount - ($price * $this->memberDiscount);
    }
}

class Product
{
    private PricingStrategy $pricingStrategy;

    public function setPricingStrategy(PricingStrategy $strategy): void
    {
        $this->pricingStrategy = $strategy;
    }

    public function getFinalPrice(float $discount = 0): float
    {
        return $this->pricingStrategy->calculate($this->price, $discount);
    }
}

// Swap strategies at runtime
$product = new Product();
$product->setPricingStrategy(new RegularPricing());
echo $product->getFinalPrice(5);  // Regular price - 5

$product->setPricingStrategy(new MembershipPricing(0.1));
echo $product->getFinalPrice(5);  // Price - 5 - 10% membership

Key Takeaway

Favor composition over inheritance. Composition provides flexibility, easier testing, and avoids deep inheritance hierarchies. Magento heavily uses composition through dependency injection.

Quiz

1. What does parent::__construct() do in a child class?

Question 1 options

2. What is the Liskov Substitution Principle (LSP)?

Question 2 options

3. What is the difference between inheritance and composition?

Question 3 options

4. Can PHP classes inherit from multiple parent classes?

Question 4 options

5. What does the final keyword do on a class?

Question 5 options

Flashcards

Question

What does extends do in PHP?

Answer

Creates inheritance - the child class inherits all public and protected methods and properties from the parent class. Use parent:: to call parent methods.

Question

What is the parent:: keyword?

Answer

Used to call parent class methods from a child class. Example: parent::__construct() calls the parent constructor. parent::methodName() calls the parent's version of an overridden method.

Question

What is LSP?

Answer

Liskov Substitution Principle: objects of child classes should be substitutable for parent class objects without breaking the program. Child behavior must be compatible with parent.

Question

When should you use composition over inheritance?

Answer

When you need flexibility (swap implementations), when relationships are HAS-A not IS-A, when you need multiple behaviors, or when testing is important.

Question

What does final do on a method?

Answer

Prevents child classes from overriding the method. Used on template methods to preserve the algorithm structure.

Question

What does final do on a class?

Answer

Prevents the class from being extended. The class cannot have any child classes.

Question

How does Magento use inheritance?

Answer

Magento uses single inheritance for models (AbstractModel -> Product), blocks (AbstractBlock -> Template), and collections. Uses interfaces for contracts.

Question

What is the diamond problem?

Answer

When a class inherits from two classes that have a common ancestor. PHP avoids this by supporting single inheritance only. Use traits for multiple inheritance-like behavior.

Revision Notes

Key Takeaways

  • 1. PHP supports single inheritance (extends only one parent class)
  • 2. Use parent:: to call overridden methods from the parent class
  • 3. LSP: child objects must be substitutable for parent objects
  • 4. Favor composition over inheritance for flexibility
  • 5. Use final to prevent method overriding or class extension
  • 6. Protected methods can be overridden; private methods cannot
  • 7. Magento uses inheritance for models, blocks, and collections

Interview Tips

  • Explain the difference between inheritance and composition
  • Describe LSP with a practical example
  • Know when to use final on methods and classes
  • Explain how parent:: works and when to use it
  • Understand Magento's class hierarchy (AbstractModel, AbstractBlock)

Cheat Sheet

PHP Inheritance Cheat Sheet

Basic Inheritance:

class Child extends Parent {
    public function __construct() {
        parent::__construct();
    }
    public function override(): string {
        return parent::override() . ' modified';
    }
}

Visibility:

  • public: inherited and accessible
  • protected: inherited and overridable
  • private: NOT inherited

LSP:
Child objects must work anywhere parent objects are expected.

Inheritance vs Composition:

  • Inheritance: IS-A (Product IS-A TaxableItem)
  • Composition: HAS-A (Product HAS-A TaxCalculator)

final keyword:

  • final method: cannot be overridden
  • final class: cannot be extended