Luminova Framework

PHP Luminova: Lazy Class Object Initalization

Last updated: 2025-01-02 11:10:00

The lazy object provides a proxy for lazily instantiating an object. The actual object is not initialized until one of its methods or properties is accessed.

The Luminova LazyObject class provides a straight-forward mechanism for implementing lazy initialization in your applications. It allows you to defer the instantiation of objects until they are accessed for the first time. This approach helps optimize resource usage and improve application performance by avoiding unnecessary object creation, especially for objects that may not always be used during execution.

The LazyObject class supports both class-based and closure-based initialization, making it highly flexible for a variety of use cases. It seamlessly integrates with native lazy-loading system and advanced PHP's ReflectionClass::newLazyGhost method (introduced in PHP 8.4), ensuring efficient and modern lazy-loading capabilities.


Key Features:

  • Lazy Initialization: Objects are instantiated only when accessed, reducing memory and processing overhead.
  • Class and Closure Support: Accepts a class name or a closure for creating objects dynamically.
  • PHP Compatibility: Leverages modern PHP features, while maintaining fallback support for older versions.
  • Type Safety: Ensures compatibility with object type casting through the use of marker interface LazyInterface.
  • Customizable Behavior: Allows passing arguments to constructors or custom initialization logic using closures.
  • PHP Magic Methods: Supports for PHP magic methods such as __get, __set, __call, __clone, __isset, __unset, __toString, __serialize, __unserialize, __debugInfo and __destruct.

Use Cases:

  • On-Demand Resource Loading: Ideal for scenarios where heavy objects, such as database connections or large datasets, are only needed conditionally.
  • Improving Performance: Helps streamline application startup by deferring unnecessary computations or object creation.
  • Decoupled Architecture: Enables dynamic and flexible design patterns where dependencies are resolved and loaded only when required.

The LazyObject class is designed to align with Luminova's philosophy of providing powerful, efficient, and developer-friendly utilities, ensuring that your applications remain scalable and performant.


Usage Examples

Creating and Using a Lazy Object

Below is an example of a Person class:

class Person 
{
   public function __construct(
      public string $name, 
      public int $age,
      public bool $underAge = false
   ) {}

   public function getDetails(): string
   {
      return "{$this->name}, Age: {$this->age}";
   }
}

Creating a Lazy Object (Dynamic Handling for PHP Versions)

The LazyObject will use lazy ghosting if the PHP version is 8.4.0 or later; otherwise, it defaults to the standard lazy-loading mechanism.

use Luminova\Utils\LazyObject;

$person = new LazyObject(Person::class, fn(): array => ['Peter', 33]);

echo $person->getDetails(); // Outputs: Peter, Age: 33

Early Object Instantiation for PHP 8.4.0+

This method creates a lazy object that performs an early return of the object instance.

use Luminova\Utils\LazyObject;

$person = LazyObject::newObject(Person::class, fn(): array => ['Peter', 33]);

echo $person->getDetails(); // Outputs: Peter, Age: 33

Using a Closure for Custom Initialization

Creating a new instance with arguments from a callable initializer argument:

use Luminova\Utils\LazyObject;
use Person;

$person = LazyObject::newObject(
   function(mixed ...$arguments): Person {
      // Custom logic to manipulate arguments
      $arguments[2] = ($arguments[1] < 18);

      return new Person(...$arguments);
   }, 
   function(): array {
      return ['Peter', 33]; // Arguments: Name, Age
   }
);

echo $person->age; // Outputs: 33
echo $person->name; // Outputs: Peter
echo $person->underAge ? 'yes' : 'no'; // Outputs: no

You can use a closure to define custom logic for initializing the object lazily.

use Luminova\Utils\LazyObject;
use Person;

$person = LazyObject::newObject(function (): Person 
{
   // Custom logic to fetch initialization data
   [$name, $age] = loadPersonInformation();

   return new Person($name, $age);
});

echo $person->getDetails(); // Outputs: Peter, Age: 33

