---
title: "What do you mean, redeclare a property static?"
url: https://www.exakat.io/what-do-you-mean-redeclare-a-property-static/
date: 2026-09-16
modified: 2026-09-16
lang: en
author: "dams"
description: "What do you mean, redeclare a property static? It started with a single line in a linting run: Fatal error: Cannot redeclare non static DOMElement::$id as static ezcDocumentPropertyContainerDomElement::$id So, a..."
categories:
  - "Code auditing"
tags:
  - "migration"
  - "php"
image: https://www.exakat.io/wp-content/uploads/2026/09/greenfield-copie.jpg
word_count: 1583
---

# What do you mean, redeclare a property static?

# What do you mean, redeclare a property static?

It started with a single line in a linting run:

`Fatal error: Cannot redeclare non static DOMElement::$id as static ezcDocumentPropertyContainerDomElement::$id`

So, a piece of code used to have a property, and now, one of the classes is now trying to redeclare this as a static. Obviously, this is never going to work, in PHP 8.6, but also, in previous versions. The class comes from the eZ Components, now Zeta Components, Document library. This is not new code, and it has not been updated for a long time. Though, it extends `DOMElement` and keeps a counter used to hand out unique numbers to nodes:

There is nothing exotic here. A static counter, named `$id`, in a class that had been stable for years. It feels like the error message is a bit misleading, or out of context. So what happened?

## First suspect: PHP 8.6

The error showed up while testing the code against PHP 8.6, so the natural first guess was a fresh change in the engine. It did not last long. Running the same code on older versions showed that the error was already there in earlier releases, and bisecting quickly narrowed it down to the 8.2 to 8.3 transition: PHP 8.2 runs the class without complaint, PHP 8.3 refuses to compile it.

## Second suspect: a new error message

Maybe PHP 8.3 introduced a new check? Not at all. The message "Cannot redeclare %s%s::$%s as %s%s::$%s" lives in `Zend/zend_inheritance.c` in PHP 7.0, and before that in `Zend/zend_compile.c` in PHP 5.6. It is an old, well established rule: a child class cannot turn an instance property of its parent into a static one, or the other way round. Userland code has always been subject to it:

So the rule is not new. Something else changed.

## Third suspect: native classes are checked differently

Since the parent is `DOMElement`, a native class, the next idea was that internal classes might escape the compile time check, and only complain later, or under certain conditions. That theory falls apart with a quick test on PHP 8.2, using a property that `DOMElement` has always had:

The same happens with `DOMNode::$nodeName`, `DOMDocument::$encoding`, `XMLReader::$name`, `ZipArchive::$status`, `Exception::$message`, `Error::$code` or `PDOStatement::$queryString`. Native classes get exactly the same treatment as custom ones. There is no special leniency.

## The actual culprit: a brand new property

The explanation is much simpler. In PHP 8.2, `DOMElement` has no `$id` property at all. Reflection lists `$tagName`, `$schemaTypeInfo`, the element traversal properties and everything inherited from `DOMNode`, but no `$id`. On an element with an `id` attribute, `property_exists($element, 'id')` returns `false`. PHP 8.3 added two properties to `DOMElement`, mirroring the DOM standard:

The UPGRADING file lists this under "New Features", not under "Backward Incompatible Changes". From the point of view of `DOMElement`, it is indeed a feature. From the point of view of every class that extends `DOMElement` and already had its own `$id`, it is a breaking change. In PHP 8.2, the static `$id` of the ezc class did not redeclare anything: there was nothing to redeclare. In PHP 8.3, it suddenly collides with a public, non static, typed property of its parent.

## Why adding a property is not free

Adding a class, a function or a constant to PHP is usually uneventful. Adding a method to a non final class is already riskier, as child classes may have a method with the same name and an incompatible signature. Properties come with their own set of inheritance rules, and all of them now apply to any child that happens to use the same name. When the parent property is public or protected, the child's declaration must:

- keep the same static or non static nature,
- keep the same or a wider visibility,
- keep the same type, when the parent property is typed.

So, beyond the static case, PHP 8.3 also rejects these, which were perfectly valid in PHP 8.2:

Each child class that used `$id` for its own purpose has to match a contract it never signed.

## When would it have been safe?

 

### A private property

Private properties do not take part in inheritance checks. Had the new property been private, the child class would simply have its own, unrelated `$id`:

Both properties live side by side, and each class sees its own. Of course, a private property is useless for a public API such as `$element->id`, so this was not an option for the DOM extension. It is, however, a good option for library authors: private properties can be added in a minor version without risking this kind of collision in child classes.

### What about final?

It is tempting to think that `final` would help, but it goes the other way. Since PHP 8.4, properties may be marked `final`, and that forbids any redeclaration, compatible or not:

A final property turns every same-named property in a child class into an error. `final` cannot be combined with `private` either ("Property cannot be both final and private"). The only `final` that removes the problem is a final class: without children, there are no collisions. `DOMElement` cannot be final, though, since extending it is the whole point of `DOMDocument::registerNodeClass()`, and that is exactly what the ezc library does.

## The popularity factor

The last ingredient is the name itself. `$id` is probably one of the most common property names in PHP code: entities, models, nodes, counters, registries all have one. `$className` is not far behind. A new property called `$isConnected`, added to `DOMNode` in the same release, is much less likely to hit anyone. The risk of adding a property to a non final, widely extended class is roughly proportional to how common its name is. `DOMElement::$id` scores high on both counts. For the record, here are the 20 most commonly used property names in PHP projects:

-

- $name
- $collection_key
- $id
- $RequestId
- $type
- $response
- $description
- $value
- $container
- $config
- $options
- $initialized
- $logger
- $data
- $table
- $message
- $getters
- $setters
- $attributeMap
- $connection

## Fixing and detecting

The fix is a rename. The Zeta Components Document library now uses `$_id` for its counter, which no longer collides with anything. To find such collisions ahead of time, here is a small script that lists properties declared in your classes that share a name with a non private property of an internal ancestor. Load your code first (autoloader, class map…), then run it on the PHP version you are migrating to:

Incompatible declarations will stop the script with a fatal error as soon as the class is loaded, which is the loud version of the report. Compatible ones, such as a `public string $id` in a `DOMElement` child, compile fine on PHP 8.3 but are reported by the script: the name now means something to the parent class, and it is worth checking that both meanings still agree.

 
MyElement::$id collides with DOMElement::$id

## Conclusion

Along the way, we ruled out a new PHP version, a new error message and a special treatment for native classes, and reviewed several edge cases of property inheritance: static versus non static, visibility, types, private and final properties. The real cause was a small, well meaning addition to the DOM API.

For the DOM extension, `$id` was a green field: an empty slot, waiting for a standard property. For the code that extends `DOMElement`, it was also a green field, when it was written. The migration error could have come from PHP adding the property, or the custome code adding it on its own. The only way out is to rename a property that had done nothing wrong for years. This is yet another case of a painful migration, discovered at compile time, one `Fatal error` at a time.