What to do when paginator is not allowed on a collection? - php

I have two entities: Post (posts) and Tag (tags). They both are in many-to-many relationship. So I have a pivot table called PostTag (post_tag). I want to list all the tags [including a) pivot table and b) post title] which belong to those posts whose author is the logged in user. So I did something like this:
$tags = collect();
$posts = Post::where('user_id', auth()->id())->with('tags')->get();
$posts->each(function($post, $key) use ($tags){
$post->tags->each(function($tag, $key) use ($tags, $post) {
$tag->post_title = $post->title;
$tags->push($tag);
});
});
return $tags;
However, I also need to paginate the result. So I attempted to return this instead:
return $tags->paginate(10);
But paginate is not a method of Collection (Maybe of Builder)
The relationship methods are:
// Post.php
public function tags() {
return $this->belongsToMany(Tag::class)->withPivot('updated_at');
}
// Tag.php
public function posts(){
return $this->belongsToMany(Post::class);
}
I have a feeling that there must be some easier way of doing it which I may not know:
PostTag::someQueryThatFetchesThoseTagsWithPostTitle();
// If I could do something like this, paginate() would have been available

Tags::query()->where('posts.user_id', auth()->id())
->join('post_tag', 'post_tag.tag_id', '=', 'tags.id')
->join('posts', 'post_tag.post_id', '=', 'posts.id')
->selectRaw('tags.*, posts.title as post_title')
->paginate(10);
You can just optimize your query in order to return what you want selecting what you need.
This should be even faster.

You can create your own pagination with LengthAwarePaginator with this piece of code I'm using in my projects sometimes.
//Get current page form url e.g. &page=6
$currentPage = LengthAwarePaginator::resolveCurrentPage();
//Number of results in pagination
$paginate = 10;
//Slice the collection to get the items to display in current page
$currentPageSearchResults = $tags->slice(($currentPage - 1) * $paginate, $paginate)->all();
//Create our paginator and pass it to the view
$paginatedSearchResults = new LengthAwarePaginator($currentPageSearchResults, $tags->count(), $paginate);
Where $paginatedSearchResults returns pagination object.

Related

How to create a filter by category in MANY TO MANY relationship in Laravel?

