Scan pivot table for two identical values - laravel

I have this pivot table:
+----+---------+-----------------+
| id | user_id | conversation_id |
+----+---------+-----------------+
| 1 | 2 | 48 |
+----+---------+-----------------+
| 2 | 1 | 48 |
+----+---------+-----------------+
I am trying to find a way to detect if two users are in the same conversation, and if they are, return the conversation. Someting like:
Conversation::with([1, 2]); // should return conversation 48
My Conversation class looks like this:
class Conversation extend Eloquent {
public function users() {
return $this->belongsToMany('User');
}
public function messages(){
return $this->hasMany('Message');
}
}
So far I have below code, but it is clumsy and very ineffective, and thought that there must exists some cleaner Laravel way:
class Conversation {
public static function user_filter(Array $users) {
$ids = array();
foreach ($users as $user)
$ids[$user->id] = $user->id;
sort($ids);
foreach (self::all() as $conversation){ // loop through ALL conversations (!!!)
$u_ids = $conversation->users->lists('id');
sort ( $u_ids );
if ($u_ids === $ids)
return $conversation;
}
return null;
}
}
Usage:
Conversation::user_filter([User::find(1), User::find(2)]); // returns conversation 48
Any ideas?

Let's first look at how you can actually retrieve the result without having too loop trough all kind of arrays...
You can use the whereHas function to filter a relationship. We can apply conditions and if they are met for the related records of a model, it will be included in the result.
$conversations = Conversation::whereHas('users', function($q){
$q->where('user_id', 1);
})->get();
So that's for one user. For two or more users it's basically the same. You just need to chain whereHass
$conversations = Conversation::whereHas('users', function($q){
$q->where('user_id', 1);
})
->whereHas('users', function($q){
$q->where('user_id', 2);
})->get();
Now this isn't very elegant. You can create a query scope to do all the work for you and keep the controller code clean. In Conversation
public function scopeWithUsers($query, array $userIds){
foreach($userIds as $id){
$query->whereHas('users', function($q) use ($id){
$q->where('user_id', $id);
});
}
return $query;
}
Usage:
$conversations = Conversation::withUsers([1,2])->get();
$conversations = Conversation::withUsers([1,2,3,4])->get();
$conversations = Conversation::withUsers([1])->get();
If it should only be one anyways use first() instead of get()

Something like this should work.
class Conversation {
public static function user_filter(Array $users) {
$ids = array();
foreach ($users as $user) {
$ids[] = $user->id;
}
return self::select("conversation_id")->whereIn("user_id", $ids)->groupBy("conversation_id")->havingRaw("count(*) = " . count($ids))->get();
}
}

Get all conversations with given users.
Conversation::whereHas('users', function($query) use ($userids) {
$query->whereIn('user_id', $userids);
})->get();
If you need get special one, you can:
Conversation::whereHas('users', function($query) use ($userids) {
$query->whereIn('user_id', $userids);
})->whereId($id)->first();

Related

Laravel filter of multiple variables from multiple models

