Livewire is a full-stack framework for Laravel that enables you to create dynamic and reactive UI components. Property data-binding is one key element of this reactivity, allowing you to synchronize data between your HTML markup and your component properties in the backend. Although Livewire supports most used PHP Primitive types properties and some widely used PHP Objects types in Laravel, it doesn't support custom property types out of the box. In this article we'll review the Livewire data binding workflow to get a deeper understanding of the Hydration system and how to use Sinthesizers to support custom data properties in our applications.
When talking about property types, we are referring to the type of the public properties that we are trying to make available on the frontend. For example:
This is just a trivial example to illustrate the issue at hand. In a real-world application, we might encounter more complex property types, such as Currency, Address, PayrollCalculator, ShoppingCart, etc.
In this case we are using an integer primitive for the `$total``. Let's say we want to replace the total property with an object that we'll use to perform more complex operations, for example:
If we try to do this, the application will fail with the following error
Exception: Property type not supported in Livewire for property: [{....}]
We are getting this exception because livewire doesn't know how to handle the Countable object.
Before jumping into the solution, let's take a deeper look at the data binding process in Livewire.
How data binding works
Livewire operates by creating a snapshot of the PHP component information, represented in the backend as PHP classes, into plain JSON or array objects for use in JavaScript via the Livewire JS library. While in the backend, we can think of those components as PHP Classes that will ultimately render a view, in the frontend, those components become a portion of the HTML.
The reactivity of these components on the frontend side comes into play when we alter the state of the component from the frontend. When that happens, the Livewire library makes a request to the backend through a route, re-renders the component (with the new snapshot), so it can compare and replace the component in the DOM.
Following the previous example:
In Laravel, you can render the component on any view using the @livewire Blade directive:
However, this is not entirely precise. Here's how the component is actually rendered into the view:
The component snapshot
Let's take a closer look at the wire:snapshot property:
This snapshot contains, among other things, all the public properties available in the PHP class under "data."
What happens when we introduce a custom object like the following?
And then change the Livewire component as follows:
Livewire will try to resolve the object and dehydrate the property when generating the snapshot, at this point the application will fail:
Exception: Property type not supported in Livewire for property: [{"total":0,"step":1}]
Solution #1: Implementing the Wireable Interface
Out of the box, Livewire supports the following Primitive Types and Common PHP Types:
- Array
- String
- Integer
- Boolean
- Null
- Collection (Illuminate\Support\Collection)
- Eloquent Collection (Illuminate\Database\Eloquent\Collection)
- Model (Illuminate\Database\Model)
- DateTime (DateTime)
- Carbon (Carbon\Carbon)
- Stringable (Illuminate\Support\Stringable)
The most straightforward option to support this new Countable::class
is to implement the `Wireable::class`` interface:
When dehydrating/hydrating the component, Livewire will execute each method to transform the object in an array that the frontend can understand or create of the original object in the backend the array version of it.
This is how the snapshot of the component looks like after these changes:
In this case, the data being sent from the backend to the frontend is more complex. The countable property is now a tuple of data and metadata, allowing Livewire to keep all the information needed to restore the dehydrated object to its original form in the backend.
In this case, although the Counter::class
could be easily used in different parts of your application (outside a Livewire component context), you are being forced to implement an interface that will only have use if the `Counter::class`` object is ever used within a Livewire component.
Another limitation of choosing the Wireable Interface approach is what happens when you want to use a vendor object as property type in your livewire components, in that case you may need to extend the vendor class to create a local object so then you can implement the Wireable interface, i.e:
Solution #2: Supporting custom properties with Livewire Synthesizers
Livewire Synthesizers allow you to add support for custom component properties, effectively extending the Livewire functionality and its ability to identify and handle all the different property types you use in your application.
In the example above, we were forced to change the Counter class by adding the Wireable interface. In this case, we can add support for the Countable::class attribute in a more transparent way with a custom Synthesizer:
The match()
method verifies if the synthesizer should run or not for the current attribute. This step is crucial, as all the attributes on a single request are evaluated each time.
The dehydrate()
method receives the PHP instance and should return a tuple of [data, metadata].
The hydrate()
method receives the data and metadata in the same format as returned from the dehydrate()
method but in two different variables and then use this information to create a new instance of the original target with the current values.
The set()
and get()
methods are used for data binding in Livewire. So, to support any custom property that will not only be used for data display but also could be modified from the frontend, it is crucial to implement these two methods; otherwise, data binding won't work.
Registering a synthesizer
You can register any custom Synthesizer using the Livewire facade in a service provider:
What about nested properties?
Following the Counter example above, imagine we want to keep a record of the last time the increment()
method was used. We could do something like this:
This small change will make our Livewire component fail because now we need to make sure the lastUsed property is correctly hydrated and dehydrated in the CountableSynth class:
This may be enough, but now we have to be careful about any new attribute that is added to the Countable class, also the attribute type to make sure we are parsing the value correctly. In some cases, we may have already registered one property synthesizer for the type of properties we want to use. For example, Livewire already has a CarbonSynth::class
, but in this case, it is not used because our synthesizer only evaluates the object at the top level and is not requesting dehydration for each attribute. To do so, we can use the $dehydrateChildCallback
and `$hydrateChildCallback`` callbacks on the dehydrate and hydrate methods as follows:
Take a look at the snapshot for the Countable component now:
Testing Synthesizers
Here are a few key points you can follow to test Synthesizers using PHPUnit in your Laravel application.
Registering Synthesizers for tests:
As mentioned before, you need to register the Synthesizers in your application so they can be used by Livewire, but you can also do this manually during tests as follows:
Testing dehydration
I think this is both one of the most important and difficult things to test when we are writing Synthesizers, as we probably end up testing manually in the browser to make sure everything is working correctly. One extra step you can take here is to test the actual data that is being returned to the frontend. You can render the component snapshot data as an array and run some tests against the result to make sure the object data is being parsed correctly:
Testing validation
You'll need to implement the get/set methods on your Synthesizers if you want to support data binding, but also, you'll need this if you want to validate the attributes in your custom properties. To make sure the validation is working as expected, it would be highly recommended to have coverage for that as well:
In the example above, we are assuming the validation is executed inside a save() method in the Counter class.
One of the common errors I've seen, regardless of Synthesizers and validation, is not making sure the objects are dehydrated correctly into arrays, especially when working with nested object properties or iterables.
When creating Synthesizers don't forget the following:
-
Comprehensive Dehydration: Ensure that all properties within your custom objects are effectively dehydrated into plain array or JSON objects. This meticulous process prevents potential issues with data validation during hydration on the frontend.
-
Use $dehydrateChildCallback and $hydrateChildCallback: These callbacks are important when working with nested or complex attributes within your custom objects, make sure you are implementing them within the dehydrate and hydrate methods to correctly format the attributes of your custom objects.
-
Implement Set and Get Methods: For custom attributes that support data binding and validation, it's crucial to implement the set and get methods within your Synthesizer. These methods enable Livewire to manage changes and validations for your custom properties effectively.
-
Manual Synthesizer Registration: During the testing phase, you may need to register your Synthesizer manually, especially when using PHPUnit to test Livewire components. This ensures that your custom object types are recognized and handled correctly within your tests.
-
Snapshot Data Validation: Include tests to validate the snapshot data of your components. This step is essential to confirm that the dehydrated data of your custom objects is being correctly parsed into Livewire components.
-
Livewire Testing for Validation: Utilize Livewire's built-in testing capabilities to rigorously validate the functionality of your custom object. This allows you to ensure that data binding, validation, and other Livewire-specific features are working as expected.
In conclusion
While Livewire natively supports a range of primitive and common PHP types, custom objects and properties can be challenging to work with. Livewire Synthesizers offer a powerful and flexible way to extend Laravel's Livewire framework to handle custom property types with ease. Synthesisers allow you to support direct data binding to custom object types in a more transparent manner without being forced to implement instances in your class objects keeping your domain logic separated from your Livewire/Interface logic.
Resources