Skip to content

Lazy computed properties

Caleb Porzio edited this page Nov 1, 2022 · 4 revisions

Problem rating: Simple ---*------------------ Deeeeep


Here's an example of a computed property in a Livewire component:

class UpdatePosts extends Component
{
    function getPostsProperty()
    {
        return Post::all();
    }

    function save()
    {
        $this->posts->each->save();
    }
}

In this case, $this->posts is the computed property generated by internally calling getPostsProperty() and using the returned value.

Why not just use methods?

You might ask, what's the advantage of using computed properties instead of just calling a method like $this->posts() like so:

class UpdatePosts extends Component
{
    function posts()
    {
        return Post::all();
    }

    function save()
    {
        $this->posts()->each->save();
    }
}

Reason A: Cacheing

The main reason is, if you access $this->posts() twice in the same Livewire request, it will run two database queries. However, computed properties are cached for the duration of a request and if you retrieve $this->posts twice, the database will only be queried once.

Admittedly, this is in general a minor performance improvement.

Reason B: Aesthetics

Even without any performance implications, it feels nicer sometimes to access something like "posts" as a property rather than a method.

Ok, so now that we've decided computed properties are good. Here's the problem:

Problem: Using them lazilly in Blade views

My ideal syntax is the following:

Livewire Class Blade View
class Example extends Component
{
    function getPostsProperty()
    {
        return Post::all();
    }

    function save()
    {
        $this->posts->each->save();
    }
}
<div>
    @foreach ($posts as $post)
        <h1>{{ $post->title }}</h1>
    @endforeach
</div>

Unfortunately, to make the $posts variable available to Blade, I have to evaluate ->getPostsProperty() first. Even if I don't use it inside the view.

The solution in V2 of Livewire was to not make it available, but instead tell people to access it using $this->posts like so:

<div>
    @foreach ($this->posts as $post)
        <h1>{{ $post->title }}</h1>
    @endforeach
</div>

This works and is a decent solution, but it's just not as perfect as it could be.

Solution A: Forgo lazy evaluation

Does it really matter that much? Are there THAT many instances where you have defined a computed property (that's expensive to run) and AREN'T using it somewhere in each render?

The purest in me isn't really happy with this, but maybe the aesthetics of not needing to access properties through $this-> is worth it.

Solution B: A stand-in, lazy variable

I could create some frankensteiny PHP object and pass THAT into the view, then when it is used, it evaluates the computed property and replaces itself with the real thing.

The biggest problem with this is, it is probably an endless pit of bugs and is more trouble than it's worth.

Here's an example of what the stand-in object might look like:

class LazyVariable implements Htmlable, Stringable, Jsonable, ArrayAccess
{
    function __toString() { }

    function toJSON() { }

    function toHtml() { }

    function __get($name) { }

    function __call($name, $arguments) { }

    function __set($name, $value) { }

    function offsetGet(mixed $offset): mixed { }

    function offsetSet(mixed $offset, mixed $value): void { }

    function offsetExists(mixed $offset): bool { }

    function offsetUnset(mixed $offset): void { }
}

Of course, the above class would contain a bunch of logic to evaluate and return the computed property.

Solution C: ???

Short of precompiling and swapping the variable in the Blade view with something lazy (which is... um... not a good idea), I can't think of any other possible solution.