Handle Initialization Arguments

Optionally manipulate initialization arguments in the closure.

use Luminova\Utils\LazyObject;
use Person;

$person = LazyObject::newObject(function (int $age): Person 
{
   $isUnderAge = ($age < 18);
   return new Person($age, $isUnderAge);
}, fn(): array => [random_int(15, 65)]);

echo $person->isUnderAge() ? 'Yes' : 'No';

Creating a Lazy Ghost Object

For PHP 8.4.0 and above, you can create a lazy ghost object that defers instantiation until the object is accessed.

use Luminova\Utils\LazyObject;

$person = LazyObject::newLazyGhost(Person::class, fn() => ['Peter', 33]);

echo $person->getDetails(); // Outputs: Peter, Age: 33

Class Definition


Methods

constructor

Initializes a new instance of LazyObject.

The constructor accepts either a class string (e.g., Example::class) or a closure responsible for creating and initializing the object. Lazy initialization postpones object creation until it is first accessed, optimizing resource usage.

For PHP versions 8.4.0 and later, this method utilizes ReflectionClass::newLazyGhost, which leverages PHP's magic methods to access properties or methods dynamically without early object instantiation. For earlier versions, a fallback approach ensures compatibility.

public __construct(\Closure|class-string<\T> $initializer, ?callable $arguments = null): mixed

Parameters:

ParameterTypeDescription
$initializer\Closure|class-string<\T>A class string or closure that creates the lazily initialized object.
$argumentscallable|nullOptional arguments to pass to the class constructor or closure initializer argument.
Must be a callable that returns a list array of arguments to pass to the constructor.

Throws:

Example:

The object Person is instantiated only when its properties or methods are accessed:

$person = new LazyObject(fn() => new Person(33, 'Peter'));

echo $person->getName(); // Outputs: Peter

newObject

Creates a lazily initialized instance of a class using a static method.

This method enables early object creation when required and utilizes ReflectionClass::newLazyGhost for PHP 8.4.0 or later. Unlike the constructor, this method ensures no further delegation of property or method access by the LazyObject class. For earlier PHP versions, it falls back to a custom lazy initialization mechanism.

public static newObject(\Closure|class-string<\T> $initializer, ?callable $arguments = null): object

Parameters:

ParameterTypeDescription
$initializer\Closure|class-string<\T>A class string or closure that creates the lazily initialized object.
$argumentscallable|nullOptional arguments to pass to the class constructor or closure initializer argument.
Must be a callable that returns a list array of arguments to pass to the constructor.

Throws:

Example:

The object Person is lazily loaded and its properties can be accessed immediately:

$person = LazyObject::newObject(Person::class);

$person->age = 33;

echo $person->age; // Outputs: 33

newLazyGhost

Creates a new lazy ghost object for the specified class.

This method utilizes PHP 8.4+'s ReflectionClass::newLazyGhost to create a lazily loaded instance of the specified class. The actual object initialization is deferred until its properties or methods are accessed, optimizing resource usage.

public static newLazyGhost(class-string<\T> $class, ?callable $arguments = null): object

Parameters:

ParameterTypeDescription
$classclass-string<\T>Fully qualified class name for which to create a lazy ghost.
$argumentscallable|nullOptional arguments to pass to the class constructor.
Must be a callable that returns a list array of arguments to pass to the constructor.

Return Value:

object - Returns a lazy ghost object of the specified class.

Throws:


newLazyInstance

Creates a new instance with arguments return the class object.

This method takes an arguments, and creates a new instance of lazy loaded class.

public newLazyInstance(mixed ...$arguments): object

Parameters:

ParameterTypeDescription
$argumentsmixedThe arguments to create a new class object with.

Return Value:

object - Return a new instance of the lazy loaded class or null.

Throws:

Examples:

Recrating a new instance with arguments:

$parent = LazyObject::newObject(Person::class, fn() => ['Peter', 33]);
echo $parent->getDetails(); // Outputs: Peter, Age: 33

