PHP inheritance lets one class extend one parent and specialize accessible behavior under signature, visibility, property, constructor, and late-static-binding rules.
Use it only for substitutable relationships, close unsupported extension with final, prefer composition for independent capabilities, and run the parent behavior contract against every child.
<?php
class Notification
{
public function subject(): string
{
return 'Account update';
}
}
final class WelcomeNotification extends Notification
{
public function subject(): string
{
return 'Welcome: ' . parent::subject();
}
}
echo (new WelcomeNotification())->subject();
Welcome: Account update
Code accepting the parent type should continue to work with the child. A child that rejects valid parent input or changes the meaning of inherited behavior signals a broken hierarchy.
<?php
final class TaxCalculator
{
public function addTax(float $amount): float
{
return $amount * 1.18;
}
}
final class Order
{
public function __construct(private TaxCalculator $tax)
{
}
public function total(float $subtotal): float
{
return $this->tax->addTax($subtotal);
}
}
echo (new Order(new TaxCalculator()))->total(100);
118
| Question | Inheritance | Composition |
|---|---|---|
| Relationship | Child is a parent type | Object uses another object |
| Change impact | Parent changes affect children | Dependency can be replaced |
| Multiple capabilities | Single class inheritance | Many collaborators or interfaces |
Use protected members sparingly because they couple subclasses to internal representation. Prefer private state with protected methods that expose a stable extension point. The final keyword can protect an invariant on a class or method that must not be overridden.
Call parent constructors when the parent establishes required state. Match compatible method signatures and visibility when overriding, add #[Override] where the supported PHP version permits it, and test through the parent contract so a subclass cannot silently violate substitution.
A class extends one parent class and inherits accessible public and protected methods, properties, and constants. Private parent members remain owned by the declaring class and are not directly accessible to the child. PHP supports multi-level inheritance but not extending multiple classes.
Inheritance should represent a substitutable is-a relationship. Code written for the parent contract should continue to work with the child without special cases. Reusing a few methods is not enough reason to create a permanent hierarchy.
The parent class must be available when the child is declared, normally through an autoloader. Keep namespace and loading failures distinct from inheritance errors. A child cannot repair a missing or incompatible parent at runtime.
Inherited mutable protected state couples every subclass to representation details. Prefer private state with protected operations or composed collaborators so the parent can preserve invariants while subclasses extend deliberate points.
A child may override an inherited method with a compatible signature. Visibility may be relaxed but not generally restricted. Parameter types may be contravariant and return types covariant under PHP compatibility rules, while static, final, reference, and other signature requirements must remain valid.
Named arguments make public parameter names observable. Keep child parameter names aligned with parent and interface declarations even where the engine permits a different spelling. A signature can be type-compatible yet still break callers that use names.
An override should preserve behavioral expectations: valid inputs, result meaning, side effects, exceptions, ordering, idempotency, and performance envelope where promised. Returning a compatible type does not prove substitutability.
Use the `Override` attribute where supported by the project runtime to ask PHP to verify that a method actually overrides an inherited method. This catches renamed parent methods and spelling mistakes that would otherwise create a new unrelated method.
`parent::method()` invokes a parent implementation from the child while retaining the current object for non-static behavior. Use it when the parent establishes a required invariant and the child adds a deliberate extension before or after it. Do not call parent automatically if the override replaces the contract entirely.
`self::` resolves relative to the class where the code is written, while `static::` uses late static binding based on the called class. Choose deliberately in factories and extensible static APIs. Confusing them can construct the base class when a subtype was expected.
Static state is shared according to declaration and inheritance rules and can become difficult to isolate. Avoid using inheritance plus mutable static properties for request, tenant, or test state. Inject instance collaborators or configuration instead.
Directly naming a grandparent class to bypass the immediate parent is a strong coupling signal. It can skip invariants introduced by the parent. Refactor the reusable behavior into a protected final operation or a composed service when a hierarchy needs selective skipping.
A child constructor does not automatically run the parent constructor when the child declares its own constructor. Call `parent::__construct` with valid dependencies when parent initialization is required. The object should not become observable before every class invariant is established.
Constructor promotion and readonly properties reduce boilerplate but do not remove validation. A child cannot override a read-write property with readonly or the reverse under inheritance compatibility rules. Keep property capability consistent across the hierarchy.
Avoid calling overridable methods from constructors. The child override may run before child fields are initialized, creating partial-state defects. Constructors should validate and assign; post-construction workflows belong in explicit methods or factories.
Destructors in a hierarchy are fragile for critical cleanup because execution timing and shutdown state can vary. Provide explicit close or transaction ownership and use destructors only as a last safety net for non-critical resources.
A final class closes inheritance; a final method prevents overriding while allowing other extension. Mark code final when subclass customization is not a supported contract. This lets internal implementation change without preserving undocumented protected behavior.
Composition gives an object collaborators for storage, policy, formatting, transport, or time. It supports replacing one capability without inheriting unrelated state and lifecycle. Prefer composition when the relationship is has-a, uses-a, or can-change-independently.
Abstract classes and interfaces can define intentional extension surfaces, but deep hierarchies increase constructor, visibility, override, and testing complexity. Keep hierarchy depth small and ensure every level adds one coherent abstraction rather than a patch for one caller.
Traits provide horizontal reuse but do not create multiple inheritance. They copy implementation into the using class and can introduce conflicts or implicit requirements. A service object is clearer when reusable behavior has dependencies or mutable state.
Run the parent contract test suite against every concrete child. Include construction, public methods, exceptions, side effects, named arguments, property access, and cleanup. Add a test where the child is passed through a parent-typed consumer to expose special-case assumptions.
When extending internal PHP classes, match current tentative or declared return types. Temporary compatibility attributes should be tracked and removed after the minimum runtime advances. Test under both minimum and current supported PHP versions.
Refactor a weak hierarchy incrementally: identify the capability being reused, extract an interface or collaborator, move one consumer, and then remove inherited access. Preserve public behavior with characterization tests before changing protected internals.
Static analysis can detect incompatible overrides, unused protected members, impossible types, and missing parent calls in some patterns. It cannot prove behavioral substitutability, so combine it with contract tests and code review focused on invariants.
Review serialization and persistence before changing a hierarchy. Stored class names, discriminator values, queued payloads, and cached objects can outlive a deployment. Prefer stable domain identifiers over concrete class names, migrate old representations explicitly, and verify mixed-version workers during rollout.
0 of 2 checked
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.