2
votes

I have an API that uses API resource and resource collections to correctly format the JSON responses. In order to decouple my controller from my model I use an adapter to query the underlying model. I'd like to pass the adapter return values as arrays, rather than Eloquent models, to ensure that any furture adapters are easier to right in respect to their return data structures. To create the array return values I serialise my adapter Eloquent results with ->toArray().

I have 2 API Resources to correctly format these results, for a single resource I have:

use Illuminate\Http\Resources\Json\Resource;

class Todo extends Resource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return $this->resource;
    }
}

For a resource collection I have:

use Illuminate\Http\Resources\Json\ResourceCollection;

class TodoCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'data' => $this->collection
                          ->map
                          ->toArray($request)
                          ->all()
        ];
    }
}

When I return a single resource from my controller with :

use App\Http\Resources\Todo;

public function show($id)
{
    return new Todo($this->todoAdapter->findById($id));
}

and the adapter query as:

public function findById(int $id){
        return TodoModel::findOrFail($id)
                ->toArray();
}

This works as expected. The problem comes when I try to pass an array of a collection of models i.e.

public function index(Request $request)
{
    $todos = $this->todoAdapter->getAllForUserId(Auth::id(), 'created_by', 'desc', self::DEFAULT_PAGINATE);
    return new TodoCollection($todos);
}

and the adapter query as:

public function getAllForUserId(int $userId, string $sortField, string $sortDir, int $pageSize = self::DEFAULT_PAGINATE)
{
        return Todo::BelongsUser($userId)
                                    ->orderBy($sortField, $sortDir)
                                    ->paginate($pageSize)
                                    ->toArray();
}

I get the following error:

"message": "Call to a member function first() on array",
    "exception": "Symfony\\Component\\Debug\\Exception\\FatalThrowableError",
    "file": "/home/vagrant/code/public/umotif/vendor/laravel/framework/src/Illuminate/Http/Resources/CollectsResources.php",
    "line": 24,

I'm guessing that I can't do 'new TodoCollection($todos)' where $todos is an array of results. How would I get my todoCollection to work with arrays? Any suggestions would be much appreciated!

2
you could have make this much simplier than this - Sohel0415
In what way do you mean? - InTooDeep
you could have passed collection object in your resource and make it an array there as laravel documentation says - Sohel0415
How would I do that in the resource? Isn't the point of returning an array from the adapter to make it easier to implement other adapters? For example if I'm getting data via an API adapter then I won't need to convert that to a collection if the resource expects an array? - InTooDeep
please have a look laravel.com/docs/5.5/eloquent-resources#introduction, it says ` Laravel's resource classes allow you to expressively and easily transform your models and model collections into JSON.` - Sohel0415

2 Answers

0
votes

Your collections toArray is trying to do too much:

$this->collection
         ->map
         ->toArray($request)
         ->all()

Just directly call $this->collection->toArray().

0
votes

Just to update this. In the end I found that creating a collection from the array of results and passing that to the resource collection constructor worked, though I did have to add explicit mappings within the resource collection for links and meta etc.