Compatibility 7 min read

PHP 8.2 Dynamic Properties: Why Your Code Started Emitting Warnings

PHP 8.2 deprecated writing to undeclared object properties. Here is what the deprecation is about, why frameworks are affected and the three ways to fix it.

When you move a codebase to PHP 8.2 you start seeing lines like Creation of dynamic property Foo::$bar is deprecated in the log, and you probably have no idea which code wrote that property. This is what that notice means, why PHP made the change, and the three ways to fix it.

The short answer

Writing to a property that the class does not declare is deprecated in PHP 8.2 and raises an E_DEPRECATED notice. It still works in 8.2 and 8.3, and it becomes a fatal error in PHP 9. The fix is to declare the property on the class, or, when the class genuinely stores extra data at runtime, to add the #[AllowDynamicProperties] attribute to the class.

This is the cheapest deprecation you will ever get, because you get a warning now and a hard failure later. A codebase that is clean of dynamic property notices today will not break on the PHP 9 upgrade.

What actually happens

A class declares its properties. When you write to a property that is not declared, PHP used to create it silently at runtime:

class Order
{
    public function total(): float
    {
        $this->subtotal = 100.00;   // not declared
        return $this->subtotal;
    }
}

$order = new Order();
$order->total();
// PHP 8.2: Deprecated: Creation of dynamic property Order::$subtotal is deprecated

The three stages are:

  • PHP 8.2 and 8.3: an E_DEPRECATED notice. The property is still created and the code still works.
  • PHP 9: the notice becomes an Error. Writing to the property throws.

The wording "Creation of dynamic property" only appears the first time a given class and property pair is written. That is deliberate: it keeps the log readable instead of printing the same line on every request in a loop. Do not assume one notice means one occurrence in your code.

Why PHP made this change

The change is about predictability, and about two specific problems that dynamic properties create.

The magic method conflict

If a class defines __get() or __set(), PHP calls those methods when the property does not exist. Before PHP 8.2, a dynamic property that happened to have the same name as a private property could bypass the visibility of that private property. A typo in a property name could silently read the wrong value instead of failing.

The performance and memory cost

Every object that carries a dynamic property carries a hash table for it. For an object that is created in a loop, that is a measurable allocation cost, and it is paid for every instance. Declared properties live in a fixed layout, which is faster and uses less memory.

The PHP vote that accepted the change was driven by performance and by the many bug reports about typos that should have been errors.

The three fixes

1. Declare the property

This is the right answer when the class owns the value. It is almost always a one-line change:

class Order
{
    private float $subtotal = 0.0;

    public function add(float $amount): void
    {
        $this->subtotal += $amount;
    }
}

Give it a type, a default and the right visibility. A property that should never be written from the outside is private; one that subclasses need is protected.

2. Add #[AllowDynamicProperties]

This is the right answer when the class intentionally accepts arbitrary data at runtime, for example a serialisation helper, a DTO that mirrors an arbitrary payload, or a hydration layer that assigns every key of an incoming array onto the object:

#[AllowDynamicProperties]
class GenericPayload
{
    // the keys are only known at runtime
}

The attribute restores the old behaviour for that one class. It is a deliberate, auditable decision, which is what makes it acceptable: a reader of the code sees that the class knows it is dynamic.

The wrong use is to silence a notice you did not investigate. If you add the attribute to a class because a notice appeared and you wanted it to stop, you have hidden a typo.

3. Fix the caller

Sometimes the property was never meant to be on this object. The write happened in a method that belongs to another class. In that case the fix is to move the assignment to the object that owns the property.

This is the case worth looking for, because it is the one that hides a design problem rather than a missing declaration.

Frameworks and libraries

Any framework or library that writes to an undeclared property triggers the notice. Most of the major ones have shipped a fix, because the deprecation was announced a full release ahead of the change.

  • Laravel and the packages built on it: the framework core and the popular packages were updated for 8.2. An old package version is often the source of the notice, not your own code.
  • Symfony and Doctrine: both have released versions that declare the properties they assign.
  • Legacy application code: this is where most of the notices come from, because application code was written without the strictness that the frameworks follow.

The first step is therefore always the same: update the dependencies, run the test suite and see what is left. composer update on a project that pins old versions is often what removes two thirds of the notices before you touch a line of your own code.

How to find every occurrence

A plain text search does not find them, because the property name is written as a string in the source but the notice is about the class. Three approaches work:

  1. Run the test suite with deprecations visible. PHPUnit reports the notices with the file and the line. This catches the paths the tests cover, which is usually most of the application and none of the rare admin routes.
  2. Turn the notice into an exception during the migration. In the test environment, convert E_DEPRECATED into an exception so the first failing test points at the exact line. This is the fastest way to get the list.
  3. Scan the source. A compatibility scanner reports the property assignments that the framework and the type system cannot prove safe, with the file and the line, without needing to execute the code. php2shell does this as one of its upgrade rules.

The third one matters for the paths the tests do not cover, which are exactly the paths that are hardest to debug in production.

The exceptions

A few classes are exempt from the notice:

  • stdClass and its subclasses: the whole point of stdClass is to hold arbitrary data, so the deprecation does not apply to it.
  • __get() and __set(): a class that defines these methods handles the missing property itself, so no dynamic property is created.
  • A class that already carries the #[AllowDynamicProperties] attribute.

ArrayObject and the other Spl classes behave like their own contracts, so they are not affected either.

Migration checklist

  1. Update the dependencies and run the test suite. Count what is left.
  2. Get the exact list of the remaining notices, with the file and the line.
  3. For each one, decide: is the property part of the contract of the class? Then declare it. Is the class a generic data holder? Then add the attribute. Is the write in the wrong place? Then move it.
  4. Run the suite again and confirm the count went to zero.
  5. Grep your own code for #[AllowDynamicProperties] and check every use. Each one should have a reason next to it.
Disclaimer. This article is guidance for planning a migration. The automatic fixes that php2shell applies are best-effort transformations. Always review the diff and run your test suite in a staging environment before you deploy. php2shell does not guarantee that an upgrade is free of runtime errors or business interruptions.

Frequently asked questions

Is the PHP 8.2 dynamic property deprecation an error?

Not yet. It is a deprecation notice in PHP 8.2 and 8.3 and becomes an error in PHP 9. That makes it the ideal moment to fix it: you get a warning now and a hard failure later.

What is #[AllowDynamicProperties] for?

It opts a class in to the old behaviour. It is the right answer for a class that intentionally stores extra data at runtime, for example a DTO that serialises an arbitrary payload. It is the wrong answer for a class where the property was simply forgotten.

Does this affect Laravel, Symfony and Doctrine?

It can. Any framework or library that writes to a property the class does not declare triggers the notice. Most of them have shipped a fix, so the first step is always to update the dependencies before changing your own code.

Where to go next