Back to blog
phpphp84backenddevelopment

PHP 8.4 new features: a practical deep-dive

January 15, 202612 min read

PHP 8.4 was released in November 2024 and brings features that fundamentally change how we build enterprise applications. Property hooks eliminate boilerplate code, new array functions make collection operations more expressive, and lazy objects open the door to better performance. In this article, we dive deep into these features with production-ready code examples.

Property hooks: getters and setters without boilerplate

Property hooks are the most impactful addition in PHP 8.4. They replace traditional getter/setter patterns and magic methods with an elegant, declarative syntax:

class Product
{
    private array $priceHistory = [];
 
    public string $sku {
        set (string $value) {
            if (!preg_match('/^[A-Z]{2}-\d{6}$/', $value)) {
                throw new InvalidArgumentException('SKU must match format: XX-000000');
            }
            $this->sku = strtoupper($value);
        }
    }
 
    public float $price {
        set (float $value) {
            if ($value < 0) {
                throw new InvalidArgumentException('Price cannot be negative');
            }
            $this->priceHistory[] = ['price' => $value, 'date' => new DateTimeImmutable()];
            $this->price = $value;
        }
        get => round($this->price, 2);
    }
 
    public float $priceWithVat {
        get => $this->price * 1.21;
    }
 
    public private(set) string $createdBy;
 
    public function __construct(string $sku, float $price, string $createdBy)
    {
        $this->sku = $sku;
        $this->price = $price;
        $this->createdBy = $createdBy;
    }
}

The private(set) syntax is asymmetric visibility: the property is publicly readable but only privately writable. This eliminates the need for separate getter methods while maintaining encapsulation.

New array functions for more expressive code

PHP 8.4 introduces four new array functions that simplify common patterns. In Magento and Pimcore context, these are particularly useful:

use Pimcore\Model\DataObject\Product;
 
class ProductService
{
    public function findFirstInStock(array $products): ?Product
    {
        return array_find(
            $products,
            fn(Product $p) => $p->getStock() > 0
        );
    }
 
    public function findDiscountedProductKey(array $products): string|int|null
    {
        return array_find_key(
            $products,
            fn(Product $p) => $p->getDiscount() > 0
        );
    }
 
    public function hasOutOfStock(array $products): bool
    {
        return array_any(
            $products,
            fn(Product $p) => $p->getStock() === 0
        );
    }
 
    public function allPublished(array $products): bool
    {
        return array_all(
            $products,
            fn(Product $p) => $p->getPublished()
        );
    }
 
    public function getValidProducts(array $products): array
    {
        if (!array_all($products, fn($p) => $p instanceof Product)) {
            throw new InvalidArgumentException('All items must be Products');
        }
 
        return array_filter(
            $products,
            fn(Product $p) => $p->getPublished() && $p->getStock() > 0
        );
    }
}

These functions replace verbose constructs with array_filter + array_key_first or foreach loops. The intent of the code becomes immediately clear.

The #[Deprecated] attribute for API versioning

The native #[Deprecated] attribute makes deprecation warnings part of your code instead of just docblocks. This integrates with IDEs and static analysis tools:

namespace Ten50\Module\Api;
 
class ProductApi
{
    #[\Deprecated(
        message: 'Use getProductBySku() instead',
        since: '2.0'
    )]
    public function getProduct(int $id): ?array
    {
        trigger_deprecation(
            'ten50/module',
            '2.0',
            'Method %s is deprecated, use %s instead',
            __METHOD__,
            'getProductBySku()'
        );
 
        return $this->productRepository->find($id)?->toArray();
    }
 
    public function getProductBySku(string $sku): ?array
    {
        return $this->productRepository->findBySku($sku)?->toArray();
    }
 
    #[\Deprecated('Bulk operations moved to BatchApi::importProducts()')]
    public function bulkImport(array $products): ImportResult
    {
        return $this->batchApi->importProducts($products);
    }
}
 
class LegacyProductController
{
    #[\Deprecated(
        message: 'This entire controller is deprecated. Use ProductApiController instead.',
        since: '1.5'
    )]
    public function __construct(
        private ProductApi $api
    ) {}
}

PHPStan and Psalm recognize the attribute automatically. IDEs show strikethrough methods and warnings on usage. This makes phased API migrations manageable.

Lazy objects for better performance

Lazy objects only initialize when they are actually used. This is powerful for dependency injection and service containers:

namespace Ten50\Module\Service;
 
class ExpensiveReportService
{
    private ?ReportGenerator $generator = null;
    private ?DataAggregator $aggregator = null;
 
    public function __construct(
        private readonly ReportGeneratorFactory $generatorFactory,
        private readonly DataAggregatorFactory $aggregatorFactory
    ) {}
 
    private function getGenerator(): ReportGenerator
    {
        return $this->generator ??= $this->generatorFactory->create();
    }
 
    public function generateReport(array $criteria): Report
    {
        return $this->getGenerator()->generate($criteria);
    }
}
 
class LazyServiceContainer
{
    private array $services = [];
    private array $factories = [];
 
    public function register(string $id, callable $factory): void
    {
        $this->factories[$id] = $factory;
    }
 
    public function get(string $id): object
    {
        if (!isset($this->services[$id])) {
            if (!isset($this->factories[$id])) {
                throw new ServiceNotFoundException($id);
            }
 
            $reflector = new ReflectionClass($this->factories[$id]());
            $this->services[$id] = $reflector->newLazyGhost(
                function (object $instance) use ($id) {
                    $real = ($this->factories[$id])();
                    foreach ((new ReflectionObject($real))->getProperties() as $prop) {
                        $prop->setAccessible(true);
                        $prop->setValue($instance, $prop->getValue($real));
                    }
                }
            );
        }
 
        return $this->services[$id];
    }
}

With ReflectionClass::newLazyGhost() and newLazyProxy() you can create objects that only initialize on first property access or method call. In a Magento module with 50+ dependencies, this can significantly reduce bootstrap time.

Need PHP 8.4 migration?

We help you upgrade your application to PHP 8.4 and implement modern PHP patterns.

Get in touch