Constraining eager loaded relationship - laravel

I found a very bizarre behavior of with function for overloading relationships. I have Product and Deal relationships, such that Product belongsTo() Deal (through product_id in deals table). Now, when I try to get all products on sale:
Product::with(['deal' => function($query) {
$query->whereDate('ends_at', '>', Carbon::now()->toDateTimeString());
}])->get()
this returns a collection of all products, even though there are no records in deals table and all products have deal_id set to NULL. At the same time Product::has('deal')->get() returns an empty collection, as you would expect.
I initially discovered this problem while trying to fetch five random products on sale together with Deal and Image relationships:
Product::with(['deal' => function ($query) {
$query->whereDate('ends_at', '>', // promo still active
Carbon::now()->toDateTimeString());
},
'images' => function ($query) {
$query->where('featured', true); // image featured on homepage
}])
->where('status', 'IN_STOCK') // 'In Stock'
->whereNull('deleted_at') // wasn't soft-deleted
->orderByRaw('RAND()')
->take(5)->get())
This yields a collection with 5 random Products out of all Products. I tried with query->whereNotNull('ends_at')->whereDate('ends_at' ..... ); but got same results.
What am I doing wrong here?

Your understanding of the concept is completely wrong here.
If you are saying that a Product belongsTo() Deal, then lets assume that a Deal hasMany() Products.
This is the deals table
deals
id | name | ends_at | blah | blah
products
id | deal_id | name | blah | blah
So basically, the Product::with('deal') should return you all products with their Deals being Eager loaded. But Deal::with('products') will return you an empty collection, since no products have a valid deal_id in it.
It is important to note that, since Product can only belongTo a single Deal, you will always get the Deal Model rather than a collection when you perform Product::with('deal') query. But when you perform Deal::with('products') you are bound to get a collection.
So basically, when you say
This returns a collection of all products, even though there are no records in deals table and all products have deal_id set to NULL.
It is pretty obvious.... because the query here is being done on Products and not Deal. If you are trying to find the Deal where ends_at > Carbon::now(), you'll have to do this.
Deal::with('product')->where('ends_at', '>', Carbon::now()->toDateTimeString())

When you use with then it only eager loads the relations on the constraints provided but if you want to filter the parent model by their relations then whereHas is your friend. So your query should be as:
Product::whereHas('deal' => function($query) {
$query->whereDate('ends_at', '>', Carbon::now()->toDateTimeString());
})->get();
Now it will fetch only those Product which satisfy the given constraint.
You can also use the combination of with and whereHas as:
Product::whereHas('deal' => function($query) {
$query->whereDate('ends_at', '>', Carbon::now()->toDateTimeString());
})
->with(['deal' => function($query) {
$query->whereDate('ends_at', '>', Carbon::now()->toDateTimeString());
}])
->get();

Related

Laravel Eloquent with() selecting specific column doesn't return results

Say I have 2 models, Category and POI where 1 Category can have many POIs.
$categoryDetails = Category::with([
'pois' => function ($query) {
$query->where('is_poi_enabled', true);
},
])->findOrFail($id);
The above query returns results from the specific Category as well as its POIs.
However, with the query below:
$query->select('id', 'name')->where('is_poi_enabled', true);
The POIs become empty in the collection.
Any idea why this is happening? When added a select clause to the Eloquent ORM?
While doing a select it's required to fetch the Relationship local or Primary key.
For an example POIs table contains category_id then it's required to select it
Try this:
$categoryDetails = Category::with([
'pois' => function ($query) {
$query->select(['id', 'category_id', 'is_poi_enabled'])
->where('is_poi_enabled', true);
},
])->findOrFail($id);
Good luck!

OneToMany + ManyToMany

