Laravel whereHas manyToMany exact match - php

I have a Model with manyToMany relationship:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public function categories()
{
return $this->belongsToMany(Category::class, 'product_category', 'product_id', 'category_id');
}
}
So 1 Product can have multiple Categories.
I'm trying to query (fetch) all Products that have exact match of categories:
Example:
$categories = ['category1', 'category2'];
Product::whereHas('categories', function($q) use ($categories){
foreach ($categories as $categoryName) {
$q->where('name', $categoryName);
}
//or $q->whereIn('name', $categories);
})->get();
Also tried:
$categories = ['category1', 'category2'];
Product::whereHas('categories', function($q) use ($categories){
$q->whereIn('name', $categories);
}, '=', count($categories))->get();
Assume my Product has only category1 attached, the query should return this product.
If Product has both categories, but my array contains only category1, then this Product should be ignored.
So I'm trying to achieve: Fetch only products with specific categories. It is doable with Eloquent or DB builder?
Pseudo Code
$allow = for each Product->categories{
category Exist in $categories
}

Based on your requirement your MySQL query would be as follow:
Use LEFT JOIN to get total numbers of mapped categories for a product.
Use GROUP BY on a product to compare its total mapped categories vs matching categories based on user input.
Above comparison can be done using HAVING clause, where total mapped categories should be equal to the count of categories the user has provided. Same matching categories based on user input should also match the exact count of categories which the user has provided.
SELECT p.id
FROM products p
LEFT JOIN product_category pc ON p.id = pc.product_id
LEFT JOIN categories c ON c.id = pc.category_id AND c.name IN ('category1', 'category2')
GROUP BY p.id
HAVING COUNT(0) = 2 -- To match that product should have only two categories
AND SUM(IF(c.name IN ('category1', 'category2'), 1, 0)) = 2; -- To match that product have only those two categories which user has provided.
Same can be achieved by query builder in the following manner:
$arrCategory = ['category1', 'category2'];
$strCategory = "'" . implode("','", $arrCategory) . "'";
$intLength = count($arrCategory);
$products = Product::leftJOin('product_category', 'products.id', '=', 'product_category.product_id')
->leftJoin('categories', function ($join) use ($arrCategory) {
$join->on('categories.id', '=', 'product_category.category_id')
->whereIn('categories.name', $arrCategory);
})
->groupBy('products.id')
->havingRaw("COUNT(0) = $intLength AND SUM(IF(categories.name IN ($strCategory), 1, 0)) = $intLength")
->select(['products.id'])
->get();
dd($products);
Note: If you have only_full_group_by mode enabled in MySQL, then include columns of products table in both group by and select clause which you want to fetch.

Try using this without wherehas
Product::with(array('categories' => function($q) use ($categories){
$query->whereIn('name', $categories);
}))->get();

Thank you for your answers I solved this with: whereExists of sub select fromSub and array_agg, operator <# (is contained by):
$categories = ['category1', 'category2'];
Product::whereExists(function ($q) use ($categories){
$q->fromSub(function ($query) {
$query->selectRaw("array_agg(categories.name) as categoriesArr")
->from('categories')
->join('product_category', 'categories.id', '=', 'product_category.category_id')
->whereRaw('products.id = product_category.product_id');
}, 'foo')->whereRaw("categoriesArr <# string_to_array(?, ',')::varchar[]", [
implode(",", $categories)
]);
})->get();

Related

Laravel multiple select filter for categories, only get one select as output

In laravel 8, I am filtering blog articles by category. However when I select multiple categories in my select menu. I do get the proper request . for example: articles/category/?category_ids=3,4
But it will only output one selected filter. If I select 2 filters it just selects that next filter as if I only selected that one. (I also use Axios but the request is done proper, so its in my Controller)
Here is my code I tried:
$data['articles'] = Article::whereHas('categories', function ($query) use($category_ids){
$query->whereHas('category_id', '=', $category_ids)->where('premium',0);
;})->get();
I also tried:
$data['articles'] = Article::whereHas('categories', function ($query) use($category_ids){
$query->whereIn('category_id', [$category_ids])->where('premium',0);
;})->get();
So how do I get to query both or more category id's ?
I am using a pivot table:
Articles can have many Categories
Categories can have many Articles
I use article_category as a pivot table
When checking for relationship existence in many-to-many relations, the check is still to be done against the id in the categories table.
Try this
$category_ids = collect(explode(',', $request->category_ids))
->map(fn($i) => trim($i))
->all();
$data['articles'] => Article::whereHas('category', fn($query) =>
$query->whereIn('categories.id', $category_ids)
->where('categories.premium', 0)
)->get();
You can explode the categories and then make the query like this.
$categories = explode(',',$request->categories);
$data['articles'] = Article::whereHas('categories', function ($query) use($categories){
$query->whereIn('category_id', $categories)->where('premium',0);
})->get();

