Prafulla Kumar Sahu
Prafulla Kumar Sahu

Reputation: 9693

Adding filter to eloquent resource to attach relationship conditionally

Laravel 5.8 PHP 7.4

I want to load the relationships conditionally like

http://127.0.0.1:8000/api/posts 

and

http://127.0.0.1:8000/api/posts/1 are my end points now, I want to load comments like

http://127.0.0.1:8000/api/posts/?include=comments and

http://127.0.0.1:8000/api/posts/1/?include=comments

If the query parameter is there, only then it should load comments with posts or it should load only posts/post

I am doing this by referring a blog post

now, RequestQueryFilter

<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
class RequestQueryFilter
{
    public function attach($resource, Request $request = null)
    {
        $request = $request ?? request();
        return tap($resource, function($resource) use($request) {
            $this->getRequestIncludes($request)->each(function($include) use($resource) {
                $resource->load($include);
            });
        });
    }
    protected function getRequestIncludes(Request $request)
    {
        // return collect(data_get($request->input(), 'include', [])); //single relationship
        return collect(array_map('trim', explode(',', data_get($request->input(), 'include', [])))); //multiple relationships
    }
}

and in helper

<?php
if ( ! function_exists('filter') ) {
    function filter($attach) 
    {
        return app('filter')->attach($attach);
    }
}
?>

in PostController

public funciton show(Request $request, Post $post) {
    return new PostResource(filter($post));
}

but when I am trying to retrieve

http://127.0.0.1:8000/api/posts/1/?include=comments getting no comments, with no error in log

A work around will be PostResource

 public function toArray($request)
    {
        // return parent::toArray($request);
        $data = [
            'id' => $this->id,
            'name' => $this->title,
            'body' => $this->content,
        ];

        $filter = $request->query->get('include', '');

        if($filter){
          $data[$filter] = $this->resource->$filter;
        }

        return $data;
    }

Upvotes: 2

Views: 3295

Answers (3)

Eva
Eva

Reputation: 173

I would create a custom resource for the posts with

php artisan make_resource 

command. E.g. PostResource. The toArray function of the resource must return the data.

PostResource.php

public function toArray($request){
     $data =['title' => $this->resource->title,

    'body' => $this->resource->body,

    'images' => new ImageCollection($this->whenLoaded('images')),
            ];

     $filter = $request->query->get('filter', '');

     if($filter){
      $data['comments'] => new CommentCollection($this->resource->comments);

     }
  return $data;
}

Also, for collections, you need to create a ResourceCollection.

PostResourceCollection.php

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

In your controller: PostsController.php

   //show one post
     public function show(Post $post, Request $request)
        {

           /**this response is for API or vue.js if you need to generate view, pass the resource to the view */
            return $this->response->json( new PostResource($post));
        } 
    //list of posts
        public function index(Request $request)
            {
               $posts = Post::all();
               /**this response is for API or vue.js if you need to generate view, pass the resource to the view */
                return $this->response->json( new PostResourceCollection($posts));
            } 

Upvotes: 1

Prafulla Kumar Sahu
Prafulla Kumar Sahu

Reputation: 9693

Partial Solution

It will need a small change in resource class

public function toArray($request)
{
    // return parent::toArray($request);
    $data = [
        'id' => $this->id,
        'title' => $this->title,
        'body' => $this->body,
        'comments' => new CommentCollection($this->whenLoaded('comments')),
        'images' => new ImageCollection($this->whenLoaded('images')),
    ];
    return $data;
}

and it will load comments and images if loaded and that depends on the include query parameter, if that is not included, it will not load the relationship.

However,

In post collection

return [
    'data' => $this->collection->transform(function($post){
        return [
            'id' => $post->id,
            'title' => $post->title,
            'body' => $post->body,
            'comments' => new CommentCollection($post->whenLoaded('comments')),
            'images' => new ImageCollection($post->whenLoaded('images')),
        ];
    }),
];

will results in

"Call to undefined method App\Models\Customer::whenLoaded()",, if anyone suggests a complete solution, it will be a great help, if I will able to do, it I will update here.

Upvotes: 0

Welder Louren&#231;o
Welder Louren&#231;o

Reputation: 1045

I want to load the relationships conditionally like

Lazy Eager Loading using the load() call

The Lazy Eager Loading accomplishes the same end results as with() in Laravel, however, not automatically. For example:

?include=comments

// Get all posts.
$posts = Post::without('comments')->all();

if (request('include') == 'comments')) {
    $posts->load('comments');
}

return PostResource::collection($posts);

Alternativelly, you could require the include query string to be an array:

?include[]=comments&include[]=tags

// Validate the names against a set of allowed names beforehand, so there's no error.
$posts = Post::without(request('includes'))->all();

foreach (request('includes') as $include) {
    $posts->load($include);
}

return PostResource::collection($posts);

The call without() is only required in case you defined your model to automatically eager load the relationships you want to conditionally load.

With all data filtered in Controller, just make sure to display only loaded relations in your PostResource

public function toArray($request) {
    $data = [...];

    foreach ($this->relations as $name => $relation)
    {
        $data[$name] = $relation;
    }

    return $data;
}

Upvotes: 3

Related Questions