Day 34: PHP 8.4 Lazy Objects: Deferring expensive service initialization

Designing and Building a High-Scale PHP Content Management System ~9 min read 60 min hands-on

Good morning, pioneers! Welcome back to our journey through building a high-scale PHP Content Management System. Yesterday, we demystified Autowiring, seeing how reflection empowers our dependency injection container to seamlessly resolve service dependencies. That was a game-changer for developer ergonomics, right? But here’s the thing about high-scale systems: ergonomics alone isn't enough.

Today, we're tackling a subtle but critical performance bottleneck that even the most elegantly autowired systems can face: the "bootload tax" of initializing expensive services before they're actually needed. Imagine a Ferrari that, every time you start it, first insists on checking tire pressure, oil levels, and then running a full diagnostic on its infotainment system, even if you just want to drive to the corner shop. That's what happens when you eagerly instantiate every dependency, every time.

This is where PHP 8.4's new Lazy Objects feature steps in, offering a built-in, elegant solution to defer the initialization cost of services until the very moment they are actually used.

The Unseen Performance Drain: Eager Initialization

In the world of persistent PHP runtimes like RoadRunner or Swoole, our application's core infrastructure (the DI container, fundamental services) is initialized once and lives across many requests. This eliminates the traditional "shared-nothing" PHP boot-up cost per request. However, within that long-lived process, individual requests still build up their unique object graphs.

Consider a typical CMS:

  • A PaymentGatewayClient for processing transactions.

  • A ComplexReportGenerator for analytics.

  • An AdminAuditLogger that only logs specific, rare admin actions.

  • A ThirdPartyAPIManager that establishes connections to external services.

If your DI container eagerly instantiates all these services every time any page loads, even a simple blog post view that doesn't touch payments, reports, or admin logs, you're paying a hidden cost:

  1. Memory Footprint: Each instantiated object consumes memory. Multiply this by hundreds or thousands of concurrent requests, and memory usage skyrockets.

  2. CPU Cycles: Object construction, especially for services with complex constructors (e.g., fetching configuration from a database, establishing network connections), consumes valuable CPU time.

  3. Latency: These initialization costs add up, contributing to the overall request latency, even for paths that don't need the service.

For a system aiming for 100 million requests per second, even micro-optimizations in memory and CPU per request become macro-scale savings. This isn't theoretical; it's the difference between a system that scales linearly and one that chokes under load, or one that requires an unnecessarily larger and more expensive infrastructure.

PHP 8.4 Lazy Objects: Deferring the Heavy Lifting

Prior to PHP 8.4, achieving lazy loading often involved manual factories, anonymous functions, or building custom proxy objects. These approaches work, but they add boilerplate, complicate type-hinting, and can obscure the true dependency graph.

PHP 8.4 introduces a first-class solution: the #[Lazy] attribute. When you apply this attribute to a service's dependency in your constructor, PHP automatically generates a proxy object for that dependency.

How it works:

  1. When OrderController is instantiated, the DI container sees the #[Lazy] attribute on PaymentGatewayClient.

  2. Instead of directly creating a PaymentGatewayClient, it injects a proxy object that implements the PaymentGatewayClient interface (or extends the class).

  3. The OrderController holds this proxy.

  4. If displayOrderForm() is called, the proxy is never touched beyond being assigned to a property. The real PaymentGatewayClient constructor is never executed.

  5. If submitOrder() is called, and it invokes a method like processPayment() on the proxy, only then does the proxy intercept the call, instantiate the real PaymentGatewayClient, and delegate the method call to it. Subsequent calls to the proxy will directly delegate to the now-initialized real object.

Benefits for a High-Scale CMS:

Keep going with the full lesson

The rest of this lesson β€” plus its implementation guide, runnable scripts and source code β€” is included with the course or an All Access plan. Create a free account to start the free lessons in every course.

Already a member? Sign in