Cant join tables in laravel

I am new to laravel.
I need to join two tables, and i dont know what i am doing wrong.
When i check at telescope, my query isnt displayed.
This is the query i want to run
SELECT * FROM products join subcategories on products.subcategories_id = subcategories.id where subcategories.categories_id = 1
In my laravel controller i have
public function showCategoriesProducts(String $cat){
$categories = Categories::all();
$products = Products::join('subcategories', function($join) {
$join->on('products.subcategories_id', '=', 'subcategories.id');
}) ;
dd($products);
return view('products.index',compact('products','categories'));
}
This is my web.php
Route::get('/categories/{categories_id}', 'ProductController#showCategoriesProducts')->name('categories.show');
My Products belong to subcategories and subcategories belong to Categories. I need to fetch products from category id.
Please Help
Thank You!
You are building the query, but not fetching records. Add ->get() add the end of your query to make the request.
$products = Products::join('subcategories', function($join) {
$join->on('products.subcategories_id', '=', 'subcategories.id');
})->get();

Laravel Group By relationship column

I have Invoice_Detail model which handles all products and it's quantities, this model table invoice_details has item_id and qty columns.
The Invoice_Detail has a relation to Items model which holds all item's data there in its items table, which has item_id, name, category_id.
The Item model also has a relation to Category model which has all categories data in its categories table.
Question: I want to select top five categories from Invoice_Detail, how?
Here's what I did:
$topCategories = InvoiceDetail::selectRaw('SUM(qty) as qty')
->with(['item.category' => function($query){
$query->groupBy('id');
}])
->orderBy('qty', 'DESC')
->take(5)->get();
But didn't work !!
[{"qty":"11043","item":null}]
Category::select('categories.*',\DB::raw('sum("invoice_details"."qty") as "qty"'))
->leftJoin('items', 'items.category_id', '=', 'categories.id')
->leftJoin('invoice_details', 'invoice_details.item_id', '=', 'items.id')
->groupBy('categories.id')
->orderBy('qty','DESC')
->limit(5)
->get();
This will return you collection of top categories.
Tested on laravel 5.5 and PostgreSQL.
UPD:
To solve this without joins you can add to Categories model this:
public function invoiceDetails()
{
return $this->hasManyThrough(Invoice_Detail::class, Item::class);
}
And to select top 5 categories:
$top = Category::select()->with('invoiceDetails')
->get()->sortByDesc(function($item){
$item->invoiceDetails->sum('qty');
})->top(5);
But first solution with joins will work faster.

Laravel Eloquent how to join on a query rather than a table?

