Advanced Livewire: A better way of working with models

By @samuel · 5 days ago

Advanced Livewire: A better way of working with models

Let's talk about Eloquent models and Livewire.

The official way of using models in Livewire is typehinting properties as the model class:

class UpdateRole extends Component
{
    public User $user;
}

This will store the data like this inside the Livewire serverMemo:

{
  "data": {
    "user": {
      "name": "foobar"
    }
  },
  "dataMeta": {
    "models": {
      "user": {
        "class": "App\\Models\\User",
        "connection": "sqlite",
        "id": 1,
        "relations": []
      }
    }
  }
}

This does work and it's intuitive to use when writing the PHP part of the component, but I personally don't like this approach for a couple of reasons.

Why it sucks (for me)

First, it exposes the internals of your app. The frontend shouldn't know the full class name of your model. It shouldn't expose the primary key (id). Some apps specifically use hashids, or dedicated uuid columns for routing, precisely because they don't want to expose the actual model keys to the users.

Second, the performance is sub-optimal. The component makes a query on every request, for every model property, including all of their loaded relations. (The model binding logic changed a bit in some recent releases so this may be inaccurate, but I'm 99% sure this still fully applies).

Note that the only reason why Livewire uses the separate the dataMeta object (shown in the code block above) is that everything in data can be changed on the frontend, which makes it bad for storing things like model ids. This was covered in the JavaScript Component Layer article of this series.

Most people don't know this because Livewire Just Works ™️.

Livewire feels like magic — because, honestly, the productivity gains really are magical — but it's also good to know how the internals work. Which is my main motivation for writing this series.

To prevent security issues, improve performance, and generally be more confident in your applications, it's good to understand how things work under the hood.

The better alternative

Let's build a better alternative to Livewire's default implementation. First, let's write down our goals:

  • No code internals exposed
  • No unnecessary database queries
  • Simple syntax
  • IDE support
  • Frontend safety

And now we'll implement those one by one.

Code internals

The obvious solution to not expose any internals is to store just some key (e.g. the route key if we don't want to expose the actual database primary key) and only resolve the model at runtime.

class UpdateRole extends Component
{
    public string $uuid;

    public string $role;

    protected function updatedRole()
    {
        User::firstWhere('uuid', $this->uuid)->update([
            'role' => $this->role,
        ]);
    }
}

Now we have a component with two properties — one to identify the User model using the uuid and one for the user's role. When the user changes the role on the frontend (imagine a <select>) the change is automatically saved to the database.

Database queries

There are no queries made until the updatedRole method is executed. This is nice, but if we had a more complex component, e.g. UpdateUser with the role, name, email, and password, we wouldn't want to make a query in each updated listener separately.

So let's make a nice abstraction for accessing the model:

protected function user(): User
{
    // Memoized call, we store the model in a dynamic property
    return $this->__user ??= User::where('uuid', $this->uuid)->firstOrFail();
}

Now we can call $this->user() to access the model, and it'll only make one query during the entire request. If we don't need the model in a specific request, there will be zero queries.

The simple syntax and IDE support points are also solved by the code above. The User return typehint provides identical IDE support to accessing a User-typed property.

The memoization using a dynamic property is a bit ugly, but it's still a one-liner and we can just write the method once and then only use it from other methods.

Frontend safety

If you've read the previous articles in the Advanced Livewire series (or if you have a deep understanding of Livewire in general), you probably see the main flaw with the code above.

The uuid is changeable on the frontend, which lets the user change other users' data.

What's the solution here? We basically need to make the property read-only.

Does Livewire have any feature for read-only properties? It doesn't, that's why the extra model data is in that dataMeta section of the serverMemo.

Does the ArchTech ecosystem have any feature for read-only properties in Livewire? 😎 Of course it does.

To make the property read-only, we only need to install our livewire-access package and add two lines of code to the component.

To install the package, run this:

composer require leanadmin/livewire-access

And to make the property read-only, add the #[BlockFrontendAccess] attribute to it and use the WithImplicitAccess trait on the component.

That's it. I won't go into detail about how the package works, since it's not just for making specific properties read-only, but for more general control of frontend access to your Livewire properties. (Also note that the package requires PHP 8).

So here's the final version of the component:

class UpdateRole extends Component
{
    use WithImplicitAccess;

    #[BlockFrontendAccess]
    public string $uuid;

    public string $role;

    protected function updatedRole()
    {
        $this->user()->update([
            'role' => $this->role,
        ]);
    }

    protected function user(): User
    {
        return $this->__user = isset($this->__user) ? $this->__user : User::where('uuid', $this->uuid)->firstOrFail();
    }
}

It covers all of our needs. No internals exposed, good performance, good IDE support (user() returning a User instance), and pretty clean code.

At first it looks a bit non-standard, but when you understand how things work and get used to writing components this way, it'll make a lot of sense.

Using Livewire like this feels a lot more mindful of the internals and therefore also all of the security and performance implications.

And that's it for this article. I hope this it gave you a good view into how Livewire works with models, and the alternative approach you can use if you want that extra bit of control.

The next article is coming out next week, also on Monday. This will probably be a regular weekly series now.

Thanks for reading, and hope you found this helpful!

  • By @snapey · 12 hours ago

    Perhaps I misunderstood Caleb's post? Talking about serverMemo.data:

    This is THE most important security feature in Livewire. Each component payload is signed with a secured checksum hash generated from the entire payload. This way if anything tampers with the data used to send back to the server, the backend will be able to tell that and will throw an exception.

    https://calebporzio.com/how-livewire-works-a-deep-dive

    • By @samuel · 9 hours ago

      See this: https://twitter.com/archtechx/status/1448758312611233794

      Livewire doesn't let you directly change the serverMemo data, since it's verified via that checksum. But you can change anything in data via the JS runtime which pushes the changes using $set.

      If you couldn't modify data on the frontend, then the Alpine integration wouldn't work — @entangle couldn't work, the $wire proxy wouldn't work — and you couldn't use $set() in wire:click handlers.

      This is a common misunderstanding which is why I covered it in the first part of this series. Everything in the component's data can be modified on the frontend — even if it's not used in any wire:click handlers or other things like that.

  • By @samuel · 2 days ago

    I've just published a new post — memoized() method calls — which takes inspiration from some of the approaches described here and improves on them. If you enjoyed this post, make sure to read the other one too!

  • By @ralphjsmit · 2 days ago

    Hey Samuel, thank you for the article! I read it twice already to be sure that I correctly understand it ;-)

    I have one question though, I saw that you're using your package with #[BlockFrontendAccess] in order to block the frontend from using the public $uuid attribute.

    Using a package if of course totally fine, but why didn't you just make the attribute private?

    Perhaps I'm missing something here, but would be great if you could help me with this. Thanks!

    • By @samuel · 2 days ago

      Using a package if of course totally fine, but why didn't you just make the attribute private?

      Because then it wouldn't be persistent. If we set the value in mount(), but keep the property private, it won't be included in the payload sent to the frontend, and when Livewire rehydrates the component on the next request, the property won't have any value.

      That's the point of the package — we need to include the data in the payload, but we also need to avoid frontend modifications.

      • By @ralphjsmit · 2 days ago

        That's a great point, something I didn't even know🤭 Thanks!

  • By @edalzell · 2 days ago

    Why the __ in $this->__user? Is that a Livewire thing?

    • By @samuel · 2 days ago

      No, it's just to distinguish the value from properties, in case both a property and a method with that name existed.

      You can create runtime properties in Livewire, and they won't be included in the frontend data.

      That said, the post has a slight issue. The ??= doesn't work since Livewire overrides __get(), so isset($this->__user) ? $this->user : ... should be used.

      I'll be editing the post later today to fix this + mention some additional improvements.

  • By @usernotnull · 4 days ago

    Manual memoization as opposed to using Livewire's computed properties magic to add type-hinting. If only it was possible to type-hint magic methods...

    • By @samuel · 4 days ago

      Right, I'll edit the article to mention that computed properties can be used as well. Seems like the approach I suggested has some issues anyway (I used a simplified example of the code I generally use).

      I generally avoid computed properties for things like these because I don't like adding @property-read annotations when I can implement things using a method instead.