DEV Community

Cover image for PHP Killed Dynamic Properties. Here's Why (And What to Do)
Laravel Mastery
Laravel Mastery

Posted on

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

Starting with PHP 8.2, dynamic properties are deprecated. In PHP 9.0, they're completely gone.
If you're seeing deprecation warnings in your Laravel 11 projects, this is why.

What Are Dynamic Properties?

Properties added to objects without being declared in the class:

class User {}

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

The Problem

  1. Silent Typos = Hidden Bugs
$user->custmer_email = 'john@example.com'; // Typo!
// Later...
echo $user->customer_email; // null 😱
Enter fullscreen mode Exit fullscreen mode
  1. No Type Safety
$user->age = 30;        // int
$user->age = 'thirty';  // string - no error!
$user->age = null;      // also "fine"
Enter fullscreen mode Exit fullscreen mode
  1. IDE Can't Help You

No autocomplete
No refactoring support
Static analysis tools are blind

  1. Architecture Decay
// Anemic domain model
$user->cached_data = $something;
$user->temp_flag = true;
$user->random_stuff = $whatever;
Enter fullscreen mode Exit fullscreen mode

The Fix: Declare Properties Explicitly

βœ… Modern PHP Way

class User {
    public string $name;
    public int $age;
    public bool $isActive;
}
Enter fullscreen mode Exit fullscreen mode

βœ… Constructor Property Promotion (PHP 8+)

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

Laravel-Specific Solutions

❌ Don't Do This

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

βœ… Do This Instead

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

    protected $casts = [
        'is_verified' => 'boolean',
        'email_verified_at' => 'datetime',
    ];
}
Enter fullscreen mode Exit fullscreen mode

βœ… Or 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}",
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The Escape Hatch (Use Sparingly!)

#[\AllowDynamicProperties]
class LegacyCode {
    // Opt into old behavior
}
Enter fullscreen mode Exit fullscreen mode

Only use this for:

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

Never use for new code!

Migration Strategy

./vendor/bin/phpstan analyze --level 8
./vendor/bin/psalm --no-cache
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Version Status
PHP 8.1 βœ… Allowed
PHP 8.2 ⚠️ Deprecated warning
PHP 9.0 ❌ Fatal error

Why This Change Matters

Modern PHP is moving toward:

βœ… Explicit over implicit
βœ… Type safety by default
βœ… Better tooling support
βœ… Refactor-friendly code

Dynamic properties don't fit this vision.

Real Example: Before & After

Before ❌

class ApiResponse {}

$response = new ApiResponse();
$response->data = $data;
$response->status = 200;
Enter fullscreen mode Exit fullscreen mode

After βœ…

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

$response = new ApiResponse($data, 200);
Enter fullscreen mode Exit fullscreen mode

Benefits: Type-safe, self-documenting, immutable, 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 migration for legacy code

This is a condensed version. For the complete guide including:

Historical context of PHP's design evolution
Detailed migration strategies for large codebases
Advanced Laravel patterns
Philosophical discussion on constraints
More real-world examples

πŸ‘‰ Read the full article on Medium:

php #laravel #webdev #programming #backend #php8 #cleancode #softwaredevelopment

Top comments (3)

Collapse
 
xwero profile image
david duymelinck • Edited

A few addendums:

  • The AllowDynamicProperties attributes is fine to use on new classes, if that is the best option. Most of the time the best option is defined properties. If the names are dynamic but follow a pattern the __get and __set methods can be used. When the dynamic names are unpredictable then go for the attribute.

  • If you read the RFC, you will be able to use dynamic properties on a stdClass instance in PHP 9

  • For the Eloquent model, adding properties is just fine because it has a __get and _set function to intercept the dynamic properties.

I think it is guiding developers to write better code, but at the same time not making the language too rigid.

Collapse
 
laravel_mastery_ffd9d10ec profile image
Laravel Mastery

Good points πŸ‘
I agree β€” PHP is not trying to ban dynamic properties entirely, but rather encourage more intentional and predictable designs.

Defined properties should be the default, get/set work well when there’s a clear pattern, and #[AllowDynamicProperties] is a reasonable escape hatch for truly unpredictable cases.

Frameworks like Eloquent already handle this safely via magic methods, so they are not really affected. Overall, this feels more like guidance toward better code than unnecessary rigidity.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.