PHP 8.2 was officially released on 8 December 2022. A month of using readonly classes in new modules and patching dynamic properties in older code gives a clear practical picture. I show what readonly classes look like in real Magento 2 code, the most common dynamic property violations and how to fix them with Rector, and a migration checklist for upgrading existing projects.
Readonly classes in production code – one month later
<?php
declare(strict_types=1);
// Before PHP 8.2 - manual readonly on each property
final class OrderCreatedEvent
{
public function __construct(
public readonly int $orderId,
public readonly int $customerId,
public readonly float $grandTotal,
public readonly string $currency,
public readonly \DateTimeImmutable $createdAt,
) {}
}
// PHP 8.2 - readonly on the class, no repetition
final readonly class OrderCreatedEvent
{
public function __construct(
public int $orderId,
public int $customerId,
public float $grandTotal,
public string $currency,
public \DateTimeImmutable $createdAt,
) {}
}
// Where I now use readonly classes by default:
// - Domain events (OrderCreated, ProductUpdated, ...)
// - Command objects for CommandBus
// - Value Objects (Money, Address, ProductId, ...)
// - DTO classes for API responses
// - Configuration objects
Dynamic properties – common patterns and fixes
<?php
// PATTERN 1: DataObject subclasses - Magento's own classes use getData/setData
// These use magic methods (__get/__set) which ARE fine - not dynamic properties
class MyBlock extends \Magento\Framework\DataObject
{
// Fine - DataObject handles this through internal $this->_data array
public function getProduct(): mixed
{
return $this->getData('product'); // not a dynamic property
}
}
// PATTERN 2: Custom data containers - actually setting dynamic properties
class OrderData
{
// PHP 8.2 Warning: Creation of dynamic property
public function __construct(array $data)
{
foreach ($data as $key => $value) {
$this->$key = $value; // Dynamic property!
}
}
}
// Fix: use an internal array
class OrderData
{
private array $data = [];
public function __construct(array $data) { $this->data = $data; }
public function __get(string $name): mixed { return $this->data[$name] ?? null; }
public function __set(string $name, mixed $value): void { $this->data[$name] = $value; }
public function __isset(string $name): bool { return isset($this->data[$name]); }
}
// PATTERN 3: Test classes - tests often assign dynamic properties to mock setup
// Fix: add #[AllowDynamicProperties] to the test class
#[\AllowDynamicProperties]
class MyTest extends \PHPUnit\Framework\TestCase
{
// dynamic property assignments in setUp() are OK here
}
Rector for automatic migration
# Find all dynamic property violations
vendor/bin/phpstan analyse app/code/ --level=2 --php-version=8.2 \
--error-format=table 2>&1 | grep "dynamic"
# Rector: automatically add #[AllowDynamicProperties] where needed
vendor/bin/rector process app/code/ \
--only=\Rector\Php82\Rector\Class_\AllowDynamicPropertiesAttributeRector \
--dry-run
# Rector: convert readonly class candidates
vendor/bin/rector process app/code/ \
--only=\Rector\Php82\Rector\Class_\ReadOnlyClassRector \
--dry-run
# Full PHP 8.2 ruleset
vendor/bin/rector process app/code/ --php-version=8.2 --dry-run
PHP 8.2 migration checklist
| Item | Command | Priority |
|---|---|---|
| Find dynamic properties | PHPStan level 2 | High |
| Fix dynamic properties | Rector AllowDynamicPropertiesAttributeRector | High |
| Check deprecated utf8_encode/decode | grep -r “utf8_encode\|utf8_decode” src/ | Medium |
| Remove ${var} string interpolation | PHPStan / IDE inspection | Medium |
| Test Composer dependencies | composer update –dry-run | High |
| Run full test suite | vendor/bin/phpunit | High |
# Deprecated in PHP 8.2 - replace with mb_ equivalents
grep -rn "utf8_encode\|utf8_decode" app/code/
# utf8_encode() -> mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1')
# utf8_decode() -> mb_convert_encoding($str, 'ISO-8859-1', 'UTF-8')
# ${var} string interpolation deprecated - use {$var}
grep -rn '\${\w\+}' app/code/ | grep -v '//'
# "Hello ${name}" -> "Hello {$name}"
Summary
PHP 8.2 is a smooth upgrade compared to the 7.x to 8.0 jump. The main work is finding and fixing dynamic properties – Rector handles most of them automatically. Readonly classes are immediately useful for DTOs and value objects. For Magento 2 projects: test thoroughly on staging, check all third-party modules for PHP 8.2 compatibility, and pay attention to dynamic properties in model classes that use DataObject patterns.
