PHP Killed Dynamic Properties. Here's Why (And What to Do)

Published: (December 17, 2025 at 12:44 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

What Are Dynamic Properties?

Dynamic properties are properties added to an object without being declared in its class.

class User {}

$user = new User();
$user->age = 30; // ⚠️ Dynamic property

The Problem

Silent Typos → Hidden Bugs

$user->custmer_email = 'john@example.com'; // Typo!
echo $user->customer_email; // null 😱

No Type Safety

$user->age = 30;        // int
$user->age = 'thirty'; // string – no error
$user->age = null;     // also "fine"

IDE & Tooling Limitations

  • No autocomplete
  • No refactoring support
  • Static analysis tools cannot detect issues

Architecture Decay

// Anemic domain model
$user->cached_data = $something;
$user->temp_flag   = true;
$user->random_stuff = $whatever;

The Fix: Declare Properties Explicitly

Modern PHP Way

class User {
    public string $name;
    public int    $age;
    public bool   $isActive;
}

Constructor Property Promotion (PHP 8+)

class CreateOrderRequest {
    public function __construct(
        public readonly string $customerId,
        public readonly array  $items,
        public readonly ?string $promoCode = null,
    ) {}
}

Laravel‑Specific Solutions

❌ Don’t Add Undefined Properties Directly

$user = User::find(1);
$user->is_verified = true; // Undefined property!

✅ Define Fillable Attributes and Casts

class User extends Model {
    protected $fillable = [
        'name',
        'email',
        'is_verified',
    ];

    protected $casts = [
        'is_verified'       => 'boolean',
        'email_verified_at'=> 'datetime',
    ];
}

✅ Use Accessors

use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model {
    protected function fullName(): Attribute {
        return Attribute::make(
            get: fn() => "{$this->first_name} {$this->last_name}",
        );
    }
}

The Escape Hatch (Use Sparingly!)

#[\AllowDynamicProperties]
class LegacyCode {
    // Opt into old behavior
}

Use this only for:

  • Legacy vendor code
  • Gradual migration (with a plan to remove)

Never use it for new code.

Migration Strategy

  1. Run Static Analysis

    ./vendor/bin/phpstan analyze --level 8
    ./vendor/bin/psalm --no-cache
  2. Fix Systematically

    • Critical – Domain models (fix now)
    • High – Controllers / Services (next sprint)
    • Medium – DTOs (incremental)
    • Low – Test utilities (add attribute if needed)
  3. Prevent Regression

    Add static analysis to CI:

    # .github/workflows/ci.yml
    - name: Static Analysis
      run: ./vendor/bin/phpstan analyze

Version Status

PHP versionStatus
8.1✅ Allowed
8.2⚠️ Deprecated (warning)
9.0❌ Fatal error (removed)

Why This Change Matters

Modern PHP is moving toward:

  • Explicit over implicit
  • Type safety by default
  • Better tooling support
  • Refactor‑friendly code

Dynamic properties conflict with these goals.

Real Example: Before & After

Before ❌

class ApiResponse {}

$response = new ApiResponse();
$response->data   = $data;
$response->status = 200;

After ✅

class ApiResponse {
    public function __construct(
        public readonly mixed $data,
        public readonly int   $status,
        public readonly int   $timestamp = time(),
    ) {}
}

$response = new ApiResponse($data, 200);

Benefits: type‑safe, self‑documenting, immutable, and refactor‑friendly.

Key Takeaways

  • Declare all properties explicitly.
  • Use typed properties for safety.
  • Leverage PHP 8+ features (constructor promotion, readonly).
  • Run static analysis regularly.
  • Plan a migration path for legacy code.

For the full guide—including historical context, detailed migration strategies, advanced Laravel patterns, and more—read the original article on Medium:

The Death of Dynamic Properties in PHP – A Deep Dive into Modern Object Design

Back to Blog

Related posts

Read more »