---
title: "Method Fossilisation in 2026"
url: https://www.exakat.io/method-fossilisation-in-2026/
date: 2026-07-07
modified: 2026-07-07
author: "dams"
description: "Method Fossilisation in 2026 This is an updated version Method fossilisation happens when methods get harder to update, due to sprawling overwriting. It is a process, more than a final..."
categories:
  - "Code auditing"
tags:
  - "code audit"
  - "method"
image: https://www.exakat.io/wp-content/uploads/2020/07/fossil-320.jpg
word_count: 2007
---

# Method Fossilisation in 2026

# Method Fossilisation in 2026

This is an updated version

Method fossilisation happens when methods get harder to update, due to sprawling overwriting. It is a process, more than a final state. In particular, it happens to methods when the signature is shared across multiple classes, via inheritance and interfaces. As their number grows, they render part of the code unmodifiable, without refactoring large number of files.

Let's see how this happens.

## Single methods

Single methods are methods that are not overridden by any child class and are not present in any parent class. Such methods can be updated freely: the impact is local only, and at call sites: this is quite straightforward. They do not get fossilized.

## Method fossilisation

When a method is overridden, one or multiple times across an inheritance hierarchy, the signature becomes fossilised. In practice, a single project can have methods with up to dozens of versions across the hierarchy. Changing one of them requires coordinated refactoring in as many locations.

This situation typically emerges from historical layered code or component strategies based on a shared template or interface.

[![](https://www.exakat.io/wp-content/uploads/2020/07/fossilisation-1-267x300.png)](https://www.exakat.io/wp-content/uploads/2020/07/fossilisation-1.png)

In the illustration, changing method `C2` in a deep hierarchy can cascade into six required edits across the application, each one a potential source of regression.

## Inherited methods

Inherited methods exist in a parent and a child class. Sometimes, there are grand-parents, and siblings, and then cousins: this is would be just more complex, without adding value to this illustration.

Their signatures must be compatible: they share a common signature constraint.

The methods don't have to be absolutely identical, so there is still some room to update them independently. Yet, there are rules about the number, the visibility, the names, and the types of the arguments.

### Visibility

Let's start with method visibility. The visibility of a method can only be from `private` to `protected` to `public`, and it will not go back. It may also stay at the level of visibility.

Usually, visibility is not changed at extension time.

Asymmetric visibility does not apply to methods, so there is nothing to check for that feature.

### Number of arguments

The simple rule is that `x::method()` may be called just like `y::method()`. This means:

- The number of compulsory arguments, which are those without default values, cannot change between parent and child.
- Extra optional arguments, with default values, may be added.

Constructors are exempt from these constraints and may change signatures freely between parent and child classes.

### Argument names

Arguments names may change from one method to its child method. PHP doesn't enforce identical argument names between parent and child methods.

And yet, as of PHP 8.0, argument names are part of the public API surface. Since they are not enforced, the last class overwrite all the previous definitions.

This means that renaming `$a` to `$b` in any class in the hierarchy silently breaks every caller using named argument syntax. PHP itself does not emit an error at the method definition level: the error surfaces at call sites, potentially in user code you do not control.

This is a third axis of fossilisation on top of arity, order, and types. A method may fossilise its parameter names too.

Note that using interfaces does not add any extra verifications to the method signatures, at the name level.

## Types fossilisation

PHP checks types for compatibility between a family of classes. The rule is based on variance:

- Return typehints are covariant: a child may return a more specific type than its parent.
- Argument typehints are contravariant: a child may accept a more general type than its parent.

This was formalised in PHP 7.4 and is still applied in PHP 8.5.

### Adopting typehints incrementally

Adopting types incrementally across fossilised methods can create inconsistencies: one class in the hierarchy may have a type while others do not, making the overall contract ambiguous.

### Scalar cul-de-sac

Scalar typehints, such as `int`, `float`, `string`, `bool`, are usually the first types adopted, but they are the hardest to refactor. Scalar types have no subtype relationship in PHP's type system, so variance cannot help evolve a `float` parameter into a class type: every class in the hierarchy must change at the same time.

#### Union and intersection types: some wiggle room

PHP 8.0 union types types do not escape the scalar cul-de-sac entirely, but they give more incremental room than the original situation.

Widening a parameter type with contravariance: a child method may accept a broader type than its parent. A parent typed `int` can be overridden with `int|float` in the child, because `int|float` is more permissive.

This allows progressively loosen parameter types downward through the hierarchy, one class at a time, without breaking compatibility.

Narrowing a return type, with covariance: a child method may return a stricter type than its parent. A parent returning `int|float` can be overridden with a child returning `int`.

You still cannot directly move a `float` parameter to a class type `Price` in a single refactoring step. Union types let you introduce `float|Price` as a transitional stage, preserving backward compatibility while you migrate callers, and then later drop the `float` leg.

That second step still requires a coordinated all-at-once change across the whole hierarchy. Union types turn a one-step cliff into a two-step slope, which is a real improvement. The cost of this approach is a bit of confusion, and a long time to propagate the transformations.

Intersection types, since PHP 8.1, follow the same covariance/contravariance rules. They are most useful for refining interface combinations in return positions: a parent returning `Countable` can be overridden returning `Countable&Stringable`, narrowing the contract without breaking it.

### Interface types

Fossilised methods are most flexible with interface-style types and abstract classes, because variance applies fully: a parameter typed to an interface can be overridden to accept any ancestor interface, and a return typed to an interface can be overridden to return any implementing class.

Using interfaces instead of scalars from the start is the best long-term investment for large class families.

The same union and intersection types tricks apply to interfaces.

## Modern PHP features and fossilisation

PHP 8 introduced several features that interact with inheritance hierarchies. None of them fundamentally change the fossilisation dynamic, but each adds a surface that deserves attention when auditing a fossilised method.

### never return type (PHP 8.1)

`never` is the ultimate covariant return type: a method returning `never` always throws or exits and never produces a value. Any child class method must also return `never`.

### Readonly properties (PHP 8.1) and readonly classes (PHP 8.2)

`readonly` properties are only defined in the constructor, as a promoted property. Constructors are exempted from compatibility with parents, so this does not impact fossilisation.

### Property hooks (PHP 8.4)

Property hooks allow `get` and `set` logic to be attached directly to a property, effectively embedding method-like behaviour in the property declaration.

In an inheritance hierarchy, a hooked property in a parent can be overridden in a child. There are two tricks at play here: first, the set method may have a type, and it only have to be compatible with the type of the property. This makes sense for the default usage: the set method of a `float` property must accept a `float`.

Here, the type of the `set` hook stays compatible with the property, as the union type is an extension of the `float` type of the property. This is legit code for PHP, and actually useful: one may accept a different type, and convert it before assiging it to the property itself.

Now, once a union type has been added to the hook, it must stay compatible with that type in the child classes. In the example below, the `set` in the `bar` class must accept `float|int`, since the `set` in the `foo` class did.

## Avoiding method fossilisation

- Monitor fossilisation. Keep a report to check when methods are overriden too many times and avoid building up large clusters of such methods. While there is no industry threshold available, the higher the number of overridden methods, the higher the risk of fossilisation.
- Limit uage scalar typehints in large class families. Prefer interface-style types; use union types as a transitional bridge when migrating away from scalars, when you have the patience for it.
- Treat parameter names as API. Since PHP 8.0, named arguments make parameter names part of the public contract. Ensure that the same name is used in parent and child classes.
- Document compatibility breaks. Mention signature changes explicitly in changelogs and reserve breaking changes for major versions.
- Consider skipping inheritance. Composition and delegation eliminate fossilisation entirely, at the cost of a different kind of indirection.

## Method fossilisation

Method fossilisation comes along with class hierarchies. Either the code keeps these families in check with low number of extends and emphasis on composition, or there will be clumps of code that are created and cannot be changed without major impact.