I want to achieve this in Laravel:
SELECT * FROM products JOIN
(SELECT product_id, MIN(price) AS lowest FROM prices GROUP BY product_id) AS q1
ON products.id = q1.product_id
ORDER BY q1.lowest;
I wrote this, but clearly there is something wrong:
$products = new Product();
$products = $products->join(
Price::whereNotNull('price')->select('product_id', DB::raw('min(price) as lowest'))->groupBy('product_id'), 'products.id', '=', 'product_id'
)->orderBy('lowest')->get();
The error I got:
ErrorException in Grammar.php line 39:
Object of class Illuminate\Database\Eloquent\Builder could not be converted to string.
I'm currently using join(DB::raw('(SELECT product_id, MIN(price) AS lowest FROM prices WHERE price IS NOT NULL GROUP BY product_id) AS q1'), 'products.id', '=', 'q1.product_id') as a workaround. Just wondering how to do this in the Eloquent way? Thanks.
If you want to make a single query for efficiency reasons in this case Eloquent is not going to help you much, it is possible but is a hassle. For cases like this you have QueryBuilder.
DB::table('products')
->join(
DB::raw('(SELECT product_id, MIN(price) AS lowest FROM prices GROUP BY product_id) AS q1'),
'products.id', '=', 'q1.product_id'
)
->orderBy('q1.lowest')->get();
If you change get() for getSql() you get the following
select * from `products` inner join
(SELECT product_id, MIN(price) AS lowest FROM prices GROUP BY product_id) AS q1
on `products`.`id` = `q1`.`product_id` order by `q1`.`lowest` asc
Unfortunately as far as I know you can't use a subquery without DB::raw, nevertheless it is not insecure as long as you don't put user input in the query. Even in that case you can use it securely by using PDO.
As for Eloquent, your product model doesn't even have a price field (it probably has a prices() function returning a relationship object) so it makes no sense to get a Product model with a single price asociated to it.
Edit:
You can also eager load the relationship, i.e. (assuming you have the model relationship set as Trong Lam Phan's example)
$products = Product::with('prices')->get();
foreach ($products as $product)
{
$minPrice = $product->prices->min('price');
// Do something with $product and $minPrice
}
This will only run a single query but the min() operation is not done by the database.
In Eloquent, follow me, you need to think a little bit differently from normal mysql query. Use Model instead of complicated query.
First of all, you need to create Product and Price models with relationship between them:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public function prices()
{
return $this->hasMany('\App\Price');
}
}
class Price extends Model
{
public function product()
{
return $this->belongsTo('\App\Product');
}
}
Then, you need to select all products:
$products = \App\Product::get();
foreach ($products as $product)
{
$minPrice = $product->prices->min('price');
echo 'Min price of product ' . $product->name . ' is: ' . $minPrice;
}
EDIT
If you have a problem with the performance, just get the product's id.
$products = \App\Product::get(['id']);
If you don't like this way of Eloquent, you may use Query Builder like that:
$result = \App\Price::join('products', 'prices.product_id', '=', 'products.id')
->select('product_id', 'name', \DB::raw("MIN(price) AS min_price"))
->groupBy('product_id')
->orderBy('min_price')
->get();
or you can do like that:
\App\Price::join('products', 'prices.product_id', '=', 'products.id')
->selectRaw("name, MIN(price) AS min_price")
->groupBy('product_id')
->orderBy('min_price')
->get()

Laravel Eloquent join several tables

I have 3 tables:
products
|id|name|about|
=categories=
|id|name|parent|
=products-categories=
|id|product_id|cat_id|
I need to take a product categories names. I have a sql query:
SELECT s.name FROM products AS p
LEFT JOIN `products-categories` AS cats ON p.id = cats.product_id
LEFT JOIN `categories` AS s ON cats.cat_id = s.id
WHERE product_id = 1;
And It works! But how I can do this with the help of Laravel Eloquent (Not Fluent!)
You can use Eloquent relationship and in this case, create two models, for both tables, i.e. product and Category:
class Category extends Eloquent {
protected $table = 'categories'; // optional
public function products()
{
return $this->belongsToMany('Product', 'products_categories', 'category_id', 'product_id');
}
}
class Product extends Eloquent {
protected $table = 'products'; // optional
public function categories()
{
return $this->belongsToMany('Category', 'products_categories', 'product_id', 'category_id');
}
}
Now you may use these relationship methods to get related data, for example:
$product = Product::with('categories')->find(1);
This will return the product with id 1 and all the related categories in a collection so you may use $product->categories->first()->name or you may do a loop on the categories like:
foreach($product->categories as $cat) {
echo $cat->name;
}
Also you may use join which doesn't require the relationship methods and you may use same approach to join the models that is used in Fluent (Check other answer). But either way, you need to store the category and product mappings in the products_categories table. Read more about many-to-many relationship on Laravel website.
Just use the leftJoin method. It works the same in Eloquent as in Query Builder.
$product = Product::leftJoin('product-categories', 'product-categories.product_id', '=', 'products.id')
->leftJoin('categories', 'categories.id', '=', 'product-categories.cat_id')
->where('product_id', 1)
->first(['categories.name']);

Categories