Inheritance: When and Why
What Inheritance Provides
Inheritance models an "is-a" relationship. A Dog is an Animal. A CheckingAccount is a BankAccount.
abstract class AbstractModel
{
protected $data = [];
protected $eventPrefix = 'model';
public function getData($key = null)
{
if ($key === null) {
return $this->data;
}
return $this->data[$key] ?? null;
}
public function setData($key, $value = null)
{
$this->data[$key] = $value;
return $this;
}
public function save()
{
$this->getResource()->save($this);
return $this;
}
}
// Product IS-A AbstractModel — inheritance makes sense here
class Product extends AbstractModel
{
public function getSku(): ?string
{
return $this->getData('sku');
}
}
When Inheritance Works Well
- True "is-a" relationships:
Product extends AbstractModel - Template Method Pattern: Parent defines algorithm skeleton, children override steps
- Shared state/behavior: Common properties across a family of classes
The Problem: Deep Hierarchies
// DON'T: 5 levels deep
class AbstractModel { /* base */ }
class AbstractEntity extends AbstractModel { /* entity layer */ }
class AbstractCatalogModel extends AbstractEntity { /* catalog layer */ }
class AbstractProduct extends AbstractCatalogModel { /* product layer */ }
class SimpleProduct extends AbstractProduct { /* simple product */ }
// Now config: SimpleProduct inherits:
// - 40+ methods from AbstractModel
// - 15+ methods from AbstractEntity
// - 20+ methods from AbstractCatalogModel
// - 30+ methods from AbstractProduct
// = 100+ methods, most irrelevant for SimpleProduct
This is called the fragile base class problem — changes in AbstractModel can break SimpleProduct in unexpected ways.
Composition: The Flexible Alternative
What Composition Provides
Composition models a "has-a" relationship. A Car has an Engine. A Product has a PriceCalculator.
namespace Vendor\Catalog\Model;
// Each concern is a separate, injectable class
class ProductPriceService
{
public function __construct(
private PriceCalculator $calculator,
private DiscountResolver $discounts,
private TaxResolver $taxes
) {}
public function getFinalPrice(ProductInterface $product): float
{
$base = $this->calculator->getBasePrice($product);
$discount = $this->discounts->resolve($product);
$tax = $this->taxes->resolve($product);
return ($base - $discount) * (1 + $tax);
}
}
class ProductSearchService
{
public function __construct(
private SearchIndex $index,
private FilterParser $filters
) {}
public function search(string $query): array
{
$parsed = $this->filters->parse($query);
return $this->index->find($parsed);
}
}
// Product uses these services via composition
class Product
{
private ProductPriceService $priceService;
private ProductSearchService $searchService;
public function getFinalPrice(): float
{
return $this->priceService->getFinalPrice($this);
}
}
Benefits of Composition
- Flexibility: Swap
TaxResolverimplementation via DI without changingProduct - Testability: Mock each service independently
- Single Responsibility: Each service does one thing
- No fragile base class: Changes in
PriceCalculatordon't affectProductSearchService
// Swapping implementations via di.xml
<config>
<type name="Vendor\Catalog\Model\ProductPriceService">
<arguments>
<argument name="taxes" xsi:type="object">Vendor\Tax\Model\VatResolver</argument>
</arguments>
</type>
</config>
No code changes needed — just configuration.
Magento's Composition Patterns
Magento Favors Composition
Magento 2 uses composition extensively through its dependency injection system. Even classes that appear to use inheritance often delegate to composed services.
Pattern 1: Delegate to Resource Model
// Product extends AbstractModel (inheritance for shared data pattern)
// But actual behavior is delegated to composed services
$resource = $this->getResource(); // Composition: ProductResource
$resource->save($this);
$collection = $this->getCollection(); // Composition: ProductCollection
Pattern 2: Strategy via DI
// Magento uses composition for strategy selection
namespace Magento\Shipping\Model\Carrier;
interface CarrierInterface
{
public function collectRates(\Magento\Shipping\Request $request);
public function getCode(string $code);
}
// Each shipping carrier is a separate class — composition
// FedexCarrier, UpsCarrier, DhlCarrier all implement CarrierInterface
// Selected via configuration, not inheritance
namespace Magento\Checkout\Model\Cart\Payment\Method;
// Payment methods use the same pattern
interface PaymentMethodInterface
{
public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount);
public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount);
}
Pattern 3: Observer/Plugin Composition
// Rather than extending classes to add behavior,
// Magento uses plugins (composition)
class ProductPlugin
{
public function beforeSave(\Magento\Catalog\Model\Product $subject): void
{
// Add behavior without extending Product
}
}
This is composition at the framework level — behavior is added by composing plugin classes, not by inheritance.
Decision Framework: Inheritance or Composition?
Use Inheritance When:
| Scenario | Why Inheritance Works |
|---|---|
| True "is-a" relationship | Product IS-A AbstractModel |
| Template Method pattern | Parent defines algorithm, children fill steps |
| Shared state across family | All products have $data, $resource |
| Framework requires it | Magento's AbstractModel hierarchy |
Use Composition When:
| Scenario | Why Composition Works |
|---|---|
| "Has-a" or "uses-a" relationship | Product HAS-A PriceCalculator |
| Need to swap implementations | Different tax calculators via config |
| Avoiding deep hierarchies | More than 2-3 levels of inheritance |
| Unit testing requirements | Mock composed services easily |
| Multiple responsibilities | Separate price, search, notification services |
The Hybrid Approach (Magento Reality)
// Magento uses BOTH: inheritance for structure, composition for behavior
class Product extends AbstractModel // Inheritance for data pattern
{
// Composition for behavior
public function __construct(
Context $context,
Registry $registry,
AbstractResource $resource,
AbstractResource $resourceCollection,
array $data = [],
// Injected services
private PriceCalculator $priceCalc,
private StockResolver $stockResolver
) {
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
}
public function getFinalPrice(): float
{
// Delegate to composed service
return $this->priceCalc->calculate($this);
}
}
Inheritance provides the data structure; composition provides the behavior. This is the practical Magento approach.
Quiz
1. Composition models which relationship?
2. What is the 'fragile base class' problem?
3. How does Magento primarily achieve composition?
Flashcards
Question
What relationship does composition model?
Click to reveal answer
Answer
Has-a (e.g., Product has a PriceCalculator)
Question
What relationship does inheritance model?
Click to reveal answer
Answer
Is-a (e.g., Product is an AbstractModel)
Question
What is the fragile base class problem?
Click to reveal answer
Answer
Parent class changes unexpectedly break child classes
Question
How does Magento prefer to add behavior without inheritance?
Click to reveal answer
Answer
Via plugins (interceptors) — composition at the framework level
Revision Notes
Key Takeaways
- 1. Inheritance = is-a, Composition = has-a
- 2. Deep hierarchies cause fragile base class problems
- 3. Magento uses inheritance for data structure, composition for behavior
- 4. DI via di.xml enables swapping composed implementations without code changes
- 5. Plugins are composition applied at the framework level
Interview Tips
- • Explain the trade-offs: inheritance is simpler but less flexible
- • Give a concrete Magento example where composition solved a real problem
- • Discuss when inheritance is still the right choice (AbstractModel)
Cheat Sheet
Inheritance (is-a): Product extends AbstractModel
Composition (has-a): Product uses PriceCalculator
Magento Pattern:
Structure → Inheritance (AbstractModel, AbstractEntity)
Behavior → Composition (DI, plugins, strategies)
Rule: If you can inject it, don't inherit it.