Laravel API Resources — What if you want to manipulate your models before transformation?

So, I recently came across this issue when showing a colleague how awesome Laravel’s API Resources are. It turns out that passing external attributes down to a resource collection’s children doesn’t happen automatically like I first thought. And then I realised that Resources aren’t magic! Why did I think they were magic? There is nothing in the documentation to suggest this. I just assumed like so many other features of Laravel that if you created a UserResource and a UserCollection that the UserCollection would automatically understand that the UserResource was the singular of the collection and would automatically transform the User model into a UserResource even though you pass in a collection of User models to the UserCollection resource. Make sense, easy, right? Well, it doesn’t work like that. API Resources are just there to do what you tell them to do, which makes them very powerful IMHO.

So, if there is no magic then how do they work?

Essentially they are just simple objects with one very important job to do — transform your objects (interesting I said objects and not models). To do this out of the box, all you have to do is instantiate the Resource (collection or individual) with an Arrayable object. If you did nothing else but generate a standard Resource and pass in an Arrayable object the Resource would transform that object automatically, and because Models are Arrayable this is where I got caught out because if you create a resource collection and instantiate it with a collection of models then the models get toArray'd and not their corresponding resource.

<?php

namespace
App\Http\Controllers\Api;

use App\Http\Resources\UserCollection;
use App\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
/**
* List all users.
*
*
@param Request $request
*
@return UserCollection
*/
public function index(Request $request)
{
$users = User::all();

return new UserCollection($users);
}
}

This will just toArray()the models never hitting the UserResource at all.

Ok, if the resource collection doesn’t automagically convert the model into the corresponding resource what do we do?

We transform the collection of models into a collection of resources. Easy! Yes? Well it’s easier than you think. We could do this in the controller with something like this;

<?php

namespace
App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\UserCollection;
use App\Http\Resources\UserResource;
use App\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
/**
* List all users.
*
*
@param Request $request
*
@return UserCollection
*/
public function index(Request $request)
{
$users = User::all();

$users->transform(function (User $user) {
return (new UserResource($user));
});

return new UserCollection($users);
}
}

This is perfectly acceptable, but it’s not very clean and we are already clogging up our controller with stuff it just shouldn’t care about. So where can we move this to? Well, how about the UserCollection::toArray() method?

<?php

namespace
App\Http\Resources;

use App\User;
use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
*
@param \Illuminate\Http\Request $request
*
@return array
*/
public function toArray($request)
{
$this->collection->transform(function (User $user) {
return (new UserResource($user));
});

return parent::toArray($request);
}
}

And revert our controller back to the original;

<?php

namespace
App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\UserCollection;
use App\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
/**
* List all users.
*
*
@param Request $request
*
@return UserCollection
*/
public function index(Request $request)
{
$users = User::all();

return new UserCollection($users);
}
}

Much nicer. We only care about creating a collection of UserResources when we are dealing with a UserCollection so this nicely encapsulates our code.

Ok, but aren’t you missing the point of this article, Dean? How do we pass meta information from the collection to it’s children?

Yes, yes, I’m getting to that.

So, scenario is that you want to transform your collection of users into a nice JSON API for the world to see. We have that now, but we also want to manipulate the JSON output for the individual UserResource based on some global value. How can we do this?

As an example say each user has a Bitcoin worth (yeah I work for a Crypto company now so this seems appropriate), so we might have a UserResource that looks like this;

<?php

namespace
App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class UserResource extends Resource
{
/**
* Transform the resource into an array.
*
*
@param \Illuminate\Http\Request $request
*
@return array
*/
public function toArray($request)
{
return [
'name' => $this->name,
'username' => $this->username,
'bitcoin' => $this->bitcoin,
];
}
}

But what if we wanted to automatically convert the bitcoin value to USD or AUD for instance. Yeah we could do that on the model itself, but that would defeat the purpose of this article. We want to pass the convertTo meta data from the controller all the way down to the individual resource. Well Taylor to the rescue once again, all we need to do is set the additional property on the top most resource and we can do that like this;

<?php

namespace
App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\UserCollection;
use App\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
/**
* List all users.
*
*
@param Request $request
*
@return UserCollection
*/
public function index(Request $request)
{
$users = User::all();

return (new UserCollection($users))->additional([
'meta' => [
'convertCryptoTo' => 'AUD'
]
]);
}
}

The nice thing about this is that we get this automatically added to the top most resource as meta information in our JSON output. NICE!

So now that our UserCollection has this useful additional meta info how do we get it to our UserResource. Well that is easy because the UserCollection extends the ResourceCollection which in turn extends the Resource class which has the attributes property, making it available to both ResourceCollections and Resources. Awesome, we can just set the attributes on the UserResource then with the ones on the UserCollection.

<?php

namespace
App\Http\Resources;

use App\User;
use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
*
@param \Illuminate\Http\Request $request
*
@return array
*/
public function toArray($request)
{
$this->collection->transform(function (User $user) {
return (new UserResource($user))->additional($this->additional);
});

return parent::toArray($request);
}
}

Wow that was easy, last thing to do is actually do the conversion on our UserResource. Easy peasy!

<?php

namespace
App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class UserResource extends Resource
{
/**
* Transform the resource into an array.
*
*
@param \Illuminate\Http\Request $request
*
@return array
*/
public function toArray($request)
{
$convertTo = $this->attributes['meta']['convertTo'];

return [
'name' => $this->name,
'username' => $this->username,
'bitcoin' => $this->bitcoin,
'conversion' => $this->convertTo($convertTo, $this->bitcoin)
];
}

private function convertTo($convertTo, $bitcoin)
{
return ...
}
}

And that’s it. I’ll let you figure out how to convert Bitcoin into Fiat as I’m just too lazy.

Anyway, your response to this should look something similar to this;

{
"data": [
{
"name": "Dean Tedesco",
"username": "developerdino",
"bitcoin": 100,
"conversion": 1000000
}
],
"meta": {
"convertTo": "AUD"
}
}

Nice ha! I thought so.

By the way that Bitcoin is totally fictitious, I wish I had 100 BTC.

Hope you found this useful. If you did check out my other article on Laravel API Resources — Using Laravel 5.5 Resources to create your own {JSON:API} formatted API