PHP

Creation of dynamic property is deprecated: fix it or snooze it

Your host moves you to PHP 8.2 and the log fills up with a warning that breaks absolutely nothing. It is very tempting to silence it. The thing to understand is that a deprecation is a dated notice, and the date is the next major version.

Deprecated: Creation of dynamic property App\Order::$total is deprecated, several thousand times, starting the morning your host moved you to PHP 8.2. Nothing is broken. Everything still works. The log is now unusable.

What changed, and why

Until 8.2, assigning to a property that a class never declared just created it:

class Order {
    public int $id;
}

$order = new Order();
$order->total = 99;   // fine before 8.2, deprecated from 8.2

That was convenient, and it was also the source of a bug that is unusually hard to see. Typo a property name and you silently create a new one instead of writing to the one you meant. $order->totl = 99 succeeds, $order->total stays empty, and nothing anywhere complains. Most PHP developers have lost an afternoon to that at least once.

Declared properties are also faster, because the engine can allocate a fixed slot per object rather than carrying a hash table for arbitrary names. So the change buys correctness and performance at the cost of some typing. It is a good change, which does not make it a convenient one on a codebase from 2016.

Three ways to respond

1. Declare the property. This is the fix. One line, and you get the typo protection and the performance:

class Order {
    public int $id;
    public int $total = 0;
}

For anything you own and understand, do this and move on. It is almost always a smaller job than it looks, because the deprecations cluster into a few classes rather than spreading evenly.

2. Use __get and __set. Legitimate when the dynamic behaviour is deliberate: an ORM row object, an API response wrapper, a config bag where the keys genuinely are not known at author time. Implement the magic methods and back them with an explicit array. You keep the flexibility and the intent becomes visible in the class, which is the real win.

3. Add #[AllowDynamicProperties]. The attribute PHP shipped alongside the deprecation, which silences it for one class:

#[\AllowDynamicProperties]
class LegacyThing { }

This is a snooze button, and worth being honest about that. It is the right call for vendor code you cannot change, for a class you are about to delete, or to buy a quiet log while you do the real work in order. It is the wrong call as a global find-and-replace, because you have then paid the cost of the upgrade and kept none of the benefit, and the next major version will ask again.

What is exempt

A few things never trigger this, which saves some pointless hunting. stdClass is exempt by design, so json_decode() results and casual (object) casts are fine. Any class already implementing __get and __set is handling it itself. Properties created through ArrayObject or similar containers are not dynamic properties in this sense.

Worth knowing because a large share of what looks alarming in the log on day one turns out to be a handful of real classes repeated across thousands of requests.

Finding all of them before the upgrade

Do not wait for production to tell you. Static analysis catches most of these without running anything: PHPStan at level 2 and above reports writes to undeclared properties, and Rector has a rule that adds the declarations for you. Run them against your own code first, then decide what to do about vendor packages.

To see what your running code actually hits, make deprecations visible on staging and nowhere else:

; staging php.ini only
error_reporting = E_ALL
log_errors = On
display_errors = Off

Exercise the real paths, then sort the log by class. In most codebases two or three classes account for nearly everything, and fixing those takes an afternoon rather than the week the raw line count suggests.

The real cost is the noise

Here is why this is worth clearing rather than ignoring, and it has nothing to do with PHP 9.

A log with ten thousand harmless deprecations in it is a log nobody reads. The fatal that actually matters arrives at 3am and lands in the middle of the noise, where it looks like everything else.

That is the practical damage. Not the deprecation, which breaks nothing, but the habit it creates of scrolling past your own error log. Teams that let this accumulate stop treating the log as a signal, and then an outage costs hours rather than minutes because the evidence was always there and nobody was looking.

Clear the deprecations, keep the log boring, and whatever watches it stays useful. For the other PHP fatal that tends to appear right after a version bump, see allowed memory size exhausted. If you would rather something else read the log, that is what Patcherly is for.

Found something wrong in here? Tell me.

Checking API...
Checking AI Gateway...
Check status
⚠️ Setup Required