Tom Headifen
Tom Headifen

Reputation: 1996

Merge Laravel Resources into one

I have two model resources and I want to be merged into a flat array without having to explicitly define all the attributes of the other resource.

Model 1:

id
name
created_at

Model 2:

id
alternate_name
child_name
parent_name
sibling_name
created_at

Model1Resource

public function toArray($request)
{
    return [
        id => $this->id,
        name => $this->name,
    ]
}

Model 2 Resource

public function toArray($request)
{
    return [
        alternate_name => $this->alternate_name, 
        child_name => $this->child_name, 
        parent_name => $this->parent_name, 
        sibling_name => $this->sibling_name
    ]
}

I want Model1Resource to contain Model2Resource in a flat structure. I can easily get the Model 2 resource in a sub array by adding in another attribute to the resource like so:

Model2 => new Model2Resource($this->model2);

But this is not the flat structure. Ideally I would want to be returned a structure like this.

[id, name, alternate_name, child_name, parent_name, sibling_name]

I could do this by redefining all the attributes of Model2Resource in the Model1Resource but this seems unnecessary.

To clarify I am referring to https://laravel.com/docs/5.5/eloquent-resources#writing-resources. Under the relationships section a one to many relationship is demonstrated using posts. However if the structure is one to one I would expect to be able to make this a flat array instead of having an array in one of the properties.

What's an easy way to merge these two resources into one with a flat structure?

Upvotes: 9

Views: 14350

Answers (3)

David Auvray
David Auvray

Reputation: 317

public function toArray($request)
{
    // base resource
    $baseData = (new UserResource($this->resource))->toArray($request);

    // second resource
    $currentData = [
        'slug' => $this->slug,
        'identifier' => $this->identifier, 
    ];

    // merge
    return array_merge($baseData, $currentData);
}

Upvotes: 0

Tom Headifen
Tom Headifen

Reputation: 1996

So after some digging this doesn't seem to be easily possible. I've decided the easiest way is to just redefine the outputs in the first model and use the mergeWhen() function to only merge when the relationship exists.

return [
    'id' => $this->id,
    'name' => $this->name,
    // Since a resource file is an extension
    // we can use all the relationships we have defined.
    $this->mergeWhen($this->Model2()->exists(), function() {
        return [
            // This code is only executed when the relationship exists.
            'alternate_name' => $this->Model2->alternate_name, 
            'child_name' => $this->Model2->child_name, 
            'parent_name' => $this->Model2->parent_name, 
            'sibling_name' => $this->Model2->sibling_name,
        ];
    }
]

Upvotes: 5

mixel
mixel

Reputation: 25846

Create base class for you resources:

use Illuminate\Http\Resources\Json\JsonResource;

class BaseResource extends JsonResource {
    /**
     * @param bool $condition
     * @param Request $request
     * @param JsonResource|string $instanceOrClass
     * @param mixed|null $model
     * @return MergeValue|mixed
     */
    public function mergeResourceWhen($condition, $request, $instanceOrClass, $model = null)
    {
        return $this->mergeResourcesWhen($condition, $request, [$instanceOrClass], $model);
    }

    /**
     * @param Request $request
     * @param JsonResource|string $instanceOrClass
     * @param mixed|null $model
     * @return MergeValue|mixed
     */
    public function mergeResource($request, $instanceOrClass, $model = null)
    {
        return $this->mergeResourceWhen(true, $request, $instanceOrClass, $model);
    }

    /**
     * @param bool $condition
     * @param Request $request
     * @param JsonResource[]|string[] $instancesOrClasses
     * @param mixed|null $model
     * @return MergeValue|mixed
     */
    public function mergeResourcesWhen($condition, $request, $instancesOrClasses, $model = null)
    {
        return $this->mergeWhen($condition, function () use ($request, $instancesOrClasses, $model) {
            return array_merge(...array_map(function ($instanceOrClass) use ($model, $request) {
                if ($instanceOrClass instanceof JsonResource) {
                    if ($model) {
                        throw new RuntimeException('$model is specified but not used.');
                    }
                } else {
                    $instanceOrClass = new $instanceOrClass($model ?? $this->resource);
                }
                return $instanceOrClass->toArray($request);
            }, $instancesOrClasses));
        });
    }

    /**
     * @param Request $request
     * @param JsonResource[]|string[] $instancesOrClasses
     * @param mixed|null $model
     * @return MergeValue|mixed
     */
    public function mergeResources($request, $instancesOrClasses, $model = null)
    {
        return $this->mergeResourcesWhen(true, $request, $instancesOrClasses, $model);
    }
}

Model1Resource (no need here to extend BaseResource but I always inherit all my API resource classes from my own custom base class):

class Model1Resource extends JsonResource {
    public function toArray($request)
    {
        return [
            id => $this->id,
            name => $this->name,
        ];
    }
}

Model2Resource:

class Model2Resource extends BaseResource {
    public function toArray($request)
    {
        return [
            $this->mergeResource($request, Model1Resource::class),
            alternate_name => $this->alternate_name, 
            child_name => $this->child_name, 
            parent_name => $this->parent_name, 
            sibling_name => $this->sibling_name
        ];
    }
}

If you want to merge multiple resources then you can use:

$this->mergeResources($request, [Model1Resource::class, SomeOtherResource::class]);

If you want to merge it by condition:

$this->mergeResourceWhen($this->name !== 'John', $request, Model1Resource::class);
// or merge multiple resources
$this->mergeResourcesWhen($this->name !== 'John', $request, [Model1Resource::class, SomeOtherResource::class]);

By default merged resources will use current model available by $this->resource. To pass other model to merged resources use last parameter of above methods:

$this->mergeResource($request, SomeModelResource::class, SomeModel::find(123));
$this->mergeResourcesWhen($this->name !== 'John', $request, [SomeModelResource::class, SomeOtherResource::class], SomeModel::find(123));

or pass JsonResource instance(s) instead of resource class(es):

$someModel = SomeModel::find(123);
$someOtherModel = SomeOtherModel::find(456);
$this->mergeResource($request, new SomeModelResource($someModel));
$this->mergeResourcesWhen($this->name !== 'John', $request, [new SomeModelResource($someModel), new SomeOtherModelResource($someOtherModel)]);

Upvotes: 7

Related Questions