Deriving a ShippingOrder from Order: What the Child Actually Gets
You have an Order class with totals, line items, and tax logic, and now shipping orders need all of that plus weight handling and tracking numbers. Copy-paste would fork the tax logic into two places that drift apart the first time the rate changes. extends forks nothing — the child is the parent, specialized:
class Order
{
public function __construct(
protected float $total,
protected array $items = [],
) {}
public function total(): float
{
return $this->total;
}
protected function tax(): float
{
return $this->total * 0.19;
}
}
final class ShippingOrder extends Order
{
public function __construct(
float $total,
array $items,
private float $weightKg,
private string $tracking,
) {
parent::__construct($total, $items);
}
public function shippingLabel(): string
{
return sprintf('%s — %.1f kg — total %.2f incl. tax',
$this->tracking, $this->weightKg, $this->total() + $this->tax());
}
}
$so = new ShippingOrder(49.9, ['book'], 0.8, 'DE-4711');
$so->total(); // inherited public method — works unchanged
$so->tax(); // Error: tax() is protected, and we are outside the class
The inheritance transfer rules are exact, and they key off the visibility ladder from Public, Private, and Protected Members:
| Parent member | Child's code can use it? |
|---|---|
public property/method |
yes — it is the child's own API surface |
protected property/method |
yes — this is the inheritance surface |
private property/method |
no — it exists inside child instances but the child cannot name it |
So ShippingOrder methods may read $this->total because it was promoted protected, and may call tax() for the same reason. Had Order kept them private, the child would have to go through total() — which is precisely the argument for keeping state private and exposing controlled protected seams rather than raw fields.
Two construction rules surprise people. First: if the child declares no constructor, the parent's constructor is inherited verbatim, arguments and all. Second: constructors are not polymorphic — parent::__construct($total, $items) is the child's job to call explicitly, because the parent constructor knows nothing about $weightKg. Forgetting that call is a classic bug: the object exists, shippingLabel() runs, and then dies on an uninitialized $this->total because the promoted-but-uncalled property was never written.
One more check worth internalizing: inheritance is a real type relationship. $so instanceof Order is true, true for ShippingOrder too — which means any function typed Order $order accepts a ShippingOrder for free. That substitutability is the seed of everything in Polymorphism and Method Overriding, and it is also the contract that overriding must not break.
Inheritance also changes where construction errors appear, and the shift is worth rehearsing. Because PHP never verifies that a child called parent::__construct, the compiler stays silent while the child's promoted-but-never-assigned properties remain uninitialized; the failure arrives later as the 'must not be accessed before initialization' Error from Classes and Objects, at a line far from its cause. The defensive habit is mechanical: write the child's constructor and the parent::__construct call together, in the same edit that adds the first property. And when a child needs no new constructor arguments at all, do not write one — the inherited constructor is the whole point of the extends clause there. Both patterns appear constantly in framework subclasses, from Symfony console commands to Laravel models.
Overriding a Method and Calling parent:: Without Breaking the Base
A child class may redefine any inherited public or protected method; that is overriding. The caller's call site does not change — $order->total() — but the implementation that runs is decided by the object's actual class:
final class WholesaleOrder extends Order
{
public function __construct(float $total, array $items, private int $customerTier)
{
parent::__construct($total, $items);
}
public function total(): float
{
$base = parent::total(); // base implementation, on demand
return $this->customerTier >= 3 ? $base * 0.85 : $base;
}
}
parent::total() runs the implementation from the class you extend, with $this still bound to the current object — so the discount version decorates the base math instead of duplicating it. parent::__construct() plays the same role for construction, and both must be called explicitly; PHP never chains them for you. (static:: also appears in this area, but it resolves against the called class via late static binding — that belongs to Static Properties and Methods.)
Overriding is restricted by signature compatibility, and PHP 8 enforces it at compile time with a fatal Declaration of WholesaleOrder::total(): float must be compatible with Order::total(): float when you get it wrong. The rules follow function variance:
- A child method must keep parameter types equal or wider (contravariant) — you may accept
Order|intwhere the parent acceptedOrder, never narrower. - The return type must stay equal or narrower (covariant) — returning
ShippingOrderwhere the parent promisedOrderis fine; promising a broader type is not. - Adding required parameters, or removing optional ones, breaks the signature. Optional additions are allowed.
An overridden method may also refuse to be overridden: marking the parent's method final makes a child re-declaration a fatal error. That is a deliberate design statement — "this step is fixed, specialize around it, not over it" — and you will see it combined with abstract methods in Abstract Classes and Contracts.
The discipline that keeps hierarchies healthy is behavioral, not syntactic. An override must honor the parent's contract: same preconditions, same postconditions, no surprising side effects. If Order::total() never threw for negative totals, WholesaleOrder::total() must not start throwing — callers typed against Order were never told to catch that. Break that promise and you do not have polymorphism; you have two unrelated functions wearing the same name, which is the failure mode the next topic's substitutability tests are designed to catch. And when the child needs no parent behavior at all — every method replaced, no shared state — that is the signal you may be forcing inheritance where composition (or an interface plus a trait) would say the same thing with less coupling.
A final compile-time detail rounds out the mechanics: visibility may widen but never narrow in an override. A child may promote a protected method to public — callers holding the child type now see more, which breaks nobody — but demoting a public method to protected removes an API the parent promised, so PHP fatals on the spot. The same asymmetry governs statics: an inherited static method may be overridden with late static binding in mind, yet self:: calls inside the parent's body still bind to the parent, never the child. Test suites earn their keep here: an override that changes behavior while keeping the signature legal is exactly what a contract test — exercising the family through the parent type — is built to catch.
One Parent Only: When extends Is the Wrong Tool
PHP allows exactly one parent: class A extends B, C {} is a parse error. That single-extends rule is not an oversight — with multiple inheritance, an override could come from two bases and the diamond problem would make parent:: ambiguous. The price is a design constraint you must route around deliberately.
The first routing tool is abstract base classes, covered next in Abstract Classes and Contracts: a half-implemented parent that both shares code and forces contracts. The second is traits, which let unrelated classes pull in the same method set without any parent-child relationship. The third — usually the healthiest — is to question whether the shared behavior belongs in a hierarchy at all:
// Coupled: ShippingOrder IS-A Order, forever, on one inheritance slot
class ShippingOrder extends Order { /* ... */ }
// Composed: TaxPolicy is a collaborator, swappable per instance
final class Order
{
public function __construct(private TaxPolicy $tax) {}
public function totalWithTax(float $net): float
{
return $this->tax->apply($net);
}
}
Composition gets you runtime flexibility (swap the policy on one order, mock it in a test, ship a new one without touching Order) where inheritance is fixed at compile time. The heuristic experienced PHP developers actually use: inheritance for a genuine is-a with a stable, closed contract (ShippingOrder really is an Order); composition for has-a or behaves-like (an order has a tax policy, a renderer is comparable). Overusing extends is one of the most common code-review findings in legacy PHP codebases, because every subclass freezes a little more of the parent.
Even when inheritance is right, two hygiene rules keep it manageable. Keep the hierarchy shallow — one or two levels; a four-deep chain where each child overrides the parent's override makes control flow unreadable in a stack trace. And prefer final on leaves you never intend to specialize: final communicates the intent to both readers and the engine (it enables small optimizations and turns accidental extension into an immediate error).
The type consequence of extends deserves one more pass because interviews return to it constantly: a child is a subtype. Anywhere the code asks for Order, a ShippingOrder is valid, and code that handles Order correctly handles every subclass without knowing they exist — as long as no override breaks the contract. That property is called substitutability, and the mechanism that builds on it — replacing parent behavior behind one call site and one type hint — is exactly what Polymorphism and Method Overriding formalizes.
There is one more inheritance cost that rarely appears in tutorials: coupling of release cycles. A subclass depends on the parent's protected surface, so changing the parent — renaming a protected method, retyping a property — is a cross-package change even when the public API looks untouched. Monorepo teams feel this as rebuild cascades; library authors feel it as major versions triggered by nothing a changelog reader can see. The countermeasures are the ones already named: keep protected surfaces tiny, mark leaves final to freeze them, and prefer injecting collaborators over inheriting behavior. PHP's own evolution mirrors the advice — the language grew traits and interfaces precisely because deep, wide hierarchies proved the wrong default in the wild.
Quiz
1. A child class's method needs to read $this->total, which the parent declared protected. What happens?
2. A child class declares no constructor. What happens when you instantiate it with the parent's constructor arguments?
3. The parent declares `public function apply(int|float $x)`. Which child override signature is legal in PHP 8?
4. Which design does PHP's single-extends rule NOT force you into?
Flashcards
Question
Which parent members does a child's code inherit, and which does it only carry?
Click to reveal answer
Answer
Public and protected members are inherited and usable by child code; private members are physically present in child instances but invisible to child code — they stay encapsulated with the declaring class. Private is per-class, not per-instance.
Question
What does parent::method() do, and when must parent::__construct() be called?
Click to reveal answer
Answer
parent::method() runs the base implementation with $this still bound to the current object — used to extend rather than duplicate base behavior. Constructors are never chained automatically: a child with its own constructor must call parent::__construct(...) itself or base initialization is skipped.
Question
Signature rules when overriding a method in PHP 8
Click to reveal answer
Answer
Parameter types must be equal or wider (contravariant); return types equal or narrower (covariant); adding optional parameters is fine, adding required ones is not. Violations are a fatal 'must be compatible' error at compile time. final methods cannot be overridden at all.
Question
Why does PHP allow only one parent class, and what are the three standard alternatives?
Click to reveal answer
Answer
Multiple inheritance makes parent:: ambiguous (diamond problem) and raises method-resolution conflicts. Alternatives: abstract base classes (shared code + enforced contracts), traits (shared method sets across unrelated classes), and interfaces (shared contract, no code). Composition covers what none of them should.
Question
Inheritance or composition — what is the working heuristic?
Click to reveal answer
Answer
Inheritance for a stable is-a subtype relationship with a closed contract (ShippingOrder is an Order); composition for has-a or behaves-like (an order has a tax policy). Composition allows runtime swapping and easy mocking; inheritance is fixed at compile time and every subclass couples you to the parent forever.
Revision Notes
Key Takeaways
- 1. extends makes the child a subtype: public and protected members transfer to child code, private ones only ride along invisibly
- 2. parent:: calls the base implementation on demand; constructors are never chained automatically, so a child with its own constructor must call parent::__construct()
- 3. Overrides must keep parameters equal-or-wider and returns equal-or-narrower; PHP 8 turns violations into compile-time fatals, not runtime surprises
- 4. An override must honor the parent's contract — same preconditions and postconditions — or substitutability breaks and the hierarchy is a lie
- 5. One parent only: route shared code through abstract classes or traits, shared contracts through interfaces, and question extends entirely when the relation is has-a
Interview Tips
- • "What does a child inherit?" — public and protected members it can use; private members it merely carries. Follow up yourself with the per-class nature of private access to show the detail
- • "Why doesn't PHP chain constructors?" — automatic chaining breaks when signatures diverge (extra promoted fields); explicit parent::__construct() makes the base initialization visible and optional where it should be
- • "Explain variance in overrides" — parameters contravariant (wider OK), returns covariant (narrower OK); give the fatal-error example: narrowing an int|float parameter to int
- • "When is inheritance wrong?" — when the relation is has-a or behaves-like, when every method would be overridden, or when the hierarchy goes deeper than two levels; name traits, interfaces, and composition as the escapes, and mention LSP as the rule overrides must not break
Cheat Sheet
Inheritance and extends
Transfer rules: public and protected members are usable in child code; private members exist in child instances but cannot be named by the child.
class Order
{
public function __construct(protected float $total) {}
public function total(): float { return $this->total; }
protected function tax(): float { return $this->total * 0.19; }
}
final class WholesaleOrder extends Order
{
public function __construct(float $total, private int $tier)
{
parent::__construct($total); // constructors never chain themselves
}
public function total(): float
{
$base = parent::total(); // decorate, don't duplicate
return $this->tier >= 3 ? $base * 0.85 : $base;
}
}
Overriding rules (fatal must be compatible when broken):
| Aspect | Allowed |
|---|---|
| Parameters | equal or wider (contravariant); optional additions OK |
| Return | equal or narrower (covariant) |
| Visibility | may widen (protected → public), never narrow |
final method |
cannot be overridden at all |
Type meaning: child is a subtype — WholesaleOrder instanceof Order is true, so any Order-typed parameter accepts it.
Single extends: one parent only; share code via abstract classes or traits, share contracts via interfaces, and prefer composition when the relationship is has-a. Keep hierarchies shallow; mark leaves final.