Goodmorning
I'm trying to make a filter with multiple variables for example I want to filter my products on category (for example 'fruit') and then I want to filter on tag (for example 'sale') so as a result I get all my fruits that are on sale. I managed to write seperate filters in laravel for both category and tag, but if I leave them both active in my productsController they go against eachother. I think I have to write one function with if/else-statement but I don't know where to start. Can somebody help me with this please?
These are my functions in my productsController:
public function productsPerTag($id){
$tags = Tag::all();
$products = Product::with(['category','tag','photo'])->where(['tag_id','category_id'] ,'=', $id)->get();
return view('admin.products.index',compact('products','tags'));
}
public function productsPerCategory($id){
$categories = Category::all(); //om het speciefieke id op te vangen heb ik alle categories nodig
$products = Product::with(['category','tag','photo'])->where('category_id', '=', $id)->get();
return view('admin.products.index',compact('products','categories'));
}
These are my routes in web.php. I guess this will also have to change:
Route::get('admin/products/tag/{id}','AdminProductsController#productsPerTag')->name('admin.productsPerTag');
Route::get('admin/products/category/{id}','AdminProductsController#productsPerCategory')->name('admin.productsPerCategory');
For filter both
change your URL like
Route::get('admin/products/tag/{tag_id?}/{category_id?}','AdminProductsController#productsPerTag')->name('admin.productsPerTag');
Make your function into the controller like
public function productsPerTag($tagId = null, $categoryId = null){
$tags = Tag::all();
$categories = Category::all();
$query = Product::with(['category','tag','photo']);
if ($tagId) {
$query->where(['tag_id'] ,'=', $tagId);
}
if ($tagId) {
$query->where(['category_id'] ,'=', $categoryId);
}
$products = $query->get();
return view('admin.products.index',compact('products','tags', 'categories'));
}
You are trying to filter in your query but you pass only 1 parameter to your controller, which is not working.
1) You need to add your filters as query params in the URL, so your url will look like:
admin/products/tag/1?category_id=2
Query parameters are NOT to be put in the web.php. You use them like above when you use the URL and are optional.
2) Change your controller to accept filters:
public function productsPerTag(Request $request)
{
$categoryId = $request->input('category_id', '');
$tags = Tag::all();
$products = Product::with(['category', 'tag', 'photo'])
->where('tag_id', '=', $request->route()->parameter('id'))
->when((! empty($categoryId)), function (Builder $q) use ($categoryId) {
return $q->where('category_id', '=', $categoryId);
})
->get();
return view('admin.products.index', compact('products', 'tags'));
}
Keep in mind that while {id} is a $request->route()->parameter('id')
the query parameters are handled as $request->input('category_id') to retrieve them in controller.
Hope It will give you all you expected outcome if any modification needed let me know:
public function productList($tag_id = null , $category_id = null){
$tags = Tag::all();
$categories = Category::all();
if($tag_id && $category_id) {
$products = Product::with(['category','tag','photo'])
->where('tag_id' , $tag_id)
->where('category_id' , $category_id)
->get();
} elseif($tag_id && !$category_id) {
$products = Product::with(['category','tag','photo'])
->where('tag_id' , $tag_id)
->get();
} elseif($category_id && !$tag_id) {
$products = Product::with(['category','tag','photo'])
->where('category_id' , $category_id)
->get();
} elseif(!$category_id && !$tag_id) {
$products = Product::with(['category','tag','photo'])
->get();
}
return view('admin.products.index',compact(['products','tags','products']));
}
Route:
Route::get('admin/products/tag/{tag_id?}/{category_id?}','AdminProductsController#productsPerTag')->name('admin.productsPerTag');

Orwhere has method does not allow null

enter image description hereI am trying to implement a many to many relationship search with 2 models.
i get input from multiple checkbox values and want to search for items that match A or B when there is an input of data.
I read this url and wrote the same logic.
https://laracasts.com/discuss/channels/laravel/many-to-many-relationship-with-2-pivot-table-data-search
public function search(Request $request)
{
$languages = $request->lang;
$fields = $request->field;
$agencies = Agency::with('languages')->with('specialized_fields')
->orWhereHas('languages', function($query) use ($languages) {
$query->whereIn('language_id', $languages);
})
->orWhereHas('specialized_fields', function($query) use ($fields) {
$query->whereIn('specialized_field_id', $fields);
})
->get();
dd($agencies);
}
i expected to achieve A or B search but instead I got this error.
Argument 1 passed to Illuminate\Database\Query\Builder::cleanBindings() must be of the type array, null given, called in /var/www/jtf/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php on line 907
it seems that it causes this error if either A or B is null, but why??? Does the OrWhereHas method work only when theres an input??
/added info/
my error message
my agency model
class Agency extends Model {
protected $guarded = [
'id'
];
public function languages(){
return $this->belongsToMany('App\Language');
}
public function specialized_fields(){
return $this->belongsToMany('App\SpecializedField');
}
public function region(){
return $this->hasOne('App\Region');
} }
I believe it's because either $languages or $fields is null.
Since ->whereIn() is expecting an array, but you're passing null.
You just need to make sure you're passing an array.
$languages = array_filter((array) $request->lang); // cast to array & remove null value
$fields = array_filter((array) $request->field);
$agencies = Agency::with('languages', 'specialized_fields')
->orWhereHas('languages', function($query) use ($languages) {
$query->whereIn('language_id', $languages);
})
->orWhereHas('specialized_fields', function($query) use ($fields) {
$query->whereIn('specialized_field_id', $fields);
})
->get();
I'm speculating that you started your where query chain with an orWhereHas() which may have caused the problem, try starting with whereHas() instead.
public function search(Request $request){
$languages = $request->lang;
$fields = $request->field;
$agencies = Agency::with('languages', 'specialized_fields') // you can get away by just using one with(), not needed but its cleaner this way
->whereHas('languages', function($query) use ($languages) { // previously orwherehas
$query->whereIn('language_id', $languages);
}) ->orWhereHas('specialized_fields', function($query) use ($fields) {
$query->whereIn('specialized_field_id', $fields);
})
->get();
dd($agencies);
}