$child = $parent->newLazyInstance('John Deo', 34);
echo $child->getDetails(); // Outputs: John Deo, Age: 34

setLazyInstance

Override the current lazy-loaded instance with a new class object.

public setLazyInstance(object $instance): void

Parameters:

ParameterTypeDescription
$instanceobjectThe new class object to replace the existing lazy-loaded instance.

getLazyInstance

Creates and return the lazy-loaded class object.

public getLazyInstance(): ?object

Return Value:

object|null - Return lazy-loaded instance of the specified class or null.

Throws:


isInstanceof

Checks if the lazy-loaded instance is of a specific class type.

This method verifies whether the lazily instantiated object is an instance of the specified class or implements the specified interface.

public isInstanceof(string $class): bool 

Parameters:

ParameterTypeDescription
$classclass-string<\T>The fully qualified class or interface name to check against.

Return Value:

object - Returns true if the lazy-loaded instance is of the specified class type, false otherwise.

Examples:

Check if the lazy-loaded instance Person:

if($object instanceof LazyObject && $object->isInstanceof(Person::class)){
    echo $object->getName();
}

Implementing the Lazy Interface

The LazyInterface in Luminova is a marker interface with no abstract methods. It allows your classes to support lazy initialization while maintaining compatibility with object type casting.

Problem Scenario

Imagine you have an existing class defined like this:

// /app/Utils/Example.php
<?php
namespace App\Utils;

class Example {}

And your controller initializes this class as shown:

// /app/Controllers/Http/ExampleController.php
<?php 
namespace App\Controllers\Http;

use Luminova\Base\BaseController;
use App\Utils\Example;

class ExampleController extends BaseController 
{
   protected ?Example $example = null;

   protected function onCreate(): void 
   {
      $this->example = new Example();
   } 
}

If you decide to use lazy loading for the Example class by assigning a lazy instance like this:

$this->example = LazyObject::newObject(Example::class);

You'll encounter an error. The property $example expects an instance of the Example class, but LazyObject::newObject returns an object or a class object that implements LazyInterface. This type mismatch causes the issue.


Solution

To resolve this, you need to modify your Example class and your controller. Follow these steps:

1. Object Type

If you need to accommodate lazy-loading while maintaining compatibility with your class property type, you might consider redefining the property to use the object type:

protected Example|object|null $example = null;

However, the above approach will result in an error because PHP does not support union types that combine a specific class type (like Example) with the generic object type. Your only alternative is to use object alone, as shown below:

protected ?object $example = null;

This approach works as expected but has a notable drawback: it is not IDE-friendly. With this definition, IDEs will not provide auto-completion or suggestions for methods and properties defined within the Example class. This can hinder development efficiency and code readability.

For a more better solution, consider using interfaces or Closure to support lazy-loading while maintaining type hints.


2. Implement the LazyInterface

Update the Example class to implement the LazyInterface:

<?php
namespace App\Utils;

use Luminova\Interface\LazyInterface;

class Example implements LazyInterface {}

3. Update the Property Type

In the controller, update the property definition to include a union type:

protected ?LazyInterface $example = null;

This allows $example to accept both Example and objects implementing LazyInterface.


Updated Controller Example

Here's the complete updated controller code:

<?php 
namespace App\Controllers\Http;

use Luminova\Base\BaseController;
use Luminova\Interface\LazyInterface;
use Luminova\Utils\LazyObject;
use App\Utils\Example;

class FooController extends BaseController 
{
   /**
    * @var Example<LazyInterface> $example
    */
   protected ?LazyInterface $example = null;

   protected function onCreate(): void 
   {
      $this->example = LazyObject::newObject(Example::class);
   }
}

Why Use the Lazy Interface?

Type Compatibility, to ensure that lazy-loaded instances work seamlessly with type hints. By implementing LazyInterface and using the correct type casting, you can harness the benefits of lazy loading without compromising type safety.