FabLPA's avatar

Eloquent Resource collection with relations

Hi All,

So... I'm stuck on how to achieve the same system as here : https://bibliotheque.braille.be/fr/catalogue

What to achieve :

I'm building an API to retrieve books.

As on the example, I have to do a "search" engine which return a paginated book resource. The search engine is already done and work well but I'm stuck on how to do for the left column.

How, on this basis

$books = Book::where('title', 'rings'); // the search is into a service provider and...is a little bit more hard

return BookCollection($books->paginate(12));

could I get, a list of authors and the count of this author's book into the search result.

With the paginate() I can't have the whole authors for the whole book's search results...

I try to add something like :

$books = Book::where('title', 'rings');

$authors = [];
foreach($books->get() as $book){
    $author = $book->author //=> the relationship exists
    $authors[] = new AuthorCollection($author);
}

$meta = [
    'meta' => [
        'authors' => $authors
    ]
];

return (BookCollection($books->paginate(12)))->additional($meta);

but it takes so many times to achieve the request that I get a timeout... and it's only for the authors...I have to do that for "catalogs" / "categories" / "series" etc.

Any clue or suggestion will be welcome !

0 likes
7 replies
Sinnbeck's avatar

You are loading each author one at the time from the database. You need to eager load them first using with (with loads the authors once and adds them to the books)

Also I would recommend using your Book Resource to organize them as it has access to it all

$books = Book::with('author')->where('title', 'rings'); // the search is into a service provider and...is a little bit more hard

return BookCollection($books->paginate(12));
FabLPA's avatar

@sinnbeck like this, I'll have only the authors for the paginate() results (here 12), not for the whole results (let say that the result will return thousands of books).

All needed relationship are already eager loaded and used through Book Resource when needed (to display book name / author on search result's pages).

But through this, I can't have all authors of the whole search result, just get the 12 authors for the 12 books of the search result's page (pagination). Am I wrong ? Have i missed something with API Resources ?

Sinnbeck's avatar

I don't see where you eagerload? This $book->author does not eager load. It gets the item from the database one at the time. If you had to do it like that you will still need with

foreach($books->with('author')->get() as $book){
    $author = $book->author //=> the relationship exists
    $authors[] = new AuthorCollection($author);
}

But still loading authors for thousands of books seems like a bad idea. Maybe I just don't understand why your api needs all authors for all requests

FabLPA's avatar

@sinnbeck it’s just a part of my code, I’ve leaved the office, I’ll copy/paste the whole code tomorrow morning. Perhaps you’ll have a better view of where I’m stuck.

This said, have you take a look to the link I gave? How would you do to have the same kind of results (left and right column) through an API with API Resources ?

Thanks for your help ;)

FabLPA's avatar

As said yesterday, I'll copy/paste the necessary parts of my code here for a better comprehension of my problem.

