Site icon NexGismo

PHP Dynamic Properties Deprecated: Why and How to Fix It

TL;DR
  • What: PHP 8.2+ warns “Creation of dynamic property X::$y is deprecated” when you write to a property the class never declared.
  • Why it matters: It floods logs today and becomes a fatal Error in PHP 9.0. Drupal, WordPress, and Laravel code all hit it.
  • What to do: Declare the property. Use #[AllowDynamicProperties] only as a bridge. Reach for WeakMap when you don’t own the class.
  • Key stat: The RFC passed 52 to 25, and removing dynamic properties can shrink every PHP object by 8 bytes.

PHP dynamic properties are deprecated because writing to an undeclared class property has been a silent bug source since PHP 4. PHP 8.2 emits a deprecation notice on the first write, and PHP 9.0 will throw an Error. Fix it by declaring the property in the class. Use #[AllowDynamicProperties] only for classes that intentionally accept arbitrary keys.

PHP dynamic properties are properties assigned on an object at runtime without being declared in the class definition. Since PHP 8.2, creating one on any class not marked with the #[AllowDynamicProperties] attribute raises a deprecation notice, and PHP 9.0 turns that notice into an Error exception. stdClass and classes implementing __set() are exempt.

PHP dynamic properties have been quietly breaking codebases since PHP 8.2 shipped in December 2022, and in 2026 the problem is getting louder, not quieter. Drupal 11 requires PHP 8.3, WordPress hosts have moved default runtimes to 8.3 and 8.4, and PHP 8.6 lands on November 19, 2026. Every one of those runtimes prints the same line: Deprecated: Creation of dynamic property Foo::$bar is deprecated. I hit it last month on a client’s Drupal 10 to 11 migration. A single contrib module wrote 14 undeclared properties in its plugin constructor, and the dblog table grew by 40,000 rows in a weekend. The fix took 20 minutes once I understood the rule. Understanding the rule is the hard part, because three separate exemptions and one nasty __set() trap make the behaviour look random. This post explains exactly when PHP fires the notice, what changes in PHP 9, and which of the four fixes to use in production code.

Why are PHP dynamic properties deprecated?

PHP deprecated dynamic properties because a typo in a property write was completely silent. Reading $user->nane warns you. Writing $user->nane = 'x' on PHP 8.1 and earlier just created a new property and moved on. Nikita Popov’s Deprecate dynamic properties RFC passed in November 2021 with 52 votes for and 25 against. That is a narrow margin for a core language change, and the “no” column included Rasmus Lerdorf and Zeev Suraski.

The RFC gives two motivations. First, static analysers cannot tell an intentional dynamic write from a typo, so PhpStorm only shows a weak grey underline. Second, if every class that wants dynamic properties says so explicitly, the engine no longer needs to reserve a properties hash table slot on every object. The RFC estimates that saves 8 bytes per object. On a Drupal render array with 200,000 objects in memory, that is roughly 1.6 MB, which is small but free.

The rule itself is simple. On PHP 8.2 and later, the first write to an undeclared property on a class without #[AllowDynamicProperties] emits E_DEPRECATED. The second write to the same property on the same object is silent, because the property now exists. In PHP 9.0 the first write throws an Error instead.

Treat this as a correctness fix, not a style rule. The deprecation is doing exactly what a type system should do.

What does “Creation of dynamic property is deprecated” mean in practice?

The message means your code assigned $obj->something and the class has no $something declared, and PHP is telling you once per object per property. Here is the smallest reproduction, plus the version behaviour from the PHP 8.2 migration guide.

class Invoice {
    public int $total = 0;
}

$invoice = new Invoice();
$invoice->total = 500;      // fine: declared
$invoice->currency = 'USD'; // PHP 8.1: silent
                            // PHP 8.2-8.6: Deprecated: Creation of dynamic property Invoice::$currency is deprecated
                            // PHP 9.0: Error: Cannot create dynamic property Invoice::$currency
$invoice->currency = 'EUR'; // no second notice; property exists now

Three things fire the notice that developers do not expect. Writes from inside the class itself count, so $this->cache = [] in a constructor without a declaration is a hit. Writes inside __set() that do $this->{$name} = $value also count, because the class has no real property. And a property declared on a parent class is fine, but a property declared on a trait that the class forgot to use is not.

The “once per object” rule is why the notice is so easy to miss in development and so loud in production. Your local request creates one object. Production creates 5,000 per request.

Why Drupal and WordPress make it worse

Drupal logs every deprecation to dblog by default, and WordPress with WP_DEBUG prints them to the page. WordPress core tracked this as Trac ticket #56034 and declared all known properties across core. Drupal core fixed it in the Views and TypedData subsystems, but hundreds of contrib modules still carry issues titled “Creation of dynamic property is deprecated”. If you maintain a module, this is also the moment to finish the move to hooks as classes in Drupal 11, because the plugin classes you touch are the same ones that write undeclared properties.

How do you fix PHP dynamic properties deprecated warnings?

Declare the property in the class. That is the correct fix in 90% of cases, and it takes one line per property.

  1. Grep your logs or run php -d error_reporting=E_ALL on the test suite to collect every Class::$property pair.
  2. Open the class and add a declaration with a type and a sensible default.
  3. If the value is set in the constructor, prefer constructor property promotion.
  4. Re-run the suite. The notice count should drop to zero for that class.

Broken and fixed side by side:

// BROKEN: fires on PHP 8.2+, fatal on PHP 9
class ReportBuilder {
    public function __construct(Connection $db) {
        $this->db = $db;          // undeclared
        $this->rows = [];         // undeclared
        $this->generatedAt = null;// undeclared
    }
}

// FIXED: declared, typed, promoted
class ReportBuilder {
    private array $rows = [];
    private ?\DateTimeImmutable $generatedAt = null;

    public function __construct(private readonly Connection $db) {}
}

The fixed version is also faster. A declared property is a fixed slot in the object’s property table, so the engine reads and writes it by offset. A dynamic property lives in a per-object hash table that has to be allocated on first write and looked up by string key on every access.

If you have hundreds of these, do not fix them by hand. Rector’s CompleteDynamicPropertiesRector in the CodeQuality set scans a class for undeclared writes and inserts the declarations. On the Drupal project I mentioned, it fixed 61 of 74 occurrences in one pass. The remaining 13 were in __set() methods and needed a human.

When should you use #[AllowDynamicProperties] instead?

Use #[AllowDynamicProperties] only on classes whose entire purpose is to hold arbitrary keys, such as a config bag or a JSON value object. It is a one-line attribute, it is inherited by every child class, and it is silently ignored on PHP 7.x because unknown attributes are treated as comments.

#[\AllowDynamicProperties]
final class LegacySettings {
    // intentionally schema-less
}

$s = new LegacySettings();
$s->anything = 'goes'; // no notice on 8.2, no Error on 9.0

The inheritance behaviour is the trap. I have seen teams slap the attribute on an abstract BaseController to silence a noisy log, which turns off the check for every controller in the application. That is the same as running PHP 8.1 forever. The attribute exists so the ecosystem can measure how much intentional dynamic-property use is left, per the RFC’s own wording. Putting it on base classes hides exactly the code the check was designed to find.

My rule on client projects: the attribute is allowed on final classes only, and every use needs a comment explaining why the shape cannot be declared.

Why does __set() not trigger the notice, and when does it?

Classes with a __set() magic method are exempt because the write never reaches the engine’s property table. PHP calls your method instead, and your method decides what to do. That makes __get() and __set() a legitimate fix for classes that must accept arbitrary keys and still want validation.

// SAFE: writes go to the array, never to a real property
class Bag {
    private array $data = [];
    public function __set(string $name, mixed $value): void { $this->data[$name] = $value; }
    public function __get(string $name): mixed { return $this->data[$name] ?? null; }
    public function __isset(string $name): bool { return isset($this->data[$name]); }
}

// STILL DEPRECATED: __set creates a real dynamic property
class LeakyBag {
    public function __set(string $name, mixed $value): void {
        $this->{$name} = $value; // Deprecated: Creation of dynamic property LeakyBag::$name
    }
}

The PHP.Watch breakdown documents this second case, and it is the one that catches senior developers. The magic method exempts the write that invokes it, not the write it performs. If __set() assigns to $this->{$name}, the engine sees an undeclared property write and fires the notice exactly as if you had written it from outside.

There is a cost. Every access routed through __set() is a full method call with argument binding, while a declared property write is a single slot assignment. In a hot loop, that difference is measurable in any profiler. Use magic methods for genuinely dynamic bags, not as a way to avoid typing property declarations.

How do you attach data to objects you don’t own?

Use a WeakMap. The classic pattern of stamping $event->processed = true onto a third-party object is exactly what PHP 9 will refuse, and you cannot edit a vendor class to declare the property. WeakMap, added in PHP 8.0, keys on the object and drops the entry automatically when the object is garbage collected.

// BROKEN on PHP 9: vendor Event class has no $processed
$event->processed = true;
if (!empty($event->processed)) { /* ... */ }

// FIXED: side table keyed by object, auto-cleaned
final class EventTracker {
    private \WeakMap $processed;
    public function __construct() { $this->processed = new \WeakMap(); }
    public function mark(object $event): void { $this->processed[$event] = true; }
    public function isProcessed(object $event): bool { return isset($this->processed[$event]); }
}

