Direct Answer
Declare all known state in the base class with protected visibility. Child classes inherit access to these declared properties without triggering deprecation warnings. Reserve #[AllowDynamicProperties] for leaf classes that genuinely require arbitrary runtime properties (e.g., DTOs, ORM proxies). The attribute does not override visibility modifiers on declared properties—it only suppresses the deprecation for truly dynamic (undeclared) properties, which remain publicly accessible from any scope.
Why This Works
- Declared properties (with
public, protected, or private) are never dynamic. A protected $x in the base class is accessible to children via $this->x with zero deprecation risk.
- Dynamic properties are created only when code writes to an undeclared name (e.g.,
$obj->y = 1). In PHP 8.2 this emits E_DEPRECATED unless the class or a parent carries #[AllowDynamicProperties].
- Visibility is unchanged:
#[AllowDynamicProperties] does not make private or protected declared properties publicly writable. It merely opts the class into the legacy "any property name works" behavior for undeclared names.
- Inheritance of the attribute: once a base class has
#[AllowDynamicProperties], all descendants inherit it (cannot be revoked in 8.2). This means a single attribute at the root opens dynamic-property creation for the entire tree.
Recommended Migration Steps
- Audit undeclared writes: run static analysis (PHPStan, Psalm, or PHP_CodeSniffer) to find every
$this->undeclared = … across the hierarchy.
- Promote real state to declared
protected properties in the base class (or the most appropriate ancestor). This preserves encapsulation—children access via $this->prop, external code cannot.
- For truly dynamic leaf classes (e.g., hydrated DTOs, proxy objects), add
#[AllowDynamicProperties] to that specific class only. Avoid putting it on a high-level base class unless the entire tree needs dynamic properties.
- Verify: execute the test suite under PHP 8.2 with
error_reporting(E_ALL) and confirm no E_DEPRECATED for dynamic properties remains.
Minimal Verification Script
<?php
// base.php
class Base {
protected $declared = 1;
}
class ChildDeclared extends Base {
public function test() { $this->declared = 2; } // OK
}
class ChildDynamic extends Base {
public function test() { $this->undeclared = 3; } // E_DEPRECATED in 8.2
}
// Run: php -d error_reporting=E_ALL base.php
// Only ChildDynamic->test() emits deprecation.
// Add #[AllowDynamicProperties] to Base and both succeed without warning.
One Diagnostic Detail That Changes the Plan
Does the codebase rely on runtime-added properties on intermediate/base classes (e.g., framework internals, serialization hooks, or legacy ORM behavior)? If yes, you may need #[AllowDynamicProperties] higher in the tree; if no, keep it at the leaves.