Filter model with HasMany relationship

Let's say I have two models: Park and Items. Each Park can have a number of Items, through a HasMany relationship.
In Items table there's a park_id field and a type flag: an Item can be a fountain, or a basketball court, or whatever.
What I need to do is to filter the parks to obtain only those who has ALL of the item types passed in an array. This is what I have:
$parks = Park::whereHas('items', function($q) use ($request) {
$q->where('type', json_decode($request->type_list));
})->get();
But it's not working properly. Any ideas?
Thanks a lot, and happy new year to all!
Edit: I've found a working though really ugly solution:
$types_array = json_decode($request->type_list, true);
$results = [];
// A collection of parks for each item type
foreach($types_array as $type) {
$results[] = Park::whereHas('items', function($q) use ($type) {
$q->where('type', $type);
})->get();
}
// Intersect all collections
$parks = $results[0];
array_shift($results);
foreach ($results as $collection) {
$parks = $collection->intersect($parks);
}
return $parks;
You can use a foreach in whereHas method. It should be something like this:
$array = json_decode($request->type_list, true)
$parks = Park::whereHas('items', function($q) use ($array) {
foreach ($array as $key => $type) {
$q->where('type', $type);
}
})->get();
I think using where clause to match all item's type for the same row will result nothing, it's like doing where id = 1 and id = 2 and this is impossible because id can take only one value, what you should do is to use whereIn to get all items that match at least one type and then use groupBy to group result by park_id, then in the having clause you can check if the group count equal the type_list count:
$types = json_decode($request->type_list, true);
$parks = Park::whereHas('items', function($q) use ($types) {
$q->select('park_id', DB::raw('COUNT(park_id)'))
->whereIn('type', $types)
->groupBy('park_id')
->havingRaw('COUNT(park_id) = ?', [ count($types) ]);
})->get();
You can add multiple whereHas() constraint to one query:
$types_array = json_decode($request->type_list, true);
$query = Park::query();
foreach($types_array as $type) {
$query->whereHas('items', function($q) use($type) {
$q->where('type', $type);
});
}
$parks = $query->get();

Laravel - passing array of columns in 'where' clause

