PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PHP 8.4 release – property hooks in production, asymmetric visibility, BcMath\Number, array_find

by Henryk Tews / Tuesday, 26 November 2024 / Published in PHP

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.

About Henryk Tews

What you can read next

PHP 7.2 – object type hint, sodium instead of mcrypt, deprecations

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}