I have a many to many between Article and Category model and a pivot containing category_id and article_id records. At the click on the category link I would have a page that shows me all articles related to the category clicked but i can't create the right function in the controller.
public function showcategory($id){
$articles=Article::whereHas('categories',function($query){
$query->whereIn('category_id', $id);
})->get();
return view('categorydetail',compact('articles);
}
Your code seems not fine. Eloquent whereIn is expecting an array as second parameter. Use where instead.
$articles=Article::whereHas('categories',function($query){
$query->where('category_id', $id);
})->get();
Ideally, this is not the best practice. You'd better try the flow below:
//Category.php
public function articles() {
return $this->belongsTo(Article::class);
}
//Controller
$category = Category::with('articles')->find($id);
if (empty($category)) {
//show 404 error or redirection
}
$articles = $category->articles()->get();
Your code is correct, you just need to pass the id variable
The variable is passed using the code by adding the following code use($id)
You should use where instead of whereIn
public function showcategory($id){
$articles=Article::whereHas('categories',function($query) use($id){
$query->where('category_id', $id);
})->get();
return view('categorydetail',compact('articles));
}
There is also another way
public function showcategory($id){
$category= Category::find($id);
empty($category) ? abort(404) : '';
$articles = $category->articles()->paginate(10);
return view('categorydetail',compact('articles));
}

Laravel Eloquent query recursive relationship model with pagination

I am building a store, where I have to display to the user all products in a given category and all other products that are contained in the subsequent subcategories of the currently accessed one. The categories have the N+1 problem since there can be infinite subcategories. I want to be able to filter trough these products and also to be able to paginate them.
This is my categories model:
class CatalogCategory extends Model
{
public function parent()
{
return $this->belongsTo('App/CatalogCategory','parent_id');
}
public function children()
{
return $this->hasMany($this,'parent_id')
->orderBy('order_place','ASC')
->with('children');
}
/*
* Return products, that belong just to the parent category.
*/
public function products()
{
return $this->hasMany('App\CatalogProduct','parent_id')
->where('is_active', 1)
->whereDate('active_from', '<=', Carbon::now('Europe/Sofia'))
->orderBy('created_at','DESC');
}
/*
* Return all products contained in the parent category and its children categories.
*/
public function all_products()
{
$products = $this->products;
foreach ($this->children as $child) {
$products = $products->merge($child->all_products());
}
return $products;
}
}
The all_products() method returns all of the products, that I want, but since it's a collection i'm unable to paginate or filter through it. My question is if there is a better way to retrieve the products and how to retrieve them so, that i can query them for filtering and paginate them?
You could use nested set technique to store categories.
Nested set technique allows to retrieve all descendants or ancestors for a certain node in hierarchical structures in one query.
You could try this package: https://github.com/lazychaser/laravel-nestedset. Imho it's the best implentation of nested set in laravel.
Installation and configuring will cost you 10 min.
After that you could retrieve your products something like this:
public function products($slug)
{
//first query: retrieving current category
$category = CatalogCategory
::where('slug', $slug)
->first();
//second query: retrieving all category descendants and self ids.
$categoryIds = $category
->descendants
->pluck('id')
->push($category->id);
//third query: retrieving all products.
$products = CatalogProduct
::whereIn('parent_id', $categoryIds)
->where('is_active', 1)
->whereDate('active_from', '<=', Carbon::now('Europe/Sofia'))
->orderBy('created_at', 'desc');
->paginate(50);
return view('path_to_view', compact('products', 'category'));
}

Call to undefined relationship on model laravel using scope

I'm trying to get 5 posts for each category so I did a little search and ends up here Getting n Posts per category
But I'm getting a weird Call to undefined relationship on model when using with scope but it all works fine If I don't use a scope. Here is the Category Model
//Relationship with posts
public function posts(){
return $this->hasMany('App\Post');
}
scopeNPerGroup
public function scopeNPerGroup($query, $group, $n = 10)
{
// queried table
$table = ($this->getTable());
// initialize MySQL variables inline
$query->from( \DB::raw("(SELECT #rank:=0, #group:=0) as vars, {$table}") );
// if no columns already selected, let's select *
if ( ! $query->getQuery()->columns)
{
$query->select("{$table}.*");
}
// make sure column aliases are unique
$groupAlias = 'group_'.md5(time());
$rankAlias = 'rank_'.md5(time());
// apply mysql variables
$query->addSelect(\DB::raw(
"#rank := IF(#group = {$group}, #rank+1, 1) as {$rankAlias}, #group := {$group} as {$groupAlias}"
));
// make sure first order clause is the group order
$query->getQuery()->orders = (array) $query->getQuery()->orders;
array_unshift($query->getQuery()->orders, ['column' => $group, 'direction' => 'asc']);
// prepare subquery
$subQuery = $query->toSql();
// prepare new main base Query\Builder
$newBase = $this->newQuery()
->from(\DB::raw("({$subQuery}) as {$table}"))
->mergeBindings($query->getQuery())
->where($rankAlias, '<=', $n)
->getQuery();
// replace underlying builder to get rid of previous clauses
$query->setQuery($newBase);
}
Calling Npergroup with relation
public function latestposts()
{
return $this->posts()->latest()->nPerGroup('category_id', 5);
}
Post Model Relationship
//Post belongs to Category
public function category(){
return $this->belongsTo('App\Category');
}
In my category controller I'm calling latestposts through
$categories = Category::with('latestposts')->get();
But I'm getting the error: Call to undefined relationship on model
What I want is:
Get the N number of posts per each category but I'm completely lost at this point. Any help would be appreciated
Reference:
Tweaking Eloquent relations – how to get N related models per parent ?
I am giving this answer based on your purpose that you want 5 posts per category.
So you have Category Model and Post Model.
And in Category Model you have relation with Post model like this
//Relationship with posts
public function posts(){
return $this->hasMany('App\Post');
}
And in Post Model you have relation with Category model like this
//Post belongs to Category
public function category(){
return $this->belongsTo('App\Category');
}
I show your question you have done SQL queries.
Instead of that, You can use two approaches
1) Give condition while eagar loading
$categories = Category::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc')->take(5);
}])->get();
Note: This approach will only work when you take only one result of parent child using first() method.
To get n number of posts per category Use this.
First, you can retrieve all categories with
$categories = Category::all();
Then you can use foreach loop and in all $category you have to give assign new attribute in it like here latestposts,
foreach ($categories as $category)
{
$category->latestposts = $category->posts()->orderBy('created_at','desc')->take(5)->get();
}
After this foreach loop you will get latest 5 posts in all categories.
Try this in your code and comment your queries and reviews.

Laravel - Eager load many-to-many, get one record only (not a collection)

My posts have images (many-to many since images can have other relations as well). In my pivot table I have a boolean field called 'featured' which designates the main image for that post. I want to display in the posts index page all the posts associated with the current user. I only want to get one image from the DB and that should be the featured image. Currently I can only get the featured images as a collection. The reason for this is if the user has lots of posts I don't want to go ahead and retrieve the featured image for all their posts (N+1) but rather using eager loading get the featured imaged with only 2 queries.
\\Post Model
public function images() {
return $this->belongsToMany(Image::class);
}
public function image(){
return $this->images()->where('featured', '=', true)->first();
}
public function featured_image(){
return $this->images()->where('featured', '=', true);
}
\\Controller
$user = Auth::user();
$posts = $user->posts()->with('image')->get();
// throws error
//Call to undefined method Illuminate\Database\Query\Builder::addEagerConstraints()
// if I do this
$posts = $user->posts()->with('featured_image')->get();
// I get all the user's posts, I get the featured image but as a collection even if I only have one record there
How can I do this?
I think this is probably the solution you want:
\\Post Model
public function images() {
return $this->belongsToMany(Image::class);
}
public function getFeaturedImageAttribute() {
return $this->images->where('featured', true)->first();
}
\\Controller
$user = Auth::user();
$posts = $user->posts()->with('images')->get();
In the resulting collection of posts, each post will have a 'featured_image' attribute that can be accessed like this:
foreach ( $posts as $post )
{
$featured_image = $post->featured_image;
// do something with the image...
}
IMPORTANT: Because the accessor method uses '$this->images' instead of '$this->images()', it will run using the eager loaded 'images' Collection's where() and first() methods instead of the query builder. This results in a decent chunk of PHP processing but no new queries.
If you are limited to using only two queries, you can use the following code to achieve your goal:
$posts = $user->posts;
$idOfPosts = $posts->pluck('id');
$featuredImages = Image::whereIn('post_id', $idOfPosts)->where('featured', true)->get();
enter code here
While this solution is not an Eager Loading approach, it does resolve the N+1 query problem.

How to list out all items in a nested table in Laravel

I am using Laravel's Eloquent ORM and I'm having trouble eager loading items for display.
Here is the scenario:
Users follow Blogs
Blogs have Posts
I have a database table named Relationships, this table is used to store the User ID and the Blog ID to show which User is following which Blog. I have a table for Blogs describing the Blog and I have a table for Posts. The Relationships table would be my pivot table to connect the Users with the Blogs tables together. Now, I need to list out all the posts from all the Blogs the User follows in a list.
Here is my User model:
public function following() {
return $this->has_many_and_belongs_to('Blog', 'relationships', 'user_id', 'blog_id');
}
Here is my Blog model:
public function followers() {
return $this->has_many_and_belongs_to('User', 'relationships', 'blog_id', 'user_id');
}
public function posts() {
return $this->has_many('Post');
}
This is how I am trying to retrieve the posts in a list:
$posts = User::with(array('following', 'following.posts'))
->find($user->id)
->following()
->take($count)
->get();
This code only lists out the actual Blogs, I need their Posts.
Thank you for your help, please let me know if you need any more details.
SOLUTION:
I slightly modified the accepted answer below, I decided to use the JOIN to reduce the amount of SQL calls to simply 1 call. Here it is:
$posts = Post::join('blogs', 'posts.blog_id', '=', 'blogs.id')
->join('relationships', 'blogs.id', '=', 'relationships.blog_id')
->select('posts.*')
->where('relationships.user_id', '=', $user->id)
->order_by('posts.id', 'desc')
->take($count)
->get();
This is not achievable by native Eloquent methods. But you can use a bit of Fluent methods to join those tables. For instance:
Edit here: I've added the eager loading to Post query.
$user = User::find(1);
$posts = Post::with('blog') // Eager loads the blog this post belongs to
->join('blogs', 'blogs.id', '=', 'posts.blog_id')
->join('relationships', 'relationships.blog_id', '=', 'blogs.id')
->where('relationships.user_id', '=', $user->id)
->order_by('posts.id', 'desc') // Latest post first.
->limit(10) // Gets last 10 posts
->get('posts.*');
foreach ($posts as $post) {
print($post->title);
}
If you also need a list of all blogs that such user is following to show on a sidebar, for instance. You can DYI instead of relying on Eloquent, which should be faster and more customizable. For instance:
$user = User::with('following')->find(1);
// This creates a dictionary for faster performance further ahead
$dictionary = array();
foreach ($user->following as $blog) {
$dictionary[$blog->id] = $blog;
}
// Retrieves latest 10 posts from these blogs that he follows
// Obs: Notice the array_keys here
$posts = Post::where_in('blog_id', array_keys($blog_ids))
->order_by('posts.id', 'desc')
->limit(10)
->get();
// Hydrates all posts with their owning blogs.
// This avoids loading the blogs twice and has no effect
// on database records. It's just a helper for views.
foreach ($posts as $post) {
$post->relationships['blog'] = $dictionary[$post->blog_id];
}
On view:
foreach ($user->following as $blog) {
print($blog->title);
}
foreach ($posts as $post) {
print($post->title . ' #'. $post->blog->title);
}

Categories