Call to undefined relationship on model laravel using scope - php

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.

Related

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'));
}

Eloquent Laravel Fetch Data from multiple tables

I've spent two days trying to solve this but I can't figure how.
I have five tables
Product
Category
Category_Product
Order
Order_Product
From the view,clicking on a category button I have to fetch all his ordered products with the relative category.
I have the current models:
Product Model
class Product extends Model
{
public function categories() {
return $this->belongsToMany('App\Category');
}
public function orders() {
return $this->belongsTo('App\Order');
}
}
Category Model
public function products() {
return $this->belongsToMany('App\Product');
}
Order Model
public function products() {
return $this->belongsToMany('App\Product');
}
Now the problem is that I can't figure how to fetch the data from the current tables.When I press a button I'm able to fetch the category from the Product Table,but I want to fetch from the Ordered_Products. I really can't figure how.
With this I'm able to fetch all the categories from Product
if (request()->category) {
$products = Product::with('categories')->whereHas('categories', function ($query) {
$query->where('slug', request()->category);
})->get();
}
With this instead,I'm able to fetch the ordered products.
$products = DB::table('order_product')
->join('products', 'order_product.product_id','=', 'products.id')
->where('order_product.user_id','=',$user_id)
->get();
For the latter, there's a better way to do it, that's for sure. I'm sorry if it's a dumb question but I'm rather new with this framework. I am using Laravel 7.2.
Basically Eloquent Model doesn't encourage joining tables to retrieve data. It should be joined only for filtering results (So you need to drop field of other table using ->select('original_table.*'))
In this case, you should simply retrieve categories at first. Then retrieve related data using relation property accessing.
e.g.
$categories = Category::query()
->with('products')
->where('slug', request('category'))
->get();
$products = $categories->flatMap->products;
$pivots = $products->map->pivot;
Solved using whereHas two times:
$products = Product::with('categories')->whereHas('categories',function($query){
$query->where('slug',request()->category);
})->whereHas('orders',function($query){
$query->where('orders.user_id',Auth::id());
})->get();

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

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.

Laravel eloquent limit results

I have a working query:
Object::all()->with(['reviews' => function ($query) {
$query->where('approved', 1);
}])
And I want to limit the number of reviews, per object, that is returned. If I use:
Object::all()->with(['reviews' => function ($query) {
$query->where('approved', 1)->take(1);
}])
or
Object::all()->with(['reviews' => function ($query) {
$query->where('approved', 1)->limit(1);
}])
it limits the total number of reviews, where I want to limit the reviews that are returned by each object. How can I achieve this?
Eloquent way
Make one relationship like below in your model class
public function reviews() {
return $this->hasMany( 'Reviews' );
}
//you can pass parameters to make limit dynamic
public function firstReviews() {
return $this->reviews()->limit( 3 );
}
Then call
Object::with('firstReviews')->get();
Faster way(if you just need one review)
Make a derived table to get the latest review 1st and then join it.
Object->select('*')
->leftJoin(DB::raw('(SELECT object_id, reviews FROM reviews WHERE approved=1 ORDER BY id DESC limit 0,1
as TMP'), function ($join) {
$join->on ( 'TMP.object_id', '=', 'object.id' );
})
->get();
Grabbing 1 child per parent
You can create a helper relation to handle this very easily...
In your Object model
public function approvedReview()
{
return $this->hasOne(Review::class)->where('approved', 1);
}
Then you just use that instead of your other relation.
Object::with('approvedReview')->get();
Grabbing n children per parent
If you need more than 1, things start to become quite a bit more complex. I'm adapting the code found at https://softonsofa.com/tweaking-eloquent-relations-how-to-get-n-related-models-per-parent/ for this question and using it in a trait as opposed to a BaseModel.
I created a new folder app/Traits and added a new file to this folder NPerGroup.php
namespace App\Traits;
use DB;
trait NPerGroup
{
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);
}
}
In your Object model, import the trait use App\Traits\NPerGroup; and don't forget to add use NPerGroup right under your class declaration.
Now you'd want to setup a relationship function to use the trait.
public function latestReviews()
{
return $this->hasMany(Review::class)->latest()->nPerGroup('object_id', 3);
}
Now you can use it just like any other relationship and it will load up the 3 latest reviews for each object.
Object::with('latestReviews')->get();

laravel 5.1 getting related 5 news of each category in many-to-many relation

I got stuck here been trying from 2-3 hours.
I have a many to many relation:
class Category extends Model
{
public function news()
{
return $this->belongsToMany('App\News');
}
}
class News extends Model
{
public function categories()
{
return $this->belongsToMany('App\Category');
}
}
I am trying to get latest 5 news of the related categories:
$front_categories = Category::with(array(
'news'=>function($query){
$query->where('publish','1')->orderBy('created_at', 'desc')->take(5);}))
->where('in_front', 1)->get();
The above query is not working for me it give a total of five results instead of 5 result for each categories.
Based on what I know about Laravel, you could try doing it this way instead.
class Category {
public function recentNews()
{
return $this->news()->orderBy('created_by', 'DESC')
->take(5);
}
}
// Get your categories
$front_categories = Category::where('in_front', 1)->get();
// load the recent news for each category, this will be lazy loaded
// inside any loop that it's used in.
foreach ($front_categories as $category) {
$category->recentNews;
}
This has the same effect as Lê Trần Tiến Trung's answer and results in multiple queries. It also depends on if you're reusing this functionality or not. If it is a one-off, it may be better to put this somewhere else. Other ways could also be more dynamic, such as creating a method that returns the collection of categories and you can ask it for a certain number:
class CategoriesRepository {
public static function getFrontCategories(array $opts = []) {
$categories = Category::where('in_front', 1)->get();
if (!empty($opts) && isset($opts['withNewsCount']))
{
foreach ($categories as $category)
{
$category->recentNews = static::getRecentNewsForCategory(
$category->id,
$opts['withNewsCount']
);
}
}
return $categories;
}
}
$front_categories = CategoriesRepository::getFrontCategories([
'withNewsCount' => 5
]);
I think, Because you do eager loading a collection which has more than one record.
To solve it, you need to loop
$front_categories = Category::where('in_front', 1)->get();
foreach ($front_categories as $fCategory) {
$fCategory->load(['news' => function($query) {
$query->where('publish','1')->orderBy('created_at', 'desc')->take(5);
}]);
}
This solution will do many queries to DB. If you want to do with only 1 query, checkout this Using LIMIT within GROUP BY to get N results per group?

Categories