The Death of Dynamic Properties in PHP: A Deep Dive into Modern Object Design
PHP Dynamic Properties: Deprecated in 8.2, Removed in 9.0 — A Complete Guide
The Death of Dynamic Properties in PHP: A Deep Dive into Modern Object Design

The Death of Dynamic Properties in PHP: A Deep Dive into Modern Object Design
For years, PHP’s permissive approach to object properties was both a convenience and a curse. You could attach properties to objects on the fly, without declaring them in your class definition. This flexibility felt liberating — until it became a maintenance nightmare.
With PHP 8.2, the language community made a decisive move: dynamic properties were officially deprecated. Come PHP 9.0, they’ll be gone entirely. This isn’t just another breaking change to grumble about — it’s a fundamental shift in how we think about object-oriented design in PHP.
If you’re building Laravel applications, working with legacy codebases, or simply trying to write better PHP, understanding this change is crucial. This article explores not just the what and when, but the deeper why — and what it means for the future of PHP development.
The Phantom Properties Problem
Let’s start with a simple example that illustrates why dynamic properties are problematic:
class Order {
public string $id;
public float $total;
}
$order = new Order();
$order->id = 'ORD-12345';
$order->total = 99.99;
$order->custmer_email = 'john@example.com'; // Typo!
Did you catch the typo? custmer_email instead of customer_email. In traditional PHP, this code runs without complaint. The property gets created dynamically, silently storing data in the wrong place. Later, when you try to access $order->customer_email, it's null—and you're left debugging phantom data issues.
This is the core problem: dynamic properties allow invisible errors to propagate through your codebase.
A Brief History of PHP’s Permissiveness
To understand why dynamic properties existed in the first place, we need context. Early PHP (versions 4 and 5) prioritized rapid development over strict correctness. The language was designed for building quick web applications, not large-scale enterprise systems.
Dynamic properties fit this philosophy perfectly. Need a temporary value? Just slap it on the object. No ceremony, no boilerplate. For small scripts, this worked fine.
But as PHP matured and applications grew more complex, this flexibility became a liability. The same feature that made prototyping fast made maintenance painful. Teams struggled with:
- Unclear data contracts: What properties does this object actually have?
- Invisible state mutations: Where did this property come from?
- Refactoring paralysis: Will changing this class name break code that dynamically adds properties?
Modern PHP is moving away from this “move fast and break things” mentality toward predictability, type safety, and developer tooling. Dynamic properties no longer fit this vision.
The Four Horsemen of Dynamic Property Problems
1. Type Safety Evaporation
Consider this seemingly innocent code:
class UserProfile {
// No properties declared
}
$profile = new UserProfile();
$profile->age = 30; // Looks fine
$profile->age = 'thirty'; // Also "fine"
$profile->age = null; // Still "fine"
$profile->age = new stdClass(); // PHP shrugs
Without declared properties, PHP has no type information to enforce. Your age field can morph from integer to string to object without warning. Type hints on method parameters can't save you here—by the time you pass $profile->age to a typed method, the damage is done.
This creates a trust problem in your codebase. You can’t rely on objects maintaining their invariants.
2. Tooling Blindness
Modern PHP development relies heavily on IDE intelligence and static analysis tools like PHPStan and Psalm. These tools work by analyzing your code structure — what properties exist, what types they have, what methods are available.
Dynamic properties are invisible to these tools:
$user->firstName = 'John'; // No autocomplete
$user->firstNme; // No error warning about typo
Your IDE can’t help you write correct code, and your static analyzer can’t catch bugs before they reach production. You’re coding blind.
3. The Typo Time Bomb
This is perhaps the most insidious issue. A simple typo creates a working program that silently does the wrong thing:
// Intended code
$invoice->paymentStatus = 'paid';
// Actual code (typo in property name)
$invoice->paymnetStatus = 'paid';
// Later...
if ($invoice->paymentStatus === 'paid') { // null !== 'paid'
sendReceipt($invoice);
}
The receipt never sends. The customer complains. You spend an hour debugging. The typo was invisible because PHP happily created a new property with the misspelled name.
In large Laravel applications with dozens of models and hundreds of properties, these bugs are devastatingly difficult to track down.
4. Architectural Decay
Dynamic properties encourage poor object-oriented design:
// Anemic domain model
class User {
public string $name;
}
// Later, somewhere else in the codebase
$user = User::find($id);
$user->cached_permissions = $this->calculatePermissions($user);
$user->last_accessed = now();
$user->temporary_flag = true;
This violates several OOP principles:
- Single Responsibility: The
Userobject is being used as a dumping ground for unrelated data - Encapsulation: Internal state is exposed and modified arbitrarily
- Intention-Revealing Names: The class definition doesn’t reflect its actual usage
Over time, objects become bags of properties with no clear boundary or purpose. Your domain model becomes anemic, and business logic scatters across the codebase.
The Timeline of Change

This gives the community a migration window. PHP 8.2 acts as a transition period where you can identify problem areas without breaking existing code. By PHP 9.0, the training wheels come off.
For Laravel developers, this timeline is particularly important. Laravel 11 targets PHP 8.2+, meaning deprecation warnings are now active. If you’re seeing these warnings, it’s time to act — before PHP 9.0 makes them errors.
The Right Way Forward: Explicit Property Declaration
The modern PHP approach is simple: declare what you mean.
Basic Declaration
class Product {
public string $name;
public float $price;
public bool $inStock;
}
This tells PHP, your IDE, and future maintainers: “A Product has exactly these three properties, with these exact types.”
Constructor Property Promotion (PHP 8.0+)
For DTOs and value objects, constructor property promotion is elegant:
class CreateOrderRequest {
public function __construct(
public readonly string $customerId,
public readonly array $items,
public readonly ?string $promoCode = null,
) {}
}
This combines declaration, initialization, and immutability in a concise syntax. It’s particularly useful for request objects, events, and commands.
Typed Properties with Defaults
class ShoppingCart {
public array $items = [];
public float $subtotal = 0.0;
public ?string $couponCode = null;
}
Defaults make objects safe to instantiate without constructors, while maintaining type safety.
Laravel-Specific Patterns
Laravel developers face unique challenges with this change because Eloquent models historically relied on some dynamic behavior.
Common Laravel Anti-Pattern
// ❌ Don't do this
$user = User::find(1);
$user->is_verified = true; // Undefined property
$user->verification_token = Str::random(32);
$user->save();
If these properties aren’t in your database schema or declared in the model, you’re creating dynamic properties.
The Laravel-Correct Approach
Option 1: Database Columns + Casts
class User extends Model {
protected $fillable = [
'name',
'email',
'is_verified',
'verification_token',
];
protected $casts = [
'is_verified' => 'boolean',
'email_verified_at' => 'datetime',
];
}
Option 2: Accessor for Computed Properties
class User extends Model {
protected $appends = ['full_name'];
public function getFullNameAttribute(): string {
return "{$this->first_name} {$this->last_name}";
}
}
Option 3: Custom Attributes (PHP 8+)
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}",
);
}
}
All three approaches maintain type safety while working with Eloquent’s magic.
The AllowDynamicProperties Escape Hatch
PHP 8.2 introduced the #[AllowDynamicProperties] attribute as a temporary bridge:
#[\AllowDynamicProperties]
class LegacyDataTransferObject {
// Opt into old behavior
}
When to Use It
This attribute should be rare and intentional. Acceptable use cases:
- Legacy vendor code you cannot modify
- Gradual migration of large codebases (with a plan to remove it)
- Interop with dynamic systems (rare cases like testing frameworks)
When NOT to Use It
❌ New code ❌ Business logic objects ❌ Domain models ❌ As a permanent solution
Think of #[AllowDynamicProperties] as a "code smell suppressor"—it masks the problem rather than solving it. Use it sparingly, document why, and plan its removal.
Migration Strategies for Large Codebases
If you’re maintaining a legacy Laravel application, the path forward requires strategy.
Step 1: Detect Dynamic Properties
Use static analysis:
# PHPStan
./vendor/bin/phpstan analyze --level 8
# Psalm
./vendor/bin/psalm --no-cache
Both tools will flag dynamic property usage in PHP 8.2+.
Step 2: Categorize Issues
Create a migration plan based on severity:
- Critical: Core domain models (fix immediately)
- High: Controllers and services (fix in next sprint)
- Medium: DTOs and value objects (refactor incrementally)
- Low: Test utilities (add attribute if necessary)
Step 3: Refactor Systematically
For each class:
- List all property assignments in the codebase
- Determine intended types
- Add property declarations
- Run tests
- Remove
#[AllowDynamicProperties]if present
Step 4: Prevent Regression
Add to your CI pipeline:
# .github/workflows/ci.yml
- name: Static Analysis
run: ./vendor/bin/phpstan analyze --error-format=github
This prevents new dynamic properties from entering the codebase.
Real-World Case Study: Refactoring a DTO
Before (Dynamic Properties)
class ApiResponse {
// Nothing declared
}
function buildResponse($data, $status) {
$response = new ApiResponse();
$response->data = $data;
$response->status = $status;
$response->timestamp = time();
return $response;
}
Problems:
- No type safety
- Unclear structure
- Easy to introduce typos
- Hard to refactor
After (Explicit Declaration)
class ApiResponse {
public function __construct(
public readonly mixed $data,
public readonly int $status,
public readonly int $timestamp = time(),
) {}
}
function buildResponse($data, $status): ApiResponse {
return new ApiResponse($data, $status);
}
Benefits:
- Type-safe
- Self-documenting
- Immutable by default
- Refactor-friendly
The refactored version is clearer, safer, and more maintainable.
Beyond Dynamic Properties: The Bigger Picture
This deprecation is part of a broader evolution in PHP’s design philosophy:
PHP’s Modern Design Principles
- Explicit over implicit: Declare intentions clearly
- Type safety by default: Use type hints everywhere
- Tooling-first: Code should be analyzable by machines
- Immutability when possible: Reduce accidental state changes
These principles are reflected in recent PHP features:
- Typed properties (PHP 7.4)
- Union types (PHP 8.0)
- Constructor property promotion (PHP 8.0)
- Readonly properties (PHP 8.1)
- Enums (PHP 8.1)
Dynamic properties don’t align with any of these principles. Their removal is inevitable and necessary.
Preparing for PHP 9.0
PHP 9.0 will throw a fatal error for dynamic properties (unless #[AllowDynamicProperties] is present). Here's how to prepare:
Immediate Actions
- Upgrade to PHP 8.2 if possible to surface warnings
- Enable strict error reporting in development
- Run static analysis regularly
- Audit your models for undeclared properties
Long-Term Strategy
- Establish coding standards that forbid dynamic properties
- Train your team on modern PHP patterns
- Refactor incrementally rather than in one big-bang migration
- Document architectural decisions about object design
Final Thoughts: Embracing Constraint
The removal of dynamic properties might feel restrictive at first. But constraints often lead to better design.
By forcing developers to think explicitly about object structure, PHP 8.2+ encourages:
- Clearer domain models that reflect business reality
- Stronger type safety that catches bugs at compile time
- Better tooling support that boosts productivity
- Easier refactoring that reduces fear of change
This change aligns PHP with other modern languages like TypeScript, Rust, and Swift — languages that prioritize developer experience through strong type systems.
For Laravel developers specifically, this is an opportunity to revisit your models, clean up technical debt, and embrace modern PHP patterns. Your codebase will be more maintainable, your bugs will be easier to find, and your team will be more productive.
The death of dynamic properties isn’t the end of something — it’s the beginning of better PHP.
Hi ,I write about Laravel & PHP best practices.
If you found this helpful:
— Follow me for more Laravel deep-dives
— Subscribe to get my articles in your inbox Got questions?
Drop them in the comments below! 👇
메타데이터
- post_id
- d26b43c8ffa0
- slug
- the-death-of-dynamic-properties-in-php-a-deep-dive-into-modern-object-design-d26b43c8ffa0
- url
- https://medium.com/@masteryoflaravel/the-death-of-dynamic-properties-in-php-a-deep-dive-into-modern-object-design-d26b43c8ffa0
- canonical_url
- https://medium.com/@masteryoflaravel/the-death-of-dynamic-properties-in-php-a-deep-dive-into-modern-object-design-d26b43c8ffa0
- author_url
- https://medium.com/@masteryoflaravel
- status
- ok
- fetched_at
- 2026-06-28 04:42:08