My mean issue is how to get the same stuff as the left column of the website I gave as example (https://bibliotheque.braille.be/fr/catalogue).

In my Eloquent Book Model, I have those relationships :

    public function catalogs()
    {
        return $this->belongsToMany(Catalog::class)
            ->using(CatalogFrontendBook::class)
            ->withPivot('frontend_book_id', 'catalog_id', 'nbr_of_copies', 'new', 'newed_at', 'length')
            ->withTimestamps();
    }

    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    public function author()
    {
        return $this->belongsTo(Author::class);
    }

    public function serie()
    {
        return $this->belongsTo(Serie::class);
    }

The search Method in my BookController :

    /**
     * Search Books
     *
     * @param BookServices $bookServices
     * @param BookSearch   $request
     *
     * @return mixed
     */
    public function search(BookServices $bookServices, BookSearch $request)
    {
        $response = $bookServices->search($request, false);
        
        // How to get those relations based on the whole results, not on the 12 results returned by the pagination,
        // without running a foreach on $response->get() to crawl all results 
    // perhaps have I to do a raw query to get all joined tables ?
        $catalogs = []; 
        $categories = [];
        $authors = [];
        $series = [];

        $meta = [
            'meta' => [
                'catalogs' => $catalogs,
                'categories' => $categories,
                'authors' => $authors,
                'series' => $series,
            ]
        ];

        return (BookForListResource::collection($response->paginate($request->perPage ? $request->perPage : 12)))->additional($meta);
    }

The Search method in the Book service :


    /**
     * Run Search for a book
     * 
     * @param      $request
     * @param bool $auth
     *
     * @return mixed
     */
    public function search($request, $auth = false)
    {
        $catalog = $request->catalog;
        $category = $request->category;
        $field = $request->field;
        $term = $request->term;

        // termSearch - catalogSearch - categorySearch are scopes in Book models
    // here is the eager load of all required relations
        $books = FrontendBook::with('author', 'serie', 'catalogs', 'category', 'subcategory')
                             ->termSearch($term, $field)
                             ->catalogSearch($catalog)
                             ->categorySearch($category);

        return $books;
    }

And the Resource "BookForListResource" (Json\JsonResource) :

    public function toArray($request)
    {
        $defaultCover = Storage::disk('s3')->url('covers/default.jpg');

        $inFavoris = $inLoans = $inWishlist = $hasCommented = false;
        if(auth('api')->check()){
            $inLoans = $this->loans()->where('member_id', auth('api')->id())->count() > 0 ? $this->loans()->where('member_id', auth('api')->id())->first() : false;
        }

        return [
            'id' => $this->id,
            'title' => $this->title,
            'author' => $this->author ? $this->author->complete_name : false,
            'resume' => $this->resume,
            'cover' => $this->cover == '' ? $defaultCover : $this->cover,
            'length' => $this->whenPivotLoaded('catalogs', function () {
                return $this->pivot->length;
            }),
            'href' => $this->unique_key,
            $this->mergeWhen(auth('api')->check(), [
                'in_loans' => $inLoans ? true : false,
            ]),
            'categories' => $this->categories
        ];
    }

Is quite hard to explain it in English... Hope it'll be easier to understand what I want to achieve and where I'm stuck. @sinnbeck

Sinnbeck's avatar
Sinnbeck
Best Answer
Level 102

Well lets start with the left things then (I assume the numbers for each category). If you need to count anything, make sure to just setup a query and run count on it directly. Never get everything from the database and try to work with it.

Let us say we want number of books in a category. I would then assume that you have a category table and model.

This can be done in a loop if needed.

$categories = Category::all();
$category_id = 33;
$category_counts[$category_id]['count'] = Book::where('category_id', $category_id)->count();
$category_counts[$category_id]['title'] = $categories->firstWhere('id', $category_id)->name;
FabLPA's avatar

Damn f*£%#$ language barrier...

So, it was not the solution I need to achieve my goal, but you lead me to the solution ;)

Doing hundreds call to the BookServices' Search method take the same time than only one... So I could refine the search and get what I need.

    /**
     * Search Books
     *
     * @param BookServices $bookServices
     * @param BookSearch   $request
     *
     * @return mixed
     */
    public function search(BookServices $bookServices, BookSearch $request)
    {
        $response = $bookServices->search($request, false);

        // Get Categories figuring in the Search results
        $forCategories = $categories = [];
        $i = 0;
        foreach(Category::whereHas('frontendBooks')->get() as $category){
            $forCategories[$i] = $bookServices->search($request, false);
            if($forCategories[$i]->where('category_id', $category->id)->count() > 0) {
                $categories[$category->id] = $category;
                $i++;
            }
        }
        $categories = CategoryResource::collection($categories);

        // Get Catalogs figuring in the Search results
        $forCatalogs = $catalogs = [];
        $i = 0;
        foreach(Catalog::get() as $catalog){
            $forCatalogs[$i] = $bookServices->search($request, false);
            $id = $catalog->id;
            if($forCatalogs[$i]->whereHas('catalogs', function ($query) use($id) {
                    return $query->where('catalogs.id', $id);
                })->count() > 0) {
                $catalogs[$id] = $catalog;
                $i++;
            }
        }
        $catalogs = CatalogResource::collection($catalogs);

        // Get stats for authors / titles / resume figuring in the Search results
        $authors = $titles = $resumes = null;
        if(isset($request->term)) {
            $authors = 0;
            $foo = [];
            foreach($bookServices->search($request, false)->select('author_id')->getResultsInAuthors($request->term)->distinct()->get() as $authorBook){
                if(!in_array($authorBook->author, $foo)){
                    $foo[] = $authorBook->author;
                    $authors++;
                }
            }
            $resumes = $bookServices->search($request, false)->select('resume')->getResultsInResumes($request->term)->distinct()->count();
            $titles = $bookServices->search($request, false)->select('title')->getResultsInTitles($request->term)->distinct()->count();
        }

        $meta = [
            'meta' => [
                'catalogs' => $catalogs,
                'categories' => $categories,
                'authors' => $authors,
                'titles' => $titles,
                'resumes' => $resumes,
            ]
        ];

        return (BookForListResource::collection($response->paginate($request->perPage ? $request->perPage : 12)))->additional($meta);
    }

I don't think it's the proper way or even a clean way to do, but it works... and after days on this, it's time to let it like this

Please or to participate in this conversation.