PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PHP 8.4 preview – property hooks, asymmetric visibility, chaining new

by Henryk Tews / Tuesday, 09 January 2024 / Published in PHP

PHP 8.4 is due in November 2024 and its headline features address two long-standing awkwardnesses. Property hooks allow computed and validated properties without getter/setter boilerplate. Asymmetric visibility lets you expose a property publicly for reading but restrict writing to the class. Chaining on new without parentheses removes a syntactic annoyance. I preview all three with practical examples.

Property Hooks – goodbye getter/setter boilerplate

<?php

declare(strict_types=1);

// Before PHP 8.4 - classic getter/setter DTO
class ProductDto
{
    private string $_name = '';
    private float $_price = 0.0;

    public function getName(): string { return $this->_name; }
    public function setName(string $name): void
    {
        if (empty($name)) throw new \InvalidArgumentException('Name cannot be empty');
        $this->_name = trim($name);
    }

    public function getPrice(): float { return $this->_price; }
    public function setPrice(float $price): void
    {
        if ($price < 0) throw new \InvalidArgumentException('Price cannot be negative');
        $this->_price = $price;
    }
}

// PHP 8.4 - property hooks eliminate the boilerplate
class ProductDto
{
    public string $name {
        get => $this->name;
        set {
            if (empty($value)) throw new \InvalidArgumentException('Name cannot be empty');
            $this->name = trim($value);
        }
    }

    public float $price {
        get => $this->price;
        set {
            if ($value < 0) throw new \InvalidArgumentException('Price cannot be negative');
            $this->price = $value;
        }
    }
}

$product = new ProductDto();
$product->name  = '  Widget  '; // triggers set hook - trims whitespace
echo $product->name;            // 'Widget' - get hook returns stored value
$product->price = -5.0;         // throws InvalidArgumentException

Computed properties with get hook

<?php

declare(strict_types=1);

class Order
{
    public function __construct(
        public readonly array $items,    // array of ['price' => float, 'qty' => int]
        public readonly float $taxRate,
    ) {}

    // Computed property - no stored value, calculated on access
    public float $subtotal {
        get => array_sum(array_map(fn($i) => $i['price'] * $i['qty'], $this->items));
    }

    public float $taxAmount {
        get => round($this->subtotal * $this->taxRate, 2);
    }

    public float $grandTotal {
        get => $this->subtotal + $this->taxAmount;
    }

    public int $itemCount {
        get => array_sum(array_column($this->items, 'qty'));
    }
}

$order = new Order(
    items: [
        ['price' => 9.99,  'qty' => 2],
        ['price' => 49.99, 'qty' => 1],
    ],
    taxRate: 0.23
);

echo $order->subtotal;   // 69.97
echo $order->taxAmount;  // 16.09
echo $order->grandTotal; // 86.06
echo $order->itemCount;  // 3

Asymmetric Visibility

<?php

declare(strict_types=1);

// PHP 8.4 - public read, private write
class OrderCollection
{
    // public get, private set - readable by everyone, writable only by this class
    public private(set) int $count = 0;
    public private(set) float $totalValue = 0.0;

    private array $orders = [];

    public function add(\Magento\Sales\Api\Data\OrderInterface $order): void
    {
        $this->orders[]     = $order;
        $this->count++;                               // allowed - private(set) in this class
        $this->totalValue  += $order->getGrandTotal(); // allowed
    }

    public function remove(int $index): void
    {
        if (isset($this->orders[$index])) {
            $this->totalValue -= $this->orders[$index]->getGrandTotal();
            unset($this->orders[$index]);
            $this->count--;
        }
    }
}

$collection = new OrderCollection();
$collection->add($order1);
$collection->add($order2);

echo $collection->count;      // 2 - public read
echo $collection->totalValue; // sum - public read
// $collection->count = 99;   // Fatal error: Cannot set private(set) property from outside

Chaining new without parentheses

<?php

// Before PHP 8.4 - parentheses required to chain on new
$slug = (new UrlBuilder())->setBase('example.com')->setPath('/product')->build();

$data = (new Collection())->addItem($a)->addItem($b)->toArray();

// PHP 8.4 - no parentheses needed
$slug = new UrlBuilder()->setBase('example.com')->setPath('/product')->build();

$data = new Collection()->addItem($a)->addItem($b)->toArray();

// Especially useful with builder pattern
$searchCriteria = new SearchCriteriaBuilder()
    ->addFilter('status', 'active')
    ->setPageSize(20)
    ->create();

Other additions in PHP 8.4

<?php

// array_find() graduated from 8.3 (where it was actually introduced)
// Confirmed stable in 8.4

// New HTML parsing functions - finally safe HTML manipulation
$dom = \Dom\HTMLDocument::createFromString('<div class="product"><span>Test</span></div>');
// Uses proper HTML5 parsing (libgumbo) instead of older libxml quirks

// Deprecated in PHP 8.4 (errors in PHP 9.0):
// Implicit nullable parameters (if not already fixed from 8.1)
// function foo(string $x = null) - still needs to be ?string $x = null

Summary

PHP 8.4’s property hooks are the most significant ergonomics improvement since constructor promotion in PHP 8.0. The get/set syntax eliminates getter/setter boilerplate while keeping full validation logic. Asymmetric visibility is the cleaner solution to the “expose read, protect write” pattern that previously required a private property with a public getter. Both features are backward-compatible and can be adopted incrementally in new code while older code continues to work.

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}