I have a search query like this:
$data = User::where('first_name', 'like', '%'.$query.'%')
->orWhere('last_name', 'like', '%'.$query.'%')
->get();
Now, I have many models, each with different column names. Instead of defining a search() function into every controller, I want to do this:
// User
public static function searchableFields()
{
return ['first_name', 'last_name'];
}
// Some other model
public static function searchableFields()
{
return ['name', 'description'];
}
And put the search logic in a shared controller, something like this:
$data = $classname::
where($classname::searchableFields(), 'like', '%'.$query.'%')
->get();
How can I achieve this?
Thanks a lot.
You can loop over the fields and add them to your Eloquent query one by one.
$data = $classname::where(function ($query) use ($classname) {
foreach ($classname::searchableFields() as $field)
$query->orWhere($field, 'like', '%' . $query . '%');
})->get();
I would use scope for that.
You can create base model that all the models should extend (and this model should extend Eloquent model) and in this model you should add such method:
public function scopeMatchingSearch($query, $string)
{
$query->where(function($q) use ($string) {
foreach (static::searchableFields() as $field) {
$q->orWhere($field, 'LIKE', '%'.$string.'%');
}
});
}
Now you can make a search like this:
$data = User::matchingSearch($query)->get();
Just to avoid confusion - $query parameter passed to matchingSearch becomes $string parameter in this method.
You can try something like this.
// Controller
function getIndex(Request $request)
{
$this->data['users'] = User::orderBy('first_name','asc')->get();
if ($request->has('keyword')) {
$results = User::search($request->keyword);
$this->data['users'] = collect([$results])->collapse()->sortBy('first_name');
}
}
// Model
function search($keyword)
{
$results = [];
$search_list = ['first_name','last_name'];
foreach ($search_list as $value)
{
$search_data = User::where($value,'LIKE','%'.$keyword.'%')->get();
foreach ($search_data as $search_value) {
$exist = 0;
if (count($results)) {
foreach ($results as $v) {
if ($search_value->id == $v->id) {
$exist++;
}
}
if ($exist == 0) {
$results[] = $search_value;
}
} else{
$results[] = $search_value;
}
}
}
return $results;
}

Chain whereHas to traverse through a self-referencing model

Using Laravel 5.4....
Imagine I have the following models:
App\Location
This has a self referencing hierarchy, and has the following data
UK
|---North West
| |----Liverpool
| |----Manchester
|---North East
|----Newcastle
|----Sunderland
In this model I have a self relation
public function parent()
{
return $this->belongsTo('App\Location', 'location_id');
}
and a recursive relation...
public function parentRecursive()
{
return $this->parent()->with('parentRecursive');
}
App\Shop
The shop model has a 'location' relation.
public function location()
{
return $this->belongsTo('App\Location', 'location_id');
}
What I want to do is to get all of the shops within a category. So if I have a shop called "ACME" that is related to "Liverpool", I can easily get it by sending the ID for "Liverpool" (as $value) in the following condition....
->whereHas('location', function($q) use ($value) {
$q->where('id', $value);
})
But technically, this shop is also in "North West" and in "UK".
So if I send the ID for the UK Location to that query, it will not return the ACME shop as it is not directly related to North West or UK ID.
I can get it working by sending the UK id ($value) to this...
$this->builder->whereHas('location', function($q) use ($value) {
$q->where('id', $value);
})->orWhereHas('location.parent', function($q) use ($value) {
$q->where('id', $value);
})->orWhereHas('location.parent.parent', function($q) use ($value) {
$q->where('id', $value);
});
So is there a better way to write the above which is ugly and would only work for a finite number of 'jumps' in the relation tree? I need it to traverse all of the locations until it reaches the top of the tree.
I encountered the same problem. My solution is not that elegant, but it works without having to use a nested set model:
First, (if you haven't done yet) you may need to create an inverse of your existing recursive relation:
public function children() {
return $this->hasMany('App\Location', 'location_id');
}
public function childrenRecursive() {
return $this->children()->with('childrenRecursive');
}
Next, you need to create a recursive function to get the max depth
public function getDepth($location) {
$depth = 0;
foreach ($location->children as $child) {
$d = $this->getDepth($child);
if ($d > $depth) {
$depth = $d;
}
}
return 1 + $depth;
}
Then, you can use the depth to chain your query with multiple orWhereHas clause
$whereHas = 'location';
$query = Shop::whereHas($whereHas, function ($q) use ($value) {
$q->where('id', $value);
});
for ($d = 1; $depth > $d; $d++) {
$whereHas = $whereHas . '.parent';
$query->orWhereHas($whereHas, function ($q) use ($value) {
$q->where('id', $value);
});
}

Resources