2

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;
    }
Prafulla Kumar Sahu
  • 9,321
  • 11
  • 68
  • 105
  • Have a look at this package - it should fit perfectly for your needs: https://github.com/spatie/laravel-query-builder – jtwes Aug 13 '19 at 13:10
  • @jtwes thank you for your response, already looked that, it seems to be for query builder and I am using eloquent resource, so I would like to make it work with out that package, any way thank you. – Prafulla Kumar Sahu Aug 13 '19 at 13:12

3 Answers3

3

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;
}
Welder Lourenço
  • 994
  • 9
  • 12
  • This will behave similar to my work around and need to implement in every resource, I want to create something generic. – Prafulla Kumar Sahu Aug 13 '19 at 23:39
  • How about creating a parent class to extend in PostResource, like `PostResource extends InjectRelationResource` ? The logic would inject any loaded relationship to the data output, this what i can think now – Welder Lourenço Aug 14 '19 at 00:25
  • Maybe that will work fine, but we also need to consider specifying attributes and also it should allow pagination. Not having a clear idea, but seems that may be a good solution. – Prafulla Kumar Sahu Aug 14 '19 at 00:27
  • It still ain't that clear to me what you are trying to achieve, apart from relations, do you want include the resource fields conditionally too? – Welder Lourenço Aug 14 '19 at 00:46
  • Just what normal resource and resource collection offers, specifying what you want to return, relationships and pagination, but we can focus on relationships for now. – Prafulla Kumar Sahu Aug 14 '19 at 00:48
  • not conditionally, but want to exclude `created_at`, `updated_at` and certain fields from resource. – Prafulla Kumar Sahu Aug 14 '19 at 02:32
  • I c, by setting up resources you actually achieve that individually, however, how about setting up the `$hidden` property in the model? – Welder Lourenço Aug 14 '19 at 12:32
  • For resource, I may not need but inside app, at some point I may need that properties, so not out putting for that. anyway formatting data is ResourceCollection's job, so I would like to achieve in this level. – Prafulla Kumar Sahu Aug 14 '19 at 12:40
  • Please, clarify to me what's the issue then... :) – Welder Lourenço Aug 14 '19 at 12:57
  • Have you looked in to the answer I have provided? you can see, it is working for resource and not for collection, this is current status, the question is simple, I want the resource to include relationships optionally based on query parameters, with out breaking any of it's default properties/abilities, neither making it overworking . – Prafulla Kumar Sahu Aug 14 '19 at 13:00
1

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));
            } 
Eva
  • 133
  • 1
  • 14
  • I have resource, let me try your code, but `comments` can be something else like images and can be `comments` and `images`, so I can not make it static. – Prafulla Kumar Sahu Aug 13 '19 at 13:28
  • Your code needs a little bit correction ` $filter = $request->query->get('include', ''); if($filter){ $data['comments'] = $this->resource->comments; }` and it will work, but need to make it dynamic and would like to make a generic solution, it will only work for a particular resource. – Prafulla Kumar Sahu Aug 13 '19 at 13:30
  • dynamic will be ` $filter = $request->query->get('include', ''); if($filter){ $data[$filter] = $this->resource->$filter; }` only for post, do not want to add this filter in every resouse, as job of resource is formatting the data not filtering. – Prafulla Kumar Sahu Aug 13 '19 at 13:32
  • @PrafullaKumarSahu, yes, sorry, i was confused by the title of the issue, that is why I used fiter: try this:$includes = collect(explode(',', $request->query->get('includes', ''))); – Eva Aug 13 '19 at 14:01
  • Just tried, not seems to be working, getting only post resource. – Prafulla Kumar Sahu Aug 13 '19 at 15:12
  • I have added a partial solution, can you please look into it, may be you can suggest something. – Prafulla Kumar Sahu Aug 14 '19 at 05:47
  • @PrafullaKumarSahu , I have updated my response I hope it helps – Eva Aug 14 '19 at 06:39
  • no it does not seems to be working, I tried, but it did not work, it is not including relationships in collection. – Prafulla Kumar Sahu Aug 14 '19 at 07:12
  • have you tried with $this->resource->whenLoaded() ? – Eva Aug 14 '19 at 07:16
  • Check my answer, it is working for resource with whenLoaded, but not working for collection. – Prafulla Kumar Sahu Aug 14 '19 at 07:20
0

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.

Prafulla Kumar Sahu
  • 9,321
  • 11
  • 68
  • 105