I have 3 main tables (sellers, stores, products), and there is another table for relation between stores and products (store_product)
A seller has many stores (One to Many relationship)
A store has many products, but any of those products can be assigned to multiple stores, maybe in another seller's stores (Many To Many relationship)
Now, I have a confusion, I want to get all products for a specific seller.
If you defined the reserve of the relationships, you can do:
// your seller's id
$seller_id = 1;
// get your products
$products = Product::whereHas('stores.seller', function ($query) use ($seller_id) {
$query->where('id', $seller_id);
})->get();
Update
To get the count of products under every seller, you could use the withCount() method, just like this:
$sellers = Seller::withCount(['stores' => function ($query){
$query->withCount('products');
})->get();
which will place a {relation}_count column inside the stores relationship of your resulting models. In this case, products_count:
foreach ($sellers as $seller) {
echo $seller->stores->first()->products_count;
}
What you need is the builder function whereHas('relation', $callback). With it your query is very straight forward:
$products = Product::query()
->whereHas('stores.seller', function ($query) use ($sellerId) {
$query->where('sellers.id', $sellerId);
})
->get();
Apparently using sellers.id (where sellers is the table name) is important because you most likely have a column called id on all three tables. If you omit the table name, the query will fail.

How to order by eager loaded relationship in Laravel 5.5?

The code below works perfectly and displays all products with discounts in JSON Format for my API. But I want the result ordered by id in the discounts table. Something like orderBy('discount.id', 'desc'). Can anyone provide a solution for this? How it is possible to use orderBy with id column in discount table using has()?
public function promotions()
{
return $this->prepareResult(true, Product::has('discount')->where([
'company_id' => 1, 'is_active' => 1
])->with('category')->with('discount')->get(), [], "All Promotions");
}
You can use a function within your with statement:
public function promotions()
{
return $this->prepareResult(true, Product::has('discount')
->where(['company_id'=> 1, 'is_active'=>1])
->with('category')
->with(['discount' => function ($query) {
return $query->orderBy('id','DESC');
}])->get(), [],"All Promotions");
}
You can read about this here in the documentation.
If you want to go the collection method route instead of Alex's answer (which is also valid), you can just continue to chain collection methods after get. Since you included the with(), you can do
->get()->sortBy('discount.id')
https://laravel.com/docs/5.6/collections#method-sortby
Not related to your question, but wanted to point out that you can pass multiple arguments to with() so that you don't call it twice.
Product::has('discount')->where(['company_id'=> 1, 'is_active'=>1])->with('discount', 'category')->get()
As DevK mentioned, you need to do a join and then you can sort your products by that. The trick here is that you join the discounts table to your products table, but only select the id column (named as discount_id) from your discounts table so you can sort by those records.
In the below example, I assumed that your
Category model's table is categories
Product model's table is products
Discount model's table is discounts
products table references a discount.id as discount_id
This code will return every column from the products table plus a discount_id column, which you can ignore, it's only there for the sorting. It will also keep it as a collection with the relationships that you stated above.
public function promotions()
{
return $this->prepareResult(
true,
Product::has('discount')
->where(['company_id' => 1, 'is_active' => 1])
->with('category')
->with('discount')
->selectRaw('products.*, discounts.id as discount_id')
->join('discount', 'products.discount_id', '=', 'discounts.id')
->orderBy('discount_id', 'DESC')
->get(),
[],
"All Promotions"
);
}

Eloquent filter on pivot table created_at

I want to filter the contents of two tables which have an Eloquent belongsToMany() to each other based on the created_at column in the pivot table that joins them. Based on this SO question I came up with the following:
$data = ModelA::with(['ModelB' => function ($q) {
$q->wherePivot('test', '=', 1);
}])->get();
Here I'm using a simple test column to check if it's working, this should be 'created_at'.
What happens though is that I get all the instances of ModelA with the ModelB information if it fits the criteria in the wherePivot(). This makes sense because it's exactly what I'm telling it to do.
My question is how do I limit the results returned based on only the single column in the pivot table? Specifically, I want to get all instances of ModelA and ModelB that were linked after a specific date.
OK, here it goes, since the other answer is still wrong.
First off, wherePivot won't work in whereHas closure. It's BelongsToManys method and works only on the relation object (so it works when eager loading).
$data = ModelA::with(['relation' => function ($q) use ($someDate) {
$q->wherePivot('created_at', '>', $someDate);
// or
// $q->where('pivot_table.created_at', '>', $someDate);
// or if the relation defines withPivot('created_at')
// $q->where('pivot_created_at', '>', $someDate);
}])->whereHas('ModelB', function ($q) use ($someDate) {
// wherePivot won't work here, so:
$q->where('pivot_table.created_at', '>', $someDate);
})->get();
You are using Eager Loading Constraints, which constrain only, like you said, the results of the related table.
What you want to use is whereHas:
$data = ModelA::whereHas('ModelB' => function ($q) {
$q->wherePivot('test', '=', 1);
})->get();
Be aware that ModelB here refers to the name of the relationship.

How to order by another table join by eager loading in Laravel

I got product and stocks table;
products
id int
name varchar
created_at timestamp
stocks
id int
name varchar
product_id varchar
created_at timestamp
Product Model
public function validStock() {
return $this->hasMany('Stock')->where('quantity', '>', 10);
}
If both have created_at, how to order by stocks's created_at, I've tried two methods and it's not work
Product::with('validStock')->orderBy('validStock.created_at', 'DESC');
Product::with(array('validStock' => function($q) {
$q->orderBy('created_at', 'DESC');
}));
Instead of sorting after retrieving all data (which is impossible to do in an efficient way when paginating results) you can use a join.
(This answer is based on this one.)
Product::with('validStock')
->join('stocks', 'stocks.product_id', '=', 'products.id')
->select('products.*') // Avoid selecting everything from the stocks table
->orderBy('stocks.created_at', 'DESC')
->get();
The only thing I don't like about this is that it takes away some of the database abstraction, in that you have to write your table names here.
Note that I haven't tried this with a hasMany relationship in this direction, as you have it in your example (selecting products, and each product has many stocks). I've tried only with the hasMany in the other direction (eg selecting stocks, each of which has exactly one product).
You can not apply an order while querying an eagerly loaded relationship in Laravel. You can order the Collections after the query has been performed.
$products = Product::with(array('validStock'))
->get()
->each(function ($product)
{
$product->validStock = $product->validStock
->sortBy(function ($validStock)
{
return $validStock->created_at;
})
->reverse();
});
You should return $q in the closure:
Product::with(array('validStock' => function($q) {
return $q->orderBy('created_at', 'DESC');
}));

Resources