This is also the pattern Symfony uses for lazy initialisation. Its Constraint::$groups is declared, then unset() in the constructor so the first read goes through __get(). A declared property that has been unset stays declared, so re-initialising it later never creates a dynamic property. That trick is worth knowing for any class with expensive lazy fields, and it pairs well with the refactoring habits in writing code a human will maintain.

What happens to dynamic properties in PHP 9?

PHP 9.0 converts the PHP dynamic properties deprecation into an Error exception, and no release date exists yet. PHP 8.6 is scheduled for November 19, 2026, which means at least one more 8.x release stands between you and the break. That is not a reason to wait. Every 8.x runtime from 8.2 onward already pays the logging cost, and on a Drupal site that cost is real: each notice is a database insert into watchdog, and I have watched that table hit 40,000 rows in 48 hours from one module. It is the same class of self-inflicted outage as the cacheability trap behind a Drupal 502 Bad Gateway: harmless in isolation, expensive at scale.

The safest posture in 2026 is to make the notice fatal in CI now. Add set_error_handler() that throws on E_DEPRECATED in your test bootstrap, or run PHPUnit with failOnDeprecation="true". Your suite then fails today in the exact places PHP 9 will fail later, while you still have time to fix them.

Comparing the four fixes for PHP dynamic properties

Four ways to resolve the dynamic property deprecation, compared
Option Performance Complexity When to Use
Declare the property Fastest: fixed slot access, 8 bytes smaller object One line per property; Rector can automate Default. Any class you own with a known shape
#[AllowDynamicProperties] Same as PHP 8.1: per-object hash table One attribute, inherited by children Final config or value-object classes that are intentionally schema-less
__get() / __set() to an array Method call per access; slowest in hot loops Four magic methods for full semantics Dynamic bags that need validation or lazy loading
WeakMap side table Hash lookup keyed by object; auto-freed Small wrapper class Attaching data to objects from vendor code you cannot edit

The NexGismo verdict: Declare the property and move on. #[AllowDynamicProperties] is a migration bridge, not a fix, and putting it on a base class throws away the only compile-time safety net PHP has ever given property writes. Run Rector’s CompleteDynamicPropertiesRector once, hand-fix the __set() leftovers, and make E_DEPRECATED fatal in CI so PHP 9 cannot surprise you.

Frequently Asked Questions

What are dynamic properties in PHP?

PHP dynamic properties are object properties created at runtime by assigning to a name the class never declared, for example $user->nickname = ‘x’ when class User has no $nickname. PHP allowed this silently through 8.1. Since PHP 8.2, creating one on a normal class emits a deprecation notice, and PHP 9.0 will throw an Error instead.

Why is creation of dynamic property deprecated in PHP 8.2?

Because silent property creation hides typos and renames. Reading an undefined property already warned; writing one did not. The 2021 RFC by Nikita Popov deprecated the behaviour so static analysers can flag every undeclared write as a real error, and so the engine can eventually drop the per-object properties table that dynamic support requires.

How do I fix “Creation of dynamic property is deprecated”?

Declare the property in the class, ideally with a type and default value. If the class is intentionally schema-less, add the #[\AllowDynamicProperties] attribute above it. If you need to attach data to an object from vendor code, store it in a WeakMap keyed by the object instead of writing to the object directly.

Does #[AllowDynamicProperties] work on PHP 7?

Yes, safely. PHP 7.x parses the #[…] syntax as a comment, and PHP 8.0 and 8.1 ignore attributes that reference unknown classes. So you can add #[\AllowDynamicProperties] to a class in a library that still supports PHP 7.4, and it only takes effect on PHP 8.2 and later where the deprecation exists.

Will dynamic properties be removed in PHP 9?

Yes. The RFC states that PHP 9.0 will throw an Error exception on creation of a dynamic property for any class without #[AllowDynamicProperties]. stdClass and classes with __set() keep working. PHP 9.0 has no release date yet; PHP 8.6 ships November 19, 2026, so there is still time to fix code, but the removal itself is decided.

The PHP dynamic properties deprecation is one of the few PHP changes that pays for itself the day you fix it. Declared properties are faster, smaller, and visible to every tool in your editor. The migration is mechanical for most classes, and Rector does the mechanical part. The two places that need a human are __set() methods that write back to $this, and base classes where someone was tempted to paste the attribute. Handle those two by hand, make deprecations fail your CI, and PHP 9 becomes a version bump instead of an incident. If you are mid-way through a Drupal or WordPress upgrade and want a second pair of eyes on the deprecation log, the comments are open.

Exit mobile version