Skip to content

Instantly share code, notes, and snippets.

@mdcass
Last active August 25, 2022 12:52
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mdcass/e7e210fab44d4e86c0139b233744928c to your computer and use it in GitHub Desktop.
Save mdcass/e7e210fab44d4e86c0139b233744928c to your computer and use it in GitHub Desktop.
Transformer pattern in Laravel 5

Transformers

Transformers contain the business logic for changing a model's format to whatever you need for the output, whether that's HTML, Datatable, or JSON API. Decoupling this logic from the model allows for unobtrusive schema changes, a reliable place for housing formatting logic, and data formatting operations outside of views (for example, converting a date, or performing translations).

Guidelines

  • Transformers, like models, could be used throughout the application, and should be stored in app/Transformers
  • Transformer class naming should be named after your model, for example UserTransformer

Tutorial

Simple Transformers

Transformation blueprints can be set on the Transformer class as properties, and should be an array of field names belonging to your Model. Create your transformer, remembering to extend the BaseTransformer class:

// app/Transformers/UserTransformer.php
namespace App\Transformers;

class UserTransformer extends BaseTransformer
{
    /**
     * Simple list of Users by their ID (suitable for a dropdown menu)
     */
    protected $listFormat = ['id', 'email'];
}

And that's it, it's ready to use! You simply pass your Model (or many Models) to your Transformer and specify the format (in our example's case, list), and the transformer will take care of the rest.

Passing a Single Model

$user = User::where('email', 'mike.casson@airangel.com')->firstOrFail();
$list = UserTransformer::transform($user)->into('list'); // array:2 ['id' => 1, 'email' => 'mike.casson@airangel.com']

Passing a Collection of Models

$users = User::where('status', 'active')->get();
$list  = UserTransformer::transform($users)->into('list');
/* Outputs:
array:x [
    ['id' => 1, 'email' => 'mike.casson@airangel.com'],
    ['id' => 2, 'email' => 'john.doe@airangel.com'],
    ...
]
 */

Adding A New Formatter

You can easily add a new formatter to use elsewhere in the application by setting a new property on the Transformer class - you will need to name it using camelCase and add the word Formatter to the end:

// UserTransformer.php
protected $tableFormatter = ['id', 'email', 'active', 'company.name|cname'];

// Controller or elsewhere
$users = User::where('status', 'active')->get();
$list  = UserTransformer::transform($users)->into('table');
/* Outputs:
array:x [
    ['id' => 1, 'email' => 'mdcasso@gmail.com', 'status' => 'active', 'cname' => 'Company'],
    ['id' => 2, 'email' => 'john.doe@gmail.com', 'status' => 'active', 'cname' => 'Company'],
    ...
]
 */

Note in the table transformer blueprint, we are accessing the company relation of the User model to get the company name (equivalent to $user->company->name - make sure you eager load!!). We are also providing an alias for the field after the pipe | operator (if not provided, the key in the outputted array would be company.name).

Advanced Transformers

You can define a transformer in a method if you need further control or additional logic (in the below example, formatting a timestamp field ready for a view and translating an attribute):

// app/Transformers/UserTransformer.php
namespace App\Transformers;
use App\Model\User;

class UserTransformer extends BaseTransformer
{
    /**
     * Transformer for Datatable
     */
     public function datatableFormat(User $user)
     {
         return [
             'id'           => $user->id,
             'email'        => $user->email,
             'company_name' => $user->company->name,
             'status'       => trans($user->status), 
             'created_at'   => $user->created_at->toDayDateTimeString()
         ];
     }
}

You call the transformation in exactly the same way as before:

$users = User::with('company')->where('status', 'active')->get();
$list  = UserTransformer::transform($users)->into('table');
/* Outputs:
array:x [
    ['id' => 1, 'email' => 'mike.casson@airangel.com', 'company_name' => 'Airangel', 'status' => 'Active', 'created_at' => 'Thu, Mar 16, 2017 3:39 PM'],
    ['id' => 2, 'email' => 'john.doe@airangel.com', 'company_name' => 'Airangel', 'status' => 'Active', 'created_at' => 'Thu, Mar 16, 2017 3:39 PM'],
    ...
]
 */

No Transformer Properties or Methods Set

If you choose not to define any properties or methods, the Transformer will default to calling toArray on the Model.

<?php
namespace App\Transformers;
/**
* Class BaseTransformer
*/
class BaseTransformer
{
/**
* The model or models to be transformed
*
* @var mixed
*/
protected $subject;
/**
* The return type of transformed models (array or object)
*
* @var string
*/
protected $returnType = 'array';
public function __construct($subject)
{
$this->subject = $subject;
return $this;
}
/**
* Instantiates the class setting the subject which could be an individual
* model or collection of models to be transformed
*
* @param mixed $subject
* @return BaseTransformer
*/
public static function transform($subject)
{
return new static($subject);
}
/**
* Specifies the format to transform models into (which must be set on
* the extending class as a method with `Format` prepended
*
* @param $format
* @return mixed
*/
public function into($format)
{
$method = $format . 'Format';
$formatBy = $this->isMethodOrProperty($method);
// If we have a collection of models, iterate over to build result
if($this->subject instanceof \Traversable)
{
$result = [];
foreach($this->subject as $model)
$result[] = $this->transformSubject($model, $method, $formatBy);
return $result;
} else {
// Return the subject transformed
return $this->transformSubject($this->subject, $method, $formatBy);
}
}
/**
* Dynamically set the type of the returned result items
*
* @param string $type array or object
* @return $this
*/
public function as($type)
{
$this->returnType = $type;
return $this;
}
/**
* Checks whether the requested formatter is set on the extending
* class as a property or a method
*
* @param $method
* @return string
*/
private function isMethodOrProperty($method)
{
if(method_exists($this, $method))
return 'method';
if(property_exists($this, $method))
return 'property';
return 'toArray';
}
/**
* Transform the $subject using either the $method (or property defined in $formatBy)
* set on the extending model
*
* @param mixed $subject The model to be transformed
* @param string $method The method or property set on the extending model
* @param string $formatBy method|property
* @return array|object
*/
private function transformSubject($subject, $method, $formatBy)
{
$transformed = null;
switch($formatBy)
{
case 'property':
$result = [];
// Loop through property and use values as property on the subject
foreach($this->$method as $key)
{
// Cater for alias set in the key names
$keyParts = explode('|', $key);
$propertyPath = $keyParts[0];
$key = isset($keyParts[1]) ? $keyParts[1] : $propertyPath;
$result[$key] = array_reduce(explode('.', $propertyPath), function($obj, $prop) {
return $obj->$prop;
}, $subject);
}
$transformed = $result;
break;
case 'method':
$transformed = $this->$method($subject);
break;
case 'toArray':
$transformed = $subject->toArray();
break;
}
return $this->returnType === 'object' ? (object) $transformed : $transformed;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment