Marco Pivetta (Ocramius)

LazyProperty - Automatic property initialization for PHP

Yesterday, I worked on yet another interesting little experiment, which is called LazyProperty .

The problem with "lazy" properties

The idea is very simple: avoid manually checking object properties to see if they were initialized.

Let's make a simple example:

<?php

class UserService
{
    // ...
    protected $userRepository;

    public function register($user)
    {
        // ...
        $this->getUserRepository()->add($user);
    }

    // ...

    protected function getUserRepository()
    {
        // we're not using DI here because of performance implications
        // developers often use a service locator for RAD here
        return $this->userRepository
            ?: $this->userRepository = new MemoryUserRepository();
    }
}

This is a fairly simple approach, and it is perfectly fine to use it when we don't want to be forced to build the MemoryUserRepository instance for calls to API methods that don't use it.

There is a problem though, which is that any direct access to UserService#$userRepository has to be avoided: all of the class' implementation as well as its subclasses have to rely on the protected getter in order to access the repository instance.

For instance, the following code works only because of a lucky combination of events:

<?php

class UserService
{
    // ...
    protected $userValidator;

    public function register($user)
    {
        $this->getUserRepository()->add($user);
    }

    public function login($userId)
    {
        // mind this - this is a mistake!
        $this->userRepository->find($userId);
        // ...
    }
}
<?php

$user = build_user_somehow();

$userService->register($user);
$userService->login($user->getId());

This code will run, but only because UserService#register() was called before UserService#login().


Possible solutions

The code in the examples exposes a silly bug, and it should be fixed by avoiding any property access to UserService#$userRepository.

Moreover, implementors of subclasses will also experience the issue if they try accessing the un-initialized property. The fix for that is to just define UserService#$userRepository as private and being very defensive about its usage.

These are all valid solutions, but we still don't get around using a protected getter in our class' scope, which I personally consider ugly.

That's where I had this possibly crazy idea: making the property itself "lazy" and avoiding the getter call completely, making the getter an implementation detail.

Let's see how this is done with the library:

<?php

class UserService
{
    use \LazyProperty\LazyPropertiesTrait;

    protected $userRepository;

    public function __construct()
    {
        // ...
        // mind this - we are marking "userRepository" as lazy
        $this->initLazyProperties(['userRepository']);
    }

    public function register($user)
    {
        // ...
        // now use the property directly
        $this->userRepository->add($user);
        // ...
    }

    public function login($userId)
    {
        $this->userRepository->find($userId);
        // ...
    }

    protected function getUserRepository()
    {
        return $this->userRepository
            ?: $this->userRepository = new MemoryUserRepository();
    }
}

By calling LazyProperty\LazyPropertiesTrait#initLazyProperties(), we've made sure that any access to the un-initialized UserService#$userRepository will actually trigger a call to UserService#getUserRepository(), and therefore initialize it.

With that, we don't need to actually worry about calling the getter: both the getter call and property access will work the same way, which is pretty cool!


Under the hood

What is going on? I'm simply exploiting an feature of the PHP language on which I've already blogged at Property Accessors in PHP Userland .

As a reference, here is the annotated body of LazyProperty\LazyPropertiesTrait#initLazyProperties() :

<?php

private function initLazyProperties(array $lazyPropertyNames, $checkLazyGetters = true)
{
    foreach ($lazyPropertyNames as $lazyProperty) {

        // verify that a getter is available for the given lazy property
        if ($checkLazyGetters && ! method_exists($this, 'get' . $lazyProperty)) {
            throw MissingLazyPropertyGetterException::fromGetter($this, 'get' . $lazyProperty, $lazyProperty);
        }

        // record the properties that were defined as "lazy"
        $this->lazyPropertyAccessors[$lazyProperty] = false;

        // if the property is defined, then ignore it (we don't want to sensibly alter object state)
        if (! isset($this->$lazyProperty)) {
            // unset the property, this allows us to use magic getters
            unset($this->$lazyProperty);
        }
    }
}

Quite simple! Nothing really incredible going on here. When is the property actually initialized?

The other method defined by the trait is the Magic getter LazyProperty\LazyPropertiesTrait#__get() :

<?php

// returning a reference is required,
// otherwise (for example) array properties accessed for writes will fail
public function & __get($name)
{
    // disallow access to non-existing properties
    if (! isset($this->lazyPropertyAccessors[$name])) {
        throw InvalidLazyProperty::nonExistingLazyProperty($this, $name);
    }

    // retrieve the object that is trying to access the lazy property
    $caller = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT)[1];

    // disallow access from invalid scope
    // basically reproduces property visibility in userland
    if (! isset($caller['object']) || $caller['object'] !== $this) {
        AccessScopeChecker::checkCallerScope($caller, $this, $name);
    }

    // set the property to `null` (disables notices)
    $this->$name = null;

    // initialize the property
    $this->$name = $this->{'get' . $name}();

    return $this->$name;
}

Conclusions

This "hack" is simple, clean, easily tested and doesn't cause any problems. It works on PHP 5.4, 5.5, 5.6 and even on HHVM, which I wasn't expecting.

Can you use it? Yes, why not? Do you need it? Usually not. Being defensive about usage of your APIs can actually avoid sloppy mistakes like the ones that I've described in the examples.

Correctly annotating your properties with /** @var Type|null */ also helps tools like PHP Analyzer in discovering such bugs via static analysis.

Clean dependency injection and eventually proxying dependencies also produces better and easier testable results than explicit lazy-loading coded in your classes.

The most interesting result in this experiment is that PHP yet again allows to implement language-level features in userland, which is awesome!

Please let me know if you like this project by giving it a star, or just drop me a tweet if you think it can be enhanced!

Tags: php, oop, hacks