In working on a new Laravel-based REST API over an existing database, I quickly realized the need to shield API consumers from my underlying database structure, especially since I knew the structure would be changing. This is a common concern in API design and many times I would turn to creating DTOs (Data Transfer Objects) or transformer classes using either a scaled-down Transformer pattern or the excellent Fractal package. However, for this project time was of the essence so I needed a very quick way to simply provide a transformation map without having to create new transformer classes for each model and without having to update the visible
and appends
attributes on every model.
Hence, the Transformable
trait was born. It’s a simple PHP trait that you drop into your model and then override the $transformation_map
property to specify your consumable model attributes.
Here’s an example:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use App\Traits\Transformable; class Group extends Model { use Transformable; protected $transformation_map = [ 'Group_ID' => 'id', 'Name' => 'name', 'PublicGroup' => 'public', ]; }
A quick explanation: we’re importing the Transformable trait (which I’ll cover shortly) and then we’re providing a database column name-to-consumable attribute name mapping. That’s it.
A consumer of this model will not see the underlying database column names on the left hand of the mapping such as “Group_ID”, “Name”, or “PublicGroup.” Instead, they’ll see the names on the right hand side: “id”, “name” and “public.” In essence, it’s a simple transformer. Here’s an example usage:
use App\Models\Group; class GroupsController { public function index() { return Group::all(); } }
With a call to the above GroupsController@index
method, we’d end up with the following JSON:
[ { "id": 1, "name": "Group 1", "public": "0" }, { "id": 2, "name": "Group 2", "public": "0" } }
Additionally, we can create the model specifying the consumable attribute names (not the database column names):
$group = new App\Models\Group(['name' => 'Group 3', 'public' => true]); // Calling this $group->toJson(); // Will yield this { "id": 3, "name": "Group 3", "public":"1" }
The magic of this is in the Transformable trait. Normally, to achieve this, we’d have to specify the $visible
and $appends
properties on our model and provide get and set mutators for each attribute mapping. Instead, with Transformable, we simply provide the $transformation_map
and the Transformable trait takes care of the rest.
It does this by overriding several methods of the Eloquent model that determine if get and set mutators exist to include checking the $transformation_map
property. This is how it can return you an instance of your model as well as create an instance of your model with the consumable attribute names without you needing to specify the source database column names.
I’m working on an enhancement to Transformable to allow queries (i.e. where* methods) on the models so that you can query with the consumable attribute names and not the database column names. As soon as I wrap up those changes, I’ll look at making this a composer package.
Here’s the entire trait implementation:
namespace App\Traits; use Illuminate\Support\Str; Trait Transformable { protected $transformation_map = []; /** * Determine if a get mutator exists for an attribute. * * @param string $key * @return bool */ public function hasGetMutator($key) { return $this->keyExistsInTransformationMap($key) || parent::hasGetMutator($key); } /** * Determine if a set mutator exists for an attribute. * * @param string $key * @return bool */ public function hasSetMutator($key) { return $this->keyExistsInTransformationMap($key) || parent::hasSetMutator($key); } /** * Get the value of an attribute using its mutator. * * @param string $key * @param mixed $value * @return mixed */ protected function mutateAttribute($key, $value) { if ($this->keyExistsInTransformationMap($key)) { return $this->{$this->getTransformationMapInverse()[$key]}; } return parent::mutateAttribute($key, $value); } /** * Set a given attribute on the model. * * @param string $key * @param mixed $value * @return $this */ public function setAttribute($key, $value) { if ($this->keyExistsInTransformationMap($key)) { if (method_exists($this, 'set'.Str::studly($key).'Attribute')) { parent::setAttribute($key, $value); } $key = $this->getTransformationMapInverse()[$key]; } return parent::setAttribute($key, $value); } public function keyExistsInTransformationMap($key) { return array_key_exists($key, $this->getTransformationMapInverse()); } public function getTransformationMapInverse() { return array_flip($this->transformation_map); } }
This could certainly be enhanced further to support casting to/from datatypes and supporting compound properties via a Closure or similar.
Let me know what you think in the comments.