Skip to main content

Fixing 'Creation of dynamic property is deprecated' Errors in PHP 8.2+ Legacy Code

 If you have recently upgraded your production environment to PHP 8.2 or 8.3, you likely woke up to an error log file explicitly inflating by the gigabyte. The culprit is almost always this warning:

Deprecated: Creation of dynamic property ClassName::$propertyName is deprecated

For over two decades, PHP allowed developers to assign values to undeclared properties on objects. It was a feature of the language's dynamic nature, frequently used in CMS plugins (like WordPress), ORMs, and rapid application development frameworks. However, as of PHP 8.2, this behavior is formally deprecated. While it is currently a warning, it will become a fatal Error in PHP 9.0.

This guide details exactly why this happens at the interpreter level and provides three architectural strategies to fix it without rewriting your entire codebase.

The Root Cause: Object Shapes and Hash Tables

To understand the fix, you must understand the optimization strategy behind the change. Modern PHP engines (Zend) rely heavily on distinct object shapes.

When a class is defined, PHP creates a blueprint (a shape) consisting of the declared properties. When you instantiate that class, the engine allocates memory specifically for those properties. This allows for optimized property access because the offset of the data is known at compile time.

When you dynamically assign a property that does not exist in the class definition:

  1. PHP cannot store the value in the optimized memory slot.
  2. It must create a separate dynamic hash table attached to that specific object instance.
  3. It breaks the "shape" of the object, preventing polymorphic caching optimizations in the Just-In-Time (JIT) compiler.

Furthermore, dynamic properties are the primary source of silent bugs caused by typos (e.g., assigning $user->nmae = 'John' instead of $user->name).

Note: The stdClass object and classes implementing the __get and __set magic methods are exempt from this deprecation.

The Fixes

We will look at three solutions ranging from "Architecturally Pure" to "Legacy Patch."

Scenario: The Legacy Data Mapper

Consider this typical legacy pattern often found in API consumers or WordPress plugins:

<?php

namespace App\Legacy;

class UserDataMapper
{
    public int $id;
    public string $name;

    public function __construct(array $data)
    {
        $this->id = $data['id'];
        $this->name = $data['name'];
        
        // THE PROBLEM: 
        // If $data contains 'meta_key', this assignment triggers the deprecation
        // because $meta_key is not defined in the class.
        if (isset($data['meta'])) {
            foreach ($data['meta'] as $key => $val) {
                $this->{$key} = $val; 
            }
        }
    }
}

Solution 1: Explicit Property Definition (The Ideal Fix)

If you own the class and the properties are known, the only correct engineering solution is to explicitly declare them. This improves type safety and IDE autocompletion.

<?php

declare(strict_types=1);

namespace App\Modern;

class UserDataMapper
{
    public int $id;
    public string $name;
    // Explicitly declare the property using nullable types or union types
    public string|null $role = null;
    public array $settings = [];

    public function __construct(array $data)
    {
        $this->id = $data['id'];
        $this->name = $data['name'];
        
        // Now valid
        $this->role = $data['role'] ?? null;
        $this->settings = $data['settings'] ?? [];
    }
}

Solution 2: The #[AllowDynamicProperties] Attribute (The Legacy Patch)

If you are maintaining a massive legacy codebase (like a custom WordPress plugin or an old Laravel/Symfony app) and cannot define every potential property, PHP 8.2 introduces a specific attribute to opt-out of this deprecation.

This tells the engine: "I intend for this class to be dynamic; do not warn me."

<?php

namespace App\Legacy;

// 1. Import the Attribute
use AllowDynamicProperties;

// 2. Apply it to the class
#[AllowDynamicProperties]
class FlexObject
{
    public string $name;
}

$obj = new FlexObject();
$obj->name = 'Production';

// This is now VALID and produces NO warning in PHP 8.2+
$obj->undefined_property = 'I am dynamic';

This is the recommended approach for updating 3rd-party libraries or legacy classes that act as generic data containers.

Solution 3: Using stdClass or Composition (The Refactor)

If a class exists solely to hold arbitrary data, it should not be a custom class definition unless it contains logic. PHP 8.2 fully allows dynamic properties on stdClass.

Instead of extending a custom class to hold data, compose your logic to hold a data bag.

<?php

declare(strict_types=1);

namespace App\Services;

use stdClass;

class ConfigLoader
{
    // Type the property as stdClass
    private stdClass $config;

    public function __construct(array $rawConfig)
    {
        // Cast array to object (stdClass)
        // stdClass natively supports dynamic properties without warnings
        $this->config = (object) $rawConfig;
    }

    public function get(string $key): mixed
    {
        return $this->config->{$key} ?? null;
    }

    public function set(string $key, mixed $value): void
    {
        // Perfectly legal in PHP 8.2+
        $this->config->{$key} = $value;
    }
}

Why This Matters for WordPress Developers

WordPress relies heavily on global objects and loosely typed structures (like $post objects). If you are writing a plugin that attaches arbitrary data to an existing class instance, you generally cannot modify the core class to add #[AllowDynamicProperties].

In these cases, you cannot assign properties directly to the object anymore. You must change your strategy to use a separate container or array.

Incorrect (PHP < 8.2):

function add_runtime_meta($post) {
    // Deprecated warning!
    $post->custom_plugin_flag = true; 
    return $post;
}

Correct (PHP 8.2+): You must treat the object as immutable regarding structure, or ensure the class (usually WP_Post) is updated by the core team. Since you cannot wait for core updates, switch to weak maps or local storage for runtime flags.

class PostFlagManager {
    // Use WeakMap to associate data with objects without modifying the object
    // and without causing memory leaks.
    private \WeakMap $flags;

    public function __construct() {
        $this->flags = new \WeakMap();
    }

    public function setFlag(object $post, bool $value): void {
        $this->flags[$post] = $value;
    }

    public function getFlag(object $post): bool {
        return $this->flags[$post] ?? false;
    }
}

Summary

  1. Strict Typing: The deprecation forces better code quality. Define your properties.
  2. Quick Fix: Use #[AllowDynamicProperties] for legacy classes you control but cannot easily refactor.
  3. Data Bags: Use stdClass or Arrays for truly unstructured data.
  4. Runtime Associations: Use WeakMap if you need to attach data to an object you do not own.

Ignoring these logs will fill your disk space today and crash your application in PHP 9.0. Apply the #[AllowDynamicProperties] attribute now to silence the logs, then schedule technical debt sprint to properly type your classes.