PHP 8.4 was officially released on 21 November 2024. After the RC months I had code ready to merge the day it went stable. A month in production gives me a clear picture of what property hooks look like in real module code, where asymmetric visibility is actually useful versus where it is overkill, and how BcMath\Number compares to the traditional bcmath functions in practice.
Property hooks in production – real examples
<?php
declare(strict_types=1);
// What I actually use property hooks for after one month:
// 1. NORMALISATION on set (most common use case)
class CustomerData
{
public string $email {
get => $this->email;
set => $this->email = strtolower(trim($value));
}
public string $phone {
get => $this->phone;
set {
$clean = preg_replace('/[^0-9+]/', '', $value);
if (strlen($clean) < 9) {
throw new \InvalidArgumentException("Invalid phone: {$value}");
}
$this->phone = $clean;
}
}
public string $postcode {
get => $this->postcode;
set {
if (!preg_match('/^\d{2}-\d{3}$/', $value)) {
throw new \InvalidArgumentException("Invalid Polish postcode: {$value}");
}
$this->postcode = $value;
}
}
}
$customer = new CustomerData();
$customer->email = ' Jan.Kowalski@EXAMPLE.COM '; // stored as: jan.kowalski@example.com
$customer->phone = '+48 600-100 200'; // stored as: +48600100200
$customer->postcode = '30-001'; // valid
// $customer->postcode = '300001'; // throws InvalidArgumentException
<?php
// 2. COMPUTED VIRTUAL PROPERTIES (no backing storage)
// Used this pattern in several order summary classes
class OrderSummary
{
public function __construct(
private readonly array $items, // [['price' => float, 'qty' => int, 'tax_rate' => float]]
) {}
public float $subtotal {
get => array_sum(array_map(fn($i) => $i['price'] * $i['qty'], $this->items));
}
public float $taxAmount {
get => array_sum(array_map(
fn($i) => $i['price'] * $i['qty'] * $i['tax_rate'],
$this->items
));
}
public float $grandTotal {
get => $this->subtotal + $this->taxAmount;
}
public int $itemCount {
get => array_sum(array_column($this->items, 'qty'));
}
public bool $hasDigitalItems {
get => array_any($this->items, fn($i) => ($i['type'] ?? 'physical') === 'virtual');
}
}
Asymmetric visibility – where I actually use it
<?php
declare(strict_types=1);
// I use public private(set) in exactly one pattern:
// Collection/aggregate classes where external code needs to READ stats
// but only the class should MODIFY them
class ImportResult
{
public private(set) int $processed = 0;
public private(set) int $created = 0;
public private(set) int $updated = 0;
public private(set) int $skipped = 0;
public private(set) int $failed = 0;
public private(set) float $duration = 0.0;
private float $startTime;
public function start(): void { $this->startTime = microtime(true); }
public function recordCreated(): void { $this->processed++; $this->created++; }
public function recordUpdated(): void { $this->processed++; $this->updated++; }
public function recordSkipped(): void { $this->processed++; $this->skipped++; }
public function recordFailed(): void { $this->processed++; $this->failed++; }
public function finish(): void
{
$this->duration = round(microtime(true) - $this->startTime, 3);
}
public function toArray(): array
{
return [
'processed' => $this->processed,
'created' => $this->created,
'updated' => $this->updated,
'skipped' => $this->skipped,
'failed' => $this->failed,
'duration' => $this->duration,
];
}
}
// Usage
$result = new ImportResult();
$result->start();
foreach ($products as $product) {
try {
$this->importProduct($product, $result);
} catch (\Exception $e) {
$result->recordFailed();
}
}
$result->finish();
echo "Processed: {$result->processed}, Created: {$result->created}, Failed: {$result->failed}";
// $result->processed = 99; // Fatal error: Cannot set private(set) property from outside
BcMath\Number in production – financial calculations
<?php
declare(strict_types=1);
use BcMath\Number;
class InvoiceCalculator
{
public function calculateLineItem(string $unitPrice, string $quantity, string $taxRate): array
{
$price = new Number($unitPrice);
$qty = new Number($quantity);
$rate = new Number($taxRate);
$one = new Number('1');
$net = ($price * $qty)->round(2);
$tax = ($net * $rate)->round(2);
$gross = ($net + $tax)->round(2);
return [
'net' => (string) $net,
'tax' => (string) $tax,
'gross' => (string) $gross,
];
}
public function calculateInvoiceTotal(array $lineItems): array
{
$totalNet = new Number('0');
$totalTax = new Number('0');
foreach ($lineItems as $item) {
$totalNet = $totalNet + new Number($item['net']);
$totalTax = $totalTax + new Number($item['tax']);
}
return [
'net' => (string) $totalNet->round(2),
'tax' => (string) $totalTax->round(2),
'gross' => (string) ($totalNet + $totalTax)->round(2),
];
}
}
// Real production case where BcMath\Number eliminated a bug:
// Old code with float arithmetic:
$price = 49.99;
$qty = 3;
$taxRate = 0.23;
$net = round($price * $qty, 2); // 149.97
$tax = round($net * $taxRate, 2); // 34.49
$gross = round($net + $tax, 2); // 184.46
// Floating point: $price * $qty = 149.97000000000002 (before round)
// New code:
$gross2 = (new Number('49.99') * new Number('3'))->round(2);
// Exact: 149.97 - no floating point issue
array_find() – one month of real use
<?php
// Replaced these patterns throughout existing modules:
// Before
$found = null;
foreach ($items as $item) {
if ($item['sku'] === $targetSku) { $found = $item; break; }
}
// After
$found = array_find($items, fn($item) => $item['sku'] === $targetSku);
// Before
$key = null;
foreach ($items as $k => $item) {
if ($item['id'] === $targetId) { $key = $k; break; }
}
// After
$key = array_find_key($items, fn($item) => $item['id'] === $targetId);
// Used array_any() to replace isset() checks on complex conditions
$hasBackorder = array_any($items, fn($i) => $i['qty'] > $i['stock'] && $i['backorder_allowed']);
$allInStock = array_all($items, fn($i) => $i['stock'] > 0);
Summary
PHP 8.4 delivers on its promises. Property hooks with normalisation are the pattern I use most – they are cleaner than setter methods for simple transformations. Asymmetric visibility is most useful in result/aggregate objects. BcMath\Number is an immediate upgrade for any financial calculation code. The migration from PHP 8.3 was smooth – no breaking changes in the codebase I maintained.
