Mike Rockétt Systems Developer & Designer @ Megafreight
← Posts 6 February 2021, 4 min read

A clean approach to casting Eloquent Resources in Laravel

Using helpers and scopes in a trait, make the model responsible for determining its resource class.

For quite some time now, Laravel has supported API Resources, which are handy when you need to break models and collections of models down into bytesize chunks – great as a data-saver, and even better as a performance-booster.

In my day-to-day, I make heavy use of resources, and so I like to use a clean, fluent API for accessing them. Resources tend to be a middle-man, somewhere between the data access layer and the presentation layer. Given that the model (data access layer) is responsible for determing how to hold its data, it could also be used for telling the presentation layer what to provide.

Normally, you might find yourself doing this:

return new UserResource($request->user()); 
 

And this:

return new UserResourceCollection(User::active()->subscribed()->paginate()); // or
return UserResource::collection(User::active()->subscribed()->paginate()); 

That’s all well and good, if you don’t do it to often.

But what if you do, and you need to rename the class? What if your code editor doesn’t support symbol-renames? Even if usage is far and few between, it’s always a nice idea to drain out repetition.

A decent – but not perfect – solution is to wrap up the behaviour of both of the above calls into a trait, using helper methods and scopes. This gets you from the above, to this:

return User::active()->subscribed()->asResourceCollection();
 

Here’s the whole trait:

<?php

namespace App\Traits;

use Illuminate\Http\Resources\Json\{AnonymousResourceCollection, JsonResource};
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\AbstractPaginator;

trait CastsToResource
{
  public function resolveResourceClass(string $resourceClass = null): string
  {
    return $resourceClass ?? $this->usesResourceClass();
  }

  public function asSingleResource(string $resourceClass = null): JsonResource
  {
    $resourceClass = $this->resolveResourceClass($resourceClass);

    return new $resourceClass($this);
  }

  public function scopeAsResourceCollection(
    Builder $builder, 
    string $resourceClass = null, 
    string $path = null
  ): AnonymousResourceCollection
  {
    $resourceClass = $this->resolveResourceClass($resourceClass);

    /** @var AbstractPaginator */
    $paginator = $builder->paginate();

    return $resourceClass::collection($paginator->withPath($path));
  }

  public function scopeAsResourceList(
    Builder $builder,
    string $resourceClass = null
  ): AnonymousResourceCollection
  {
    $resourceClass = $this->resolveResourceClass($resourceClass);

    return $resourceClass::collection($builder->get());
  }
}
 

And a quick walk-through:

  • resolveResourceClass accepts a class to use, if the default provided in usesResourceClass is not required in a particular instance.
  • usesResourceClass is defined in the model. Probably a good idea to create a contract for this (and the other methods):
<?php

namespace App\Contracts;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Resources\Json\{AnonymousResourceCollection, JsonResource};

interface Resourceable
{
  public function usesResourceClass(): string;
  public function resolveResourceClass(string $resourceClass = null): string
  public function asSingleResource(string $resourceClass = null): JsonResource;
  
  public function scopeAsResourceCollection(
    Builder $builder, 
    string $resourceClass = null, 
    string $path = null
  ): AnonymousResourceCollection
    
  public function scopeAsResourceList(
    Builder $builder,
    string $resourceClass = null
  ): AnonymousResourceCollection
}
  • asSingleResource is a helper method that pipes the current model instance into a new instance of the resource class.
  • scopeAsResourceCollection, called using asResourceCollection(), is a scope that’s used to paginate the current query and pipe that collection into a new resource instance – at this point, you can call additional, etc. This method also takes a resource-class override, as well a path that is tacked onto the paginator, used for setting the links.
  • scopeAsResourceList, called using asResourceList(), is a scope that’s used to get all the results for the current query, and pipe them into a new resource instance. Like above, you can override the resource class, if needed, and call other methods on the resource at this point.
 

Using this is simple:

  1. First, your model should implement Resourcable and use CastsToResource.
  2. Then, whatever needs to use the helpers may do so directly:
return User::asResourceCollection();
// a UserResource paginated collection

return User::limit(20)->asResourceList(); 
// a UserResource collection list (non-paginated)

return $request->user()->asSingleResource();
// an instance of UserResource

Looks great when you have a bit of a query going on:

return User::query()
  ->applyQueryFilters($queryFilters)
  ->when($condition, fn (Builder $builder) => $builder->whereSomething())
  ->orderBy('name')
  ->asResourceCollection('/users')
  ->additional(['users_count' => User::count()]);

I would package this, but it’s just too small for that - copy and paste is easy enough. 🍻