At Whitecube, we’re using Laravel for most of our projects. For some of them, we used to build custom admin panels and as you may have experienced it yourself, that’s not always as simple as it sounds. More often than not, the benefits of a stable community-driven product outnumber those of a custom alternative. Therefore, we’ve been switching a lot of our back offices to Laravel Nova since the day it came out. Even if there are still a lot of improvements that could be made, Nova’s popularity is growing and the tool has, in my opinion, a lot of potential to become the tool we were all wishing for.
In the past, we also worked with WordPress for our smaller projects and even if I don’t regret adopting Laravel for a second, I think we can all agree it had its bright sides as well. Especially when using the simple yet powerful features of the Advanced Custom Fields plugin (ACF), of course. Luckily, Laravel Nova did what WordPress failed to do and implemented an easy way to integrate user-defined custom fields from the very beginning. More importantly, developers can now customize the UI by editing version-controlled files, without having to configure and duplicate database structures into never-ending JSON files. Luxury.
But, let’s be honest: not every field out there has the clarity and the flexibility of ACF’s bundle. The main reason being, I think, the lack of a collective vision shared by all package authors. Most fields just don’t work well together without workarounds. I don’t blame anyone, all package authors have their own needs and we all simply build the solutions that solve our problems. Plus, sharing individual work as open-source packages is probably what gives our community the strength it has today. The important thing I’m trying to say here is that we should start planning the next step together if we want Nova to reach the huge potential that it has.
This brings me to the main subject of this post which, I hope, will be one humble step in that direction: let’s create fields that can understand each other’s output.
But first… Why?
Most of the basic fields we need are already built-in, meaning the majority of our field contributions can be considered as complex fields. What I call “complex” is not specially something difficult to create. It’s more about the role it covers or the benefits it offers in specific use cases. One common thing for a lot of these complex fields is that they have to interact with other fields.
When writing classes that need to interact, in PHP or other languages, we often take advantage of Interfaces. They can act as contracts between developers, assuring some capabilities are available when they’re implemented. Pretty handy, isn’t it? Yep. But for some reason we often forget to use them when designing Nova Field outputs.
I’m pretty sure this forgetfulness comes from a misleading idea everybody has about fields, being: “it should store its value on a model” or “its value will not evolve and can be considered final”. Even if that’s probably the case most of the time, this should only be true for basic fields outputing basic values such as an integer, a raw string or any other direct user input.
If your field treats a value, doesn’t output anything or stores a JSON string on the model, you should probably let it know somehow.
When we’re talking about fields filling attributes on models, you should keep in mind that those models are not necessarily real Eloquent Models. Fake models could for example be substitute classes handling Laravel Nova fields in particular contexts where knowing what their values are about is essential.
The last one, Nova Flexible Content, which basically groups fields in different layouts used to provide repeatable content to resources, illustrates why serializable value wrappers are so important. In this particular case, the field interacts with other sub-fields using its own layout classes, substituting the model. Those layout classes will later be assembled into a Collection, which will be set as an attribute on what could be the final model.
This complex field couldn’t work properly if it directly hydrated the “model” with a JSON string.
Because other complex fields (maybe another higher-level Flexible field) wouldn’t be aware of its JSON nature, meaning they could JSON encode a previously encoded JSON string, which would end up into a considerable mess to decode. By using Collections (or other serializable objects), the whole nested structure remains clean and can be encoded at once when the model is finally sent to the database.
Simply put: we need complex values wrapped into instances of serializable classes in case other fields need to interact with them.
How do we know when to return storage-ready values?
We don’t. The real question should be: do we even need to know? We have an incredible ORM working behind the scenes, Eloquent. It will take care of transforming our field’s output into the value we want it to be when its time has come. The only thing we need to do is to explain how to achieve this transformation.
That’s something we can do using PHP’s magic methods, like __toString. In case of a desired JSON result, we could even implement the JsonSerializable interface. If the value is retrieved as an array, Laravel’s Collections are probably the way to go. It doesn’t necessarily need to be hard to implement, most of the time it will make the code more readable, testable and maintainable.
Okay, but… when does a field return its wrapped value?
Technically speaking, we do not return a value when working with Nova fields. The field directly hydrates models using its extended fillAttributeFromRequest method, “storing” its value directly on the concerned model. When another field needs to interact with this value, it could use the model to access the targeted attribute.
Laravel Nova’s documentation shows a very straightforward example on how to store direct request input on the model:
Most of the time however, complex fields need to act upon the provided request data, meaning the data is first treated by some custom methods:
This approach requires to write a lot of value treatment logic in the field itself. Instead, we could extract those methods into a value object, where all the value treatment would take place. This way, our core Field class remains tidy and the Value class can now become the subject of its own specific unit tests. Furthermore, by implementing serialization interfaces on the Value class, we don’t even need to extract the treated value before assigning it to the model’s attribute.
Without knowing it, we didn’t only implement a few good code design principles, we also opened the door to a new world of value sharing between field packages. In the last example, our MySerializableNovaFieldValue class can now implement interfaces that provide useful information to other third-party actors. The JsonSerializable interface is probably one of the most straightforward examples, but why not go further? Maybe some day we’ll find common patterns in field interactions that could be translated into “official” Nova interfaces, providing the missing collective view. Am I dreaming out loud or could this become a reality? Let’s find out.
One thing I didn’t mention yet is that a similar scenario could take place during a field’s resolving process since it could also be exploited or displayed in other related complex fields. Why not use the same Value class during the field resolving and the attribute filling processes? It certainly depends on the field’s specificities, but it’s worth thinking about.
I’ve been using Laravel Nova on a few projects and have searched for, installed and built some packages to improve its interface or features. One thing I learned is that most other developers are doing the same, but nobody really uses those packages in the same way or for the same purposes. We’re all pretty creative when it comes to implementing tools.
Let’s make each other’s life easier by creating adaptable packages or at least understandable results, especially when we’re talking about packages handling small bits of complicated user interfaces, such as Nova Fields.
I’m sure there are plenty other things worth mentioning in this topic and that some important use cases were left out. Can you think about some of them? I’d be glad to discuss them, let me know on Twitter!