Merge Laravel Resources into one
Asked Answered
E

3

9

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?

Emotionality answered 7/2, 2018 at 17:40 Comment(3)
Why don't u use merge on colection ?Orbital
@MahdiYounesi I can merge on the collection but when I pass it through the Resource for that model it won't have the correct keys.Emotionality
Need an elegant solution too. Have something like this for the moment: $clientArray = parent::toArray($request); if (array_key_exists($clientArray, "user")) { $clientArray = array_merge($clientArray, $clientArray["user"]); unset($clientArray["user"]); }Vicechairman
E
5

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,
        ];
    }
]
Emotionality answered 8/2, 2018 at 18:38 Comment(3)
this code is really strange, doesn't understand it.Vicechairman
So a resource file is just an extension of the model object which enables you to reference relationships in the model. The mergeWhen() functions is specific to resources and can be found in the docs. I've corrected a minor mistake and added some comments.Emotionality
Ok sorry, i should be more clear. I don't understand your function which has no return. You seem to do an array but no brackets. Netbeans agree me. Your syntax is still not correct. But i understand what you wanted to do.Vicechairman
K
7

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)]);
Kawai answered 30/6, 2019 at 4:26 Comment(0)
E
5

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,
        ];
    }
]
Emotionality answered 8/2, 2018 at 18:38 Comment(3)
this code is really strange, doesn't understand it.Vicechairman
So a resource file is just an extension of the model object which enables you to reference relationships in the model. The mergeWhen() functions is specific to resources and can be found in the docs. I've corrected a minor mistake and added some comments.Emotionality
Ok sorry, i should be more clear. I don't understand your function which has no return. You seem to do an array but no brackets. Netbeans agree me. Your syntax is still not correct. But i understand what you wanted to do.Vicechairman
G
0
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);
}
Geum answered 28/8 at 12:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.