After upgrading to PHP 8.1 or later, a class extending ArrayObject logs "Return type of Bag::count() should either be compatible"; what does it mean and how do you fix it?
answer
- tentative return types, PHP 8.1
- internal non-final methods only
- E_DEPRECATED at class declaration
- declare the type, or suppress temporarily
- user-class parents stay fatal
basics
~10 sSince PHP 8.1 most non-final internal methods carry tentative return types; an override without a compatible one raises E_DEPRECATED. Declaring the type fixes it; #[\ReturnTypeWillChange] only suppresses the notice until checking becomes strict.
solid answer
~40 sPHP 8.1 gave most non-final methods of **internal** classes and interfaces **tentative return types**, such as `ArrayObject::count(): int`. An override that omits the return type or declares an incompatible one is still accepted, but inheritance raises an `E_DEPRECATED` notice when the class is declared: `Return type of Bag::count() should either be compatible with ArrayObject::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice`. The real fix is declaring `: int`. `#[\ReturnTypeWillChange]` on the method silences the notice, which is useful only for code that must also run on versions that cannot express the type. The manual says a future version will make the check strict and the attribute will stop working. The attribute never helps against user-defined parents, where a mismatch is always fatal.
code
php · 30 lines<?php
declare(strict_types=1);
// Deprecated since 8.1: no return type on an override of ArrayObject::count(): int
class LegacyBag extends ArrayObject
{
public function count()
{
return parent::count();
}
}
// Fixed: the compatible return type is declared
final class Bag extends ArrayObject
{
public function count(): int
{
return parent::count();
}
}
// Suppressed only: still needs a real fix before strict checking arrives
final class PortableBag extends ArrayObject
{
#[\ReturnTypeWillChange]
public function count()
{
return parent::count();
}
}go deeper
Recall that since PHP 8.1 overriding methods of built-in classes need compatible return types, and that declaring the type makes the deprecation go away.
Explain tentative return types, when the notice fires, and why #[\ReturnTypeWillChange] exists for cross-version code but has no effect on user-class parents.
Plan the upgrade: surface deprecations in CI, add types to your own overrides, update vendor packages, and track every #[\ReturnTypeWillChange] as debt to remove.
Weigh how long to keep supporting PHP versions that force the attribute against the cost of carrying suppressed notices that will become fatal errors.
## Where the notice comes from PHP's own classes and interfaces (the SPL containers, `Countable`, `IteratorAggregate`, `JsonSerializable`, the date classes and many more) were written before PHP had return types. When return types arrived, adding them to internal methods would have instantly broken every user class that overrides those methods without a return type, because a missing return type in an override is normally a fatal error. **PHP 8.1** introduced a transition instead: **tentative return types**. Most non-final internal methods now declare a return type, for example: - `ArrayObject::count(): int` - `Countable::count(): int` - `IteratorAggregate::getIterator(): Traversable` - `JsonSerializable::jsonSerialize(): mixed` For these methods the engine treats an incompatible override as a **deprecation**, not a fatal error. The notice is raised during inheritance checking, when the class is declared or autoloaded, not when the method is called: `Deprecated: Return type of Bag::count() should either be compatible with ArrayObject::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice` The class still works exactly as before; the notice is a warning about the future. ## The two fixes | Fix | What it does | When to use it | |---|---|---| | declare the compatible return type, e.g. `: int` | removes the incompatibility for good | code that runs on PHP 8.1+ only, or whose required type is expressible on every supported version | | add `#[\ReturnTypeWillChange]` to the method | suppresses this deprecation only | code that must also run on versions where the type cannot be written, or that genuinely returns something else today | The cross-version case is the reason the attribute exists. A library supporting PHP 7.4 cannot write `: mixed`, which appeared in 8.0. The attribute syntax `#[...]` begins with `#`, which older PHP versions read as a line comment, so the attribute costs nothing there. ## What the attribute does not do 1. It does **not** relax checks against **user-defined** parents. If your own base class declares `: int` and the child omits it, the declaration fails with `Declaration of ... must be compatible with ...` whether or not the attribute is present. 2. It does **not** make the override correct. A `count()` that returns a string still breaks callers that rely on `int`. 3. It is **temporary** by design. The manual states that signature checking for internal methods will become strict in a future PHP version, at which point the attribute stops working and mismatches become fatal. As of PHP 8.5 the check is still a deprecation. ## Finding every affected class The notices are easy to miss because they come from class declaration, often inside an autoloader, and a class that is never loaded in a given request never reports. To find them all: 1. Run the full test suite with every deprecation reported, since tests load far more classes than a single request. 2. Search the code for classes that extend or implement internal types (`ArrayObject`, `ArrayIterator`, `Countable`, `IteratorAggregate`, `JsonSerializable`, the date classes) and check each override's return type. 3. Check whether each hit is your code or a dependency, because the fixes differ. ## Working through an upgrade - Turn deprecations on in development and CI so the notices surface; they appear once per class declaration, not per call. - Fix your own classes by adding the return type the notice names. Where the internal type is `mixed`, declaring `: mixed` is the compatible choice. - Treat `#[\ReturnTypeWillChange]` in your own code as a TODO with a removal date tied to dropping old PHP versions. - For notices coming from `vendor/`, upgrade the package; a maintained library has usually already added the types or the attribute.
- Your own base class adds : int to a method, and a subclass without a return type starts failing. Can #[\ReturnTypeWillChange] bridge the gap?No. The attribute only applies to tentative return types of internal methods. Against a user-defined parent, a missing or incompatible return type is a fatal `Declaration ... must be compatible` error regardless. Add the return type to the subclasses in the same change, or ship the parent's type in a major version.
- The notice appears only once per request even though count() is called thousands of times. Why?It is raised during inheritance checking, when the class is declared or autoloaded, not when the method runs. Calls to `count()` execute normally and produce nothing. That is also why the notice names the class and method but never a call site.
saying these in an interview costs you the question
- The deprecation fires each time the overriding method is called
- #[\ReturnTypeWillChange] also silences mismatches against user-defined parents
- Adding #[\ReturnTypeWillChange] is a permanent, recommended fix
- The notice means the overriding method is already broken at runtime
- Fixing it means changing the built-in parent class, not the override