It has been roughly six months since we released Laravel-Mediable, our first open source Laravel package. Developing this project has been a truly fulfilling experience. Leading this project has made me a better developer by making me better at thinking about API design, writing unit tests, documenting my code, and much more knowledgeable about the internal workings of the Laravel framework. The response has been even more amazing: to date, our package has been downloaded through composer over 2,700 times, has been starred 250 times on Github. We have merged 22 pull requests from the community, and have received nothing but positive feedback and accolades from the many developers who have made use of our code. Our package has even been featured by Freek van der Herten.
With all of that in mind, it was only natural that we would want to find new things to contribute to such a welcoming open source community. Today, we are happy to announce Laravel-Metable our newest Laravel package. Check out Laravel-Metable on GitHub, browse the documentation or read on for more information about how this package came to be.
While building a custom CMS for one of our clients, we came across an interesting problem. The site we were building allowed our client to create new pages and assign each page to a template to customize its appearance. However, each template needed a handful of additional fields to modify how the template would behave. We needed to figure out how to store and retrieve these additional fields. With dozens of templates, each having up to ten extra fields, we couldn't simply add all of these as columns to the pages table as it would quickly become unwieldy.
It immediately occurred to us that other publishing platforms had previously solved a similar problem. Though Plank has mostly moved away from using WordPress for our client work, the platform was able to serve as an inspiration for how to tackle our problem. In WordPress, just about all content is some form of "post" in the
wp_posts table and all custom fields describing or modifying these posts are "meta" in the
wp_post_meta table. Meta is effectively a key/value pair with a foreign key to the wp_posts table. This simple structure allows WP developers to attach a virtually unlimited number of additional fields to their post types, without needing to change anything in the database.
Of course, we didn't want to simply rewrite WordPress's implementation of Meta for Laravel. Though the post meta API is relatively straight forward, We had a number of complaints with the way WordPress handled things:
- Meta is intrinsically tied to the wp_posts table. Though the code base does include mechanisms for attaching meta to other WP Object types (e.g. users, taxonomies, terms), the database tables for these need to manually created, and each requires a separate table.
- Anything added as meta is cast to a string and always returned as a string. If you need to handle any more complex data types, it is up to the developer to serialize the values before applying it as meta, and then unserialize it before using it in a template.
- The API allows setting multiple values to the same key and sometimes returns an array of strings instead of a string. This inconsistency can cause confusion and problems for indexing.
We searched the ecosystem of existing laravel packages hoping to find something that would address our needs. We found a handful of packages that tried to address the problem, but each came up a little short. Most of the packages we found suffered from similar issues: a lack of polymorphism, a lack of dynamic typing and/or a complete lack of caching. Clearly, there was a gap in the ecosystem that we could fill.
Building a Package we are Proud to Release
Over the course of the holidays, I devoured Adam Wathan's stellar video course, Test-Driven Laravel. Having completed all of the videos published to date, I was eager to give TDD a spin. Given its clear feature set, this project seemed like an ideal candidate to be able to think about what the tests might look like before writing any actual code. This approach proved to be a significant boon to the project, as thinking about testable features instead of going straight to implementation made it much easier to think about meaningful abstractions, and helped keep the code cleaner. I am happy to announce that the initial release of the package has 100% test coverage!
The final package is comprised of three primary components:
Most users of the package will interact almost exclusively with this trait, which can be dropped into any Eloquent model to extend it with all of the features of this package. The trait adds a number of helper methods and query scopes to the model to allow them to interact with Meta.
The trait uses a polymorphic one-to-many relationship under the hood for associating Meta records, however, this behavior is abstracted away from the users, who only need to concern themselves with the key/value pairs that the meta represent. For example
$model->setMeta('key', 'some value'); $model->getMeta('key');
By building on top of Eloquent's relationship system, we were able to take advantage of built in functionality for eager loading and caching the loaded meta in memory to save unnecessary database queries. We are patching into the system, however, to keep the meta indexed by key, to make lookups even faster.
Meta is an Eloquent subclass which keeps track of one key/value pair of data. In addition to these two fields, each meta record also tracks a polymorphic foreign key to the Metable model it is attached to and a "type" field which keys track of the kind of data it is storing, so that it can be returned in the proper format.
Meta records store the value field in the database as a string. Any values passed need to be serialized to a string for storage and then unserialized back to their original type. The data type Registry (below) handles this serialization, but each Meta instance will cache the unserialized value upon access, to optimize cases where rebuilding the value is an expensive operation (e.g. loading other models from the database).
One of our most important goals was creating a system which could store and retrieve any number of data types, whether they be scalar values or complex objects. Rather than implement a long switch or if/elseif statement for determining how to handle a particular value, we resolved to construct an abstracted solution that would be fully extensible. Though our package comes with a fairly exhaustive set of supported data types out of the box, users can add support for their own classes by registering their own Handlers which take care of serializing and unserializing the data.
The result of this implementation is a package that we think is not only very robust, efficient and extensible, but extremely intuitive and user-friendly. We hope you enjoy it!