What Changed in PHP 8.4 and Why It Matters
PHP 8.4 is not another minor release with a couple of syntax sugar additions. Three fundamental changes alter how you write code: property hooks, asymmetric visibility, and mature Fiber support. Plus a dozen smaller improvements that individually seem trivial but collectively eliminate entire categories of boilerplate.
I've worked with PHP since my first commercial projects — Laravel, plain PHP, some Symfony. Over the past two years my primary stack shifted toward Rails, but PHP projects haven't gone anywhere: two large Laravel monoliths in production, plus microservices. Both upgraded to 8.4 — here's what actually proved useful.
Property Hooks — Goodbye, Magic Getters
The Problem They Solve
Before 8.4, PHP had two ways to control property access: public fields (no control) and magic __get/__set (control, but no autocomplete, no typing, and useless stack traces).
// Old approach: __get/__set
class User
{
private string $email;
public function __get(string $name): mixed
{
return match ($name) {
'email' => $this->email,
default => throw new \InvalidArgumentException("Unknown property: {$name}"),
};
}
public function __set(string $name, mixed $value): void
{
match ($name) {
'email' => $this->email = strtolower(trim($value)),
default => throw new \InvalidArgumentException("Unknown property: {$name}"),
};
}
}
Does it work? Yes. But the IDE doesn't know about the email property, PHPStan complains, and the stack trace on error points to __get, not the specific property. In a project with 200+ models this turns into a nightmare.
New Syntax
class User
{
public string $email {
get => $this->email;
set(string $value) => strtolower(trim($value));
}
public string $displayName {
get => "{$this->firstName} {$this->lastName}";
}
public function __construct(
public string $firstName,
public string $lastName,
string $email,
) {
$this->email = $email;
}
}
$user = new User('Vadim', 'Bobkov', ' Vadim@Example.COM ');
echo $user->email; // vadim@example.com
echo $user->displayName; // Vadim Bobkov
Property hooks are native getters and setters built right into the property declaration. No magic, full typing, autocomplete, and proper stack traces.
Real Production Use Cases
1. Data normalization on write
class Product
{
public string $sku {
set(string $value) => strtoupper(trim($value));
}
public int $priceInCents {
set(int $value) {
if ($value < 0) {
throw new \DomainException('Price cannot be negative');
}
$this->priceInCents = $value;
}
}
// Computed property — no backing store
public float $priceFormatted {
get => $this->priceInCents / 100;
}
}
Notice: $priceFormatted doesn't store a value. It's a pure computed property. Unlike __get, the IDE knows its type, PHPStan validates it, and accessing {{ $product->priceFormatted }} in templates works without additional accessors.
2. Lazy loading with caching
class OrderReport
{
private ?array $cachedMetrics = null;
public array $metrics {
get {
if ($this->cachedMetrics === null) {
$this->cachedMetrics = $this->calculateMetrics();
}
return $this->cachedMetrics;
}
}
private function calculateMetrics(): array
{
// Heavy query to ClickHouse
return DB::connection('clickhouse')
->select('SELECT sum(amount), count(*) FROM orders WHERE report_id = ?', [$this->id]);
}
}
3. Validation with domain rules
class Employee
{
public string $taxId {
set(string $value) {
// INN: 10 or 12 digits
if (!preg_match('/^\d{10}(\d{2})?$/', $value)) {
throw new \DomainException('Invalid tax ID format');
}
$this->taxId = $value;
}
}
public \DateTimeImmutable $hireDate {
set(\DateTimeImmutable $value) {
if ($value > new \DateTimeImmutable()) {
throw new \DomainException('Hire date cannot be in the future');
}
$this->hireDate = $value;
}
}
}
Property Hooks vs get/set — Comparison
| Criterion | Property Hooks | get/set |
|---|---|---|
| IDE Autocomplete | Full | Requires @property PHPDoc |
| Static Analysis | PHPStan level 9 | Partial, via plugins |
| Stack Trace | Points to property | Points to __get |
| Performance | Native opcode | Fallback through magic |
| Inheritance | Override individual hooks | Override entire __get |
| Interfaces | Properties in interfaces | Only via @property |
Properties in Interfaces
This is a separate big topic. PHP 8.4 allows declaring properties in interfaces:
interface HasFullName
{
public string $fullName { get; }
}
class User implements HasFullName
{
public string $fullName {
get => "{$this->firstName} {$this->lastName}";
}
public function __construct(
public readonly string $firstName,
public readonly string $lastName,
) {}
}
class Company implements HasFullName
{
public string $fullName {
get => $this->legalName;
}
public function __construct(
public readonly string $legalName,
) {}
}
// Now you can type-hint properly
function greet(HasFullName $entity): string
{
return "Hello, {$entity->fullName}!";
}
Previously you needed a getFullName() method in the interface. Properties in interfaces aren't sugar — they're a new level of contracts.
Asymmetric Visibility — Control Writes Without Closing Reads
The Problem
Classic situation: a property needs to be readable from outside but writable only from inside the class. Before 8.4 — two options:
// Option 1: readonly (but can't change after constructor)
class User
{
public function __construct(
public readonly string $name,
) {}
}
// Option 2: private + getter
class User
{
private string $name;
public function getName(): string
{
return $this->name;
}
public function changeName(string $name): void
{
$this->name = $name;
}
}
readonly is too strict — you can't change the value even inside the class after construction. Private + getter works but it's boilerplate for every property.
New Syntax
class User
{
public private(set) string $name;
public protected(set) string $email;
public private(set) \DateTimeImmutable $createdAt;
public function __construct(string $name, string $email)
{
$this->name = $name;
$this->email = $email;
$this->createdAt = new \DateTimeImmutable();
}
public function changeName(string $newName): void
{
// Works — we're inside the class
$this->name = $newName;
}
}
$user = new User('Vadim', 'vadim@example.com');
echo $user->name; // Vadim — reading works
$user->name = 'Ivan'; // Error: Cannot modify private(set) property
$user->changeName('Ivan'); // OK — through method
Where This Is Actually Needed
1. DTOs and Value Objects
class MoneyTransfer
{
public private(set) float $amount;
public private(set) string $currency;
public private(set) string $status = 'pending';
public function __construct(float $amount, string $currency)
{
if ($amount <= 0) {
throw new \DomainException('Amount must be positive');
}
$this->amount = $amount;
$this->currency = strtoupper($currency);
}
public function markCompleted(): void
{
$this->status = 'completed';
}
public function markFailed(string $reason): void
{
$this->status = 'failed';
}
}
// Outside — read only
$transfer = new MoneyTransfer(100.50, 'usd');
echo $transfer->amount; // 100.5
echo $transfer->currency; // USD
echo $transfer->status; // pending
// IDE and templates see all properties
// but modification only through business methods
2. Eloquent Models (Laravel)
class Order extends Model
{
// Public read, write only through business logic
public private(set) string $status = 'draft';
public function submit(): void
{
if ($this->items()->count() === 0) {
throw new \DomainException('Cannot submit empty order');
}
$this->status = 'submitted';
$this->save();
event(new OrderSubmitted($this));
}
public function cancel(): void
{
if (!in_array($this->status, ['draft', 'submitted'])) {
throw new \DomainException("Cannot cancel order in status: {$this->status}");
}
$this->status = 'cancelled';
$this->save();
}
}
3. Combining with property hooks
class Temperature
{
public private(set) float $celsius {
set(float $value) {
if ($value < -273.15) {
throw new \DomainException('Temperature below absolute zero');
}
$this->celsius = $value;
}
}
public float $fahrenheit {
get => $this->celsius * 9/5 + 32;
}
public float $kelvin {
get => $this->celsius + 273.15;
}
public function __construct(float $celsius)
{
$this->celsius = $celsius;
}
}
$temp = new Temperature(36.6);
echo $temp->celsius; // 36.6
echo $temp->fahrenheit; // 97.88
echo $temp->kelvin; // 309.75
$temp->celsius = 100; // Error: private(set)
readonly vs private(set) — When to Use Which
| Scenario | readonly | private(set) |
|---|---|---|
| Value never changes after creation | Yes | Overkill |
| Value changes through methods | No — impossible | Yes |
| DTO from API/database | Yes | Yes |
| Eloquent/Doctrine models | No — ORM mutates fields | Yes |
| Event Sourcing state | No | Yes |
| Config / immutable settings | Yes | Overkill |
Simple rule: if the value is set once — readonly. If it can change but only through business logic — private(set).
Fibers — Async Without Callback Hell
What Are Fibers
Fibers appeared in PHP 8.1, but in 8.4 they became truly mature. A Fiber is a cooperative coroutine: a function that can pause its execution and return control to the caller.
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('first pause');
echo "Received: {$value}\n";
Fiber::suspend('second pause');
echo "Finished\n";
});
$result1 = $fiber->start(); // 'first pause'
$result2 = $fiber->resume('hello'); // prints "Received: hello", returns 'second pause'
$fiber->resume(); // prints "Finished"
A Fiber pauses via Fiber::suspend(), returning a value to the outside. The calling code continues via resume(), passing data back into the fiber.
Fibers vs Generators
Generators (yield) are similar but limited:
// Generator — only yields values outward
function readLines(string $file): Generator
{
$handle = fopen($file, 'r');
while ($line = fgets($handle)) {
yield trim($line);
}
fclose($handle);
}
// Fiber — full bidirectional communication
$fiber = new Fiber(function (): void {
$url = Fiber::suspend(); // receive URL from outside
$response = file_get_contents($url);
$parsed = json_decode($response, true);
Fiber::suspend($parsed); // send result outside
// continue with new data...
});
| Criterion | Generator | Fiber |
|---|---|---|
| Bidirectional data transfer | Limited (send) | Full |
| Nesting | yield from | Free |
| Stackful | No | Yes |
| Use case | Iterators, lazy collections | Async I/O, concurrency |
| Memory overhead | ~400 bytes | ~8 KB |
Practical Example: Parallel HTTP Requests
class AsyncHttpClient
{
private array $fibers = [];
private array $results = [];
public function addRequest(string $name, string $url, array $options = []): void
{
$this->fibers[$name] = new Fiber(function () use ($url, $options): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $options['timeout'] ?? 30,
CURLOPT_HTTPHEADER => $options['headers'] ?? [],
]);
if (isset($options['post'])) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($options['post']));
}
// Suspend fiber — curl handle is ready, but request not executed yet
Fiber::suspend($ch);
// When fiber resumes, curl_multi already executed the request
$response = curl_multi_getcontent($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status' => $httpCode,
'body' => $response,
'time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
];
});
}
public function execute(): array
{
$multiHandle = curl_multi_init();
$handles = [];
// Start all fibers — each creates a curl handle and suspends
foreach ($this->fibers as $name => $fiber) {
$ch = $fiber->start();
curl_multi_add_handle($multiHandle, $ch);
$handles[$name] = $ch;
}
// Execute all requests in parallel
do {
$status = curl_multi_exec($multiHandle, $active);
curl_multi_select($multiHandle);
} while ($active && $status === CURLM_OK);
// Resume fibers — each processes its own result
foreach ($this->fibers as $name => $fiber) {
$this->results[$name] = $fiber->resume();
}
curl_multi_close($multiHandle);
return $this->results;
}
}
// Usage
$client = new AsyncHttpClient();
$client->addRequest('users', 'https://api.example.com/users');
$client->addRequest('orders', 'https://api.example.com/orders');
$client->addRequest('products', 'https://api.example.com/products');
$results = $client->execute();
// All 3 requests executed in parallel, not sequentially
Fibers in Laravel / Symfony
Laravel: parallel data collection for dashboard
class DashboardController extends Controller
{
public function index(): View
{
$fibers = [
'stats' => new Fiber(fn() => Fiber::suspend(
Cache::remember('dashboard.stats', 60, fn() => [
'users' => User::count(),
'orders' => Order::whereMonth('created_at', now()->month)->count(),
'revenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
])
)),
'chart' => new Fiber(fn() => Fiber::suspend(
Order::selectRaw('DATE(created_at) as date, SUM(total) as total')
->where('created_at', '>=', now()->subDays(30))
->groupByRaw('DATE(created_at)')
->get()
)),
'recent' => new Fiber(fn() => Fiber::suspend(
Order::with('user')->latest()->limit(10)->get()
)),
];
$data = [];
foreach ($fibers as $key => $fiber) {
$data[$key] = $fiber->start();
}
return view('dashboard', $data);
}
}
Symfony: async message processing
class OrderProcessor
{
public function processBatch(array $orderIds): array
{
$fibers = [];
foreach ($orderIds as $id) {
$fibers[$id] = new Fiber(function () use ($id): array {
$order = $this->orderRepository->find($id);
// Stock check
$stockCheck = $this->stockService->check($order->items);
Fiber::suspend(); // yield processor
// Shipping calculation
$shipping = $this->shippingService->calculate($order);
Fiber::suspend();
// Send notification
$this->notifier->send($order->user, new OrderConfirmation($order));
return [
'order_id' => $id,
'stock_ok' => $stockCheck,
'shipping_cost' => $shipping->cost,
'status' => 'processed',
];
});
}
// Start all fibers
foreach ($fibers as $fiber) {
$fiber->start();
}
// Round-robin resumption
$results = [];
$active = true;
while ($active) {
$active = false;
foreach ($fibers as $id => $fiber) {
if (!$fiber->isTerminated()) {
$result = $fiber->resume();
if ($fiber->isTerminated()) {
$results[$id] = $fiber->getReturn();
}
$active = true;
}
}
}
return $results;
}
}
When NOT to Use Fibers
Fibers aren't a silver bullet. They're unnecessary when:
- Single HTTP request to an API — a plain
Http::get()is simpler - CPU-bound tasks — Fibers don't create threads, they're cooperative. For CPU you need
pcntl_forkor queues - Simple sequential logic — the overhead of creating a Fiber (~8KB) isn't justified
- Everything is already queued — if logic is in a
Job, Fibers inside that job are usually redundant
Fibers are useful when you have multiple I/O operations that can run in parallel: requests to multiple APIs, parallel database queries, sending notifications.
New Array Functions
PHP 8.4 added functions that have been missing for years.
arrayfind, arrayfind_key
$users = [
['id' => 1, 'name' => 'Alice', 'role' => 'admin'],
['id' => 2, 'name' => 'Bob', 'role' => 'editor'],
['id' => 3, 'name' => 'Charlie', 'role' => 'admin'],
];
// Before: array_filter + reset (or current)
$admin = current(array_filter($users, fn($u) => $u['role'] === 'admin'));
// PHP 8.4
$admin = array_find($users, fn($u) => $u['role'] === 'admin');
// ['id' => 1, 'name' => 'Alice', 'role' => 'admin']
$adminKey = array_find_key($users, fn($u) => $u['role'] === 'admin');
// 0
arrayany, arrayall
$prices = [100, 250, 50, 800, 120];
// Before: count(array_filter(...)) > 0
$hasExpensive = count(array_filter($prices, fn($p) => $p > 500)) > 0;
// PHP 8.4
$hasExpensive = array_any($prices, fn($p) => $p > 500); // true
$allCheap = array_all($prices, fn($p) => $p < 1000); // true
$allExpensive = array_all($prices, fn($p) => $p > 500); // false
Real Example: Cart Validation
class CartValidator
{
public function validate(Cart $cart): ValidationResult
{
$items = $cart->items;
// Any items without a price
$hasMissingPrice = array_any(
$items,
fn(CartItem $item) => $item->price === null
);
// All items in stock
$allInStock = array_all(
$items,
fn(CartItem $item) => $item->stock > 0
);
// Find first age-restricted item
$ageRestricted = array_find(
$items,
fn(CartItem $item) => $item->ageRestriction !== null
);
return new ValidationResult(
valid: !$hasMissingPrice && $allInStock,
ageVerificationRequired: $ageRestricted !== null,
firstRestricted: $ageRestricted,
);
}
}
New String Methods and Closure Improvements
mbtrim, mbltrim, mb_rtrim
Finally, native trim for multibyte strings:
$input = "\u{00A0}Hello, world!\u{00A0}"; // non-breaking spaces
echo trim($input); // "\u{00A0}Hello, world!\u{00A0}" — trim doesn't see NBSP
echo mb_trim($input); // "Hello, world!" — mb_trim handles it
In my projects this eliminated a dozen custom helpers like mb_trim($str, "\xC2\xA0").
Closure::fromCallable improvements
class EventDispatcher
{
private array $listeners = [];
public function on(string $event, callable $listener): void
{
// PHP 8.4: first-class callable syntax
$this->listeners[$event][] = Closure::fromCallable($listener);
}
public function emit(string $event, mixed ...$args): void
{
foreach ($this->listeners[$event] ?? [] as $listener) {
$listener(...$args);
}
}
}
$dispatcher = new EventDispatcher();
// All these variants work uniformly
$dispatcher->on('user.created', $logger->info(...));
$dispatcher->on('user.created', UserNotification::send(...));
$dispatcher->on('user.created', function (User $user) {
Cache::forget("users.{$user->id}");
});
Deprecations and Breaking Changes
What Breaks When Upgrading from 8.3
1. Implicit nullable typing removed
// PHP 8.3: worked (but deprecated)
function process(string $name = null): void {}
// PHP 8.4: error. Must be:
function process(?string $name = null): void {}
This is the most widespread breaking change. In a large Laravel project we had ~150 such places. Auto-fix:
# PHP CS Fixer rule
php-cs-fixer fix --rules=nullable_type_declaration_for_default_null_value src/
2. CURLOPT_BINARYTRANSFER removed
// Was
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
// Now — just remove it, this option did nothing since PHP 5.1.2
3. sessionregister, sessionunregister — fully removed
If legacy code with these functions still exists somewhere — time to migrate to $_SESSION[].
4. DateTime deprecation notices
// Deprecated: creating DateTime without arguments in certain contexts
$date = new DateTime();
$date->modify('+1 day'); // mutates the object
// Recommended:
$date = new DateTimeImmutable();
$tomorrow = $date->modify('+1 day'); // new object
Full Migration Checklist from 8.3
# 1. Update dependencies
composer update --dry-run
# Check package compatibility with 8.4
# 2. Run Rector for automatic migration
composer require rector/rector --dev
vendor/bin/rector process --set php84
# Rector automatically fixes:
# - nullable type declarations
# - removed constants
# - deprecated function replacements
# 3. PHPStan at maximum level
vendor/bin/phpstan analyse -l 9 src/
# 4. Tests
php artisan test --parallel
Rector Rules for PHP 8.4
// rector.php
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return RectorConfig::configure()
->withPaths([__DIR__ . '/app', __DIR__ . '/src'])
->withSets([LevelSetList::UP_TO_PHP_84])
->withRules([
// Add property hooks where __get/__set exist
\Rector\Php84\Rector\Class_\PropertyHookFromMagicGetSetRector::class,
]);
Performance: Benchmarks
All benchmarks on the same machine (AMD Ryzen 7, 32GB RAM, Ubuntu 24.04, PHP compiled with -O2).
Property Hooks vs get/set
Benchmark: 1M property reads
__get magic: 0.847s
Property hook (get): 0.312s
Direct public: 0.298s
Property hooks are ~2.7x faster than magic methods.
Difference from direct access — within noise (~5%).
arrayfind vs arrayfilter + reset
Benchmark: find first in array of 10000 elements (target at index 50)
array_filter + reset: 0.0034s (processes ALL elements)
array_find: 0.000018s (stops at first match)
~190x faster for early exit scenarios.
Fiber Creation Overhead
Benchmark: create + start + resume 100K fibers
Fiber create + start: 0.089s
Generator create: 0.043s
Function call: 0.011s
Overhead per Fiber — ~2x over generator, ~8x over plain call.
But: this is creation overhead. For I/O-bound tasks (HTTP, DB) it's irrelevant.
Laravel and Symfony — Specific Adaptation Examples
Laravel: Request Validation with Property Hooks
class CreateOrderRequest extends FormRequest
{
public array $validated {
get => parent::validated();
}
// Typed access to fields
public string $customerEmail {
get => $this->validated['customer_email'];
}
public array $items {
get => $this->validated['items'];
}
public float $totalAmount {
get => collect($this->items)->sum('price');
}
public function rules(): array
{
return [
'customer_email' => ['required', 'email'],
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'exists:products,id'],
'items.*.price' => ['required', 'numeric', 'min:0.01'],
];
}
}
// In controller:
class OrderController extends Controller
{
public function store(CreateOrderRequest $request): JsonResponse
{
// Instead of $request->validated()['customer_email']
$order = Order::create([
'email' => $request->customerEmail,
'items' => $request->items,
'total' => $request->totalAmount,
]);
return response()->json($order, 201);
}
}
Laravel: Resource with Asymmetric Visibility
class UserResource extends JsonResource
{
public private(set) array $computed;
public function __construct(User $resource)
{
parent::__construct($resource);
$this->computed = [
'full_name' => "{$resource->first_name} {$resource->last_name}",
'avatar_url' => $resource->avatar
? Storage::url($resource->avatar)
: null,
'is_active' => $resource->last_login_at?->isAfter(now()->subDays(30)),
];
}
public function toArray($request): array
{
return [
'id' => $this->id,
'email' => $this->email,
...$this->computed,
'role' => $this->whenLoaded('role', fn() => $this->role->name),
'created_at' => $this->created_at->toISOString(),
];
}
}
Symfony: DTO with Property Hooks for API Platform
#[ApiResource]
class ProductOutput
{
public string $name {
get => $this->name;
}
public private(set) string $slug {
set(string $value) => Str::slug($value);
}
public Money $price {
get => new Money($this->priceInCents, $this->currency);
}
public string $formattedPrice {
get => $this->price->format();
}
public bool $isAvailable {
get => $this->stock > 0 && $this->visible;
}
public function __construct(
string $name,
public private(set) int $priceInCents,
public private(set) string $currency,
public private(set) int $stock,
public private(set) bool $visible = true,
) {
$this->name = $name;
$this->slug = $name;
}
}
Other Changes in PHP 8.4
new Without Parentheses in Method Chains
// PHP 8.3: parentheses required
$name = (new ReflectionClass($obj))->getName();
$result = (new Collection([1, 2, 3]))->map(fn($x) => $x * 2)->toArray();
// PHP 8.4: no parentheses
$name = new ReflectionClass($obj)->getName();
$result = new Collection([1, 2, 3])->map(fn($x) => $x * 2)->toArray();
Small thing, but in fluent API chains it removes visual noise.
#[\Deprecated] Attribute
class PaymentGateway
{
#[\Deprecated("Use processPayment() instead", since: "2.0")]
public function pay(float $amount): bool
{
return $this->processPayment($amount);
}
public function processPayment(float $amount): bool
{
// new implementation
}
}
// Calling $gateway->pay(100) — E_USER_DEPRECATED with the message
Previously you needed trigger_error() inside the method or a @deprecated PHPDoc. The attribute is a native, machine-readable way.
New HTML5 Parser: \Dom\HTMLDocument
// PHP 8.3: DOMDocument with XML parser (breaks HTML5)
$doc = new DOMDocument();
@$doc->loadHTML($html); // @ to suppress warnings
// PHP 8.4: native HTML5 parser
$doc = \Dom\HTMLDocument::createFromString($html);
$doc = \Dom\HTMLDocument::createFromFile('page.html');
// Full HTML5 support: <template>, <slot>, <details>, inline SVG
// No warnings, no @ hacks
For HTML parsing in scrapers and tests — a massive step forward.
BCMath: Object API
// PHP 8.3
$result = bcadd('1.5', '2.3', 2); // '3.80'
$total = bcmul(bcadd('100', '50'), '1.2', 2); // '180.00'
// PHP 8.4: object API
use BCMath\Number;
$a = new Number('1.5');
$b = new Number('2.3');
$result = $a + $b; // Number('3.8')
$price = new Number('100');
$tax = new Number('1.2');
$total = ($price + new Number('50')) * $tax; // Number('180.0')
For financial calculations where float arithmetic is unacceptable — finally a proper API.
php.ini Configuration: What to Check
; New directives in 8.4
; Property hooks use slightly more memory per class (negligible)
; If you have strict memory_limit — check after migration
memory_limit = 256M
; Fiber stack size (default 8KB, may need increase)
; Relevant if fibers call deep recursion
; fiber.stack_size = 8192
; opcache — property hooks create new opcodes
; Recommended to increase buffer if many classes with hooks
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
Final Migration Checklist for PHP 8.4
- [ ] Run
rector process --set php84— automatic syntax migration - [ ] Fix all
function foo(Type $param = null)→function foo(?Type $param = null) - [ ] Check
composer update— all dependencies compatible with 8.4 - [ ] Run PHPStan level 8+ — catch deprecated constructs
- [ ] Run tests — especially where
__get/__setare used - [ ] Replace
DOMDocument::loadHTML()with\Dom\HTMLDocumentfor HTML5 parsing - [ ] Consider property hooks for Value Objects and DTOs — instead of boilerplate getters
- [ ] Consider
private(set)for Eloquent/Doctrine models — instead ofreadonly - [ ] Replace
array_filter + resetwitharray_findwhere early exit applies - [ ] Check
php.ini— opcache buffers, memory_limit - [ ] Update CI — PHP 8.4 image in GitHub Actions / GitLab CI
- [ ] Update Docker image —
php:8.4-fpmorphp:8.4-cli - [ ] Post-deploy monitoring — error rate, memory usage, response time
Comments (0)