Laravel filtering a hasMany relation - php

Background first on my question. A user hasMany contacts and a contact hasMany anniversaries. I want to filter the upcoming anniversaries. So far I have this:
$filtered = auth()->user()->contacts()->get()->each(function ($contact) {
$contact->anniversaries->filter(function ($anniversary) {
// return true or false based on a method on the anniversary model
return $anniversary->method() == true;
});
});
But this just returns all the contacts (obviously) with all their anniversaries, and I wish to exclude the ones that are false when calling the $anniversary->method().
Whatever is in the $anniversary->method() is not important, this just returns a true or false.
When I do the following, it works:
$collection = auth()->user()->anniversaries()->get();
$filtered = $collection->filter(function ($anniversary) {
return $anniversary->method() == true;
});
I get only the anniversaries from where the $anniversary->method() is indeed true.
My question is mainly, why does this happen, I only want to understand it, not so much need an answer on how to make it work. Thanks in advance for any insights!

In the first example, you are only filtering the anniversaries of each contact. You are not filtering the contacts directly per see.
$filtered = auth()->user()->contacts()->get()->each(function ($contact) {
// You are filtering only the anniversaries of each contact
$anniversaries = $contact->anniversaries->filter(function ($anniversary) {
return $anniversary->method() == true;
});
// over here you should get the same as in your second example
dd($anniversaries);
});
In your second example you are doing the following pseudo-code:
Fetch all anniversaries of each user
Filter the anniversaries that matches $anniversary->method() === true
To get the same results in the first example you would have to use a combination of filter with ->count()
$filtered = auth()->user()->contacts()->get()->filter(function ($contact) {
$filteredByMethod = $contact->anniversaries->filter(function ($anniversary) {
// return true or false based on a method on the anniversary model
return $anniversary->method() == true;
});
return $filteredByMethod->count() > 0;
});
Which one is more performant is beyond the scope of this answer.
Quick tip
The method filter from Laravel's collections needs to return a truthy value to be filtered by. Since your method returns true or false you can just call the method directly without comparing with true:
$collection = auth()->user()->anniversaries()->get();
$filtered = $collection->filter(function ($anniversary) {
return $anniversary->method();
});

Related

Return relation if it counts greater than 0 else return other relation

I want to return a relation if it has results greater than 0 otherwise it should return other relation
public function default_product_image()
{
if(count($this->ztecpc_product_images()->get()) > 0)
{
return $this->hasMany('App\ZtecpcProductImage','product_id','id')//if count > 0 then return this relation
->select('id','product_id','image');
}
return $this->hasOne('App\ProductImagesModel','product_id','id')//else return this relation
->where('main_image',1)
->select('id','product_id','image');
}
public function ztecpc_product_images()
{
return $this->hasMany('App\ZtecpcProductImage','product_id','id')
->select('id','product_id','image');
}
I want to return return $this->hasMany('App\ZtecpcProductImage','product_id','id')
->select('id','product_id','image'); if ztecpc_product_images relation has the results greater than 0 otherwise it should return other relation in the function. My problem is that it is always return empty array which I guess it the if-condition if(count($this->ztecpc_product_images()->get()) > 0) is always true.
Change your if statement like that:
if($this->ztecpc_product_images()->count() > 0)
I think your guess is correct and I believe I know why:
$this->ztecpc_product_images
Will return an Eloquent Collection rather than an array. This is an object and will always return true even if empty.
Try replacing it with the following:
if ($this->ztecpc_product_images->isNotEmpty()) {
...
}
This uses the available Eloquent Collection methods to check for a lack of results.

Laravel check if return of eloquent is single collection or collection of items

I wants to know how to check if the return of Eloquent Query is single row from DB or multiple rows.
I tried $record->count() but it always return a value greater than 1 in the 2 cases.
For example:
$record = User::first();
return $record->count(); //it return the count of columns in users table
and if I tried to get all users;
$record = User::all();
return $record->count(); //it return the count of all rows in users table
So how to deal with this case ?
You can use the instanceof construction to check what kind of data your variable is.
For your examples, this will likely be something like this:
$record = User::first();
$record instanceof \App\User; // returns true
$record instanceof \Illuminate\Database\Eloquent\Collection; // returns false
$record = User::all();
$record instanceof \App\User; // returns false
$record instanceof \Illuminate\Database\Eloquent\Collection; // returns true
Docs: https://secure.php.net/instanceof
$record = ......;
if($record instanceof \Illuminate\Support\Collection){
// its a collection
}
elseif($record instanceof \App\User){
// its a single url instance
}
However, above will not work directly if you are using DB builders :
$record = DB::table('users')->get();
$record is an array. So you need to hydrate it so you can use above logic on it :
if(is_array($record){
$record = \App\User::hydrate($record);
}
Now you can use if else logic on $record as its converted from an array to \Illuminate\Database\Eloquent\Collection which internally extends from \Illuminate\Support\Collection
Also, second case if someone did first() instead of get():
$record = \DB::table('users')->first();
Then $record is an stdClass object. so you can avoid hydration and consider it as a single user data.
I am concerned about the system logic and patterns where you need to have this kind of conditional. If possible, I would recommend to refactor in such a way that you function always knows if it's a collection or an instance. You can use type hints in functions to be more clear.
first() always returns only 1 row
$record = User::first(); // this will return only 1 records
To get number of rows in users table you need to create another query
$allrow = User::all();
return $allrow->count();
OR
$allrow = DB::table('users')->get();
return $allrow->count();
User::all() returns an array of User So the simple way is to check if is an array.
if (is_array($record)){
}

Laravel 5.4 eloquent hasMany and subsequent PHP to check an approval status

I have two models, Email and EmailReview. Email hasMany EmailReviews. This query:
$data = Email::with('emailReviews')->where('created_by', '=', $personId)->get();
returns a collection of a specific person's emails that have a review_group_type of either approval or feedback and a relationship of email_reviews that have an approved property of either true, false or null. I'm looking for the cleanest way to check if the email is an approval email and all of the reviews in the collection have their approved column set to true in order to set an approval status for each email i.e., $email->approvalStatus. Some emails will also not have reviews yet, so I'll need to check for that. Something along these lines, but it's clearly getting ugly and I'm having a hard time getting my end result:
if ($data->review_group_type === 'approval') {
foreach($data as $email) {
if (!is_null($email->email_reviews))
foreach($email->email_reviews as $review) {
}
}
}
Building on #Salama96 response and going a little bit further, you can do this:
$data = Email::with('emailReviews')
->where(['created_by'=>$personId,'review_group_type'=>'approval'])
->has('emailReviews')
->get()
->map(function ($email){
// Here, you check if all the emailReviews are 'true'
$reviewsStatus = $email->emailReviews->pluck('approved')->unique();
if ($reviewsStatus->count() == 1 AND $reviewsStatus->first())
{
$email->approvalStatus = true;
}
return $email;
});
}
PS: Haven't test it properly, but it should work.
I think you can simplify your query to be like this :
$data = Email::with('emailReviews')
->where(['created_by'=>$personId,'review_group_type'=>'approval'])
->has('emailReviews')->get();
Here you are going to get all emails created by specific person with an approval condition and have emailReviews

Check if collection contains a value

I have the relationships set up (correctly i think).. I have 3 tables, users, comments, and comments' likes table.
In my blade, I can access {{ $comment->commentLikes }} with this and it's returning me:
[{"id":85,"comment_id":12,"user_id":1,"action":1},
{"id":86,"comment_id":12,"user_id":3,"action":1},
{"id":87,"comment_id":12,"user_id":4,"action":1},
{"id":88,"comment_id":12,"user_id":6,"action":1},
{"id":89,"comment_id":12,"user_id":9,"action":1}]
user_id represents owner of the like.
Now I want to check if this collection has the authenticated user, in other words if the current user liked this comment or not.. Is there a way to do that rather than using a for loop? Maybe something like {{ $comment->commentLikes->owner }} so that I can use
'if (this) contains Auth::user->id()'...
One way to check this is using where() and first() collection methods:
if ($comment->commentLikes->where('user_id', auth()->user()->id)->first())
// This works if $comment->commentLikes returns a collection
$userLikes = $comment->commentLikes->where('user_id', Auth::user()->id)->all();
if ($userLikes->count() > 0) {
// Do stuff
}
Option 1
Using Laravel collection you could do this:
$comment->commentLikes->search(function ($item, $key) {
return $item->user_id == auth()->user()->id;
});
This will return false if none is found or index if the user is in the collection.
Making it pretty
But since you'll probably use this in a view I would package it in an accessor, something like this:
class Comment extends Model
{
public function commentLikes() {
// relationship defined
}
public function getHasLikedAttribute()
{
// use getRelationValue so it won't load again if it was loaded before
$comments = $this->getRelationValue('commentLikes');
$isOwner = $comment->commentLikes->search(function ($item, $key) {
return $item->user_id == auth()->user()->id;
});
return $isOwner === false ? false : true;
}
}
This way you can call $comment->has_liked which will return true if currently logged in user is in the relationship commentLikes or false if he isn't.
Use:
if ($comment->commentLikes()->where('user_id', auth()->user()->id)->count() > 0)
This is more efficient than other answers posted because both the where() and the count() applies on the query builder object instead of the collection

Laravel $query->toSql() loses relation extras

I've got the below function, which attempts to match users on specific whitelisted fields, which works brilliantly, for small amounts of data, but in our production environment, we can have > 1 million user records, and Eloquent is (understandably) slow when creating models in: $query->get() at the end. I asked a question this morning about how to speed this up and the accepted answer was brilliant and worked a treat, the only problem now, is that the resulting query being sent to DB::select($query->toSql()... has lost all of the required extra relational information I need. So is there any way (keeping as much of the current function as possible), to add joins to DB::select so that I can maintain speed and not lose the relations, or will it require a complete re-write?
The recipients query should include relations for tags, contact details, contact preferences etc, but the resulting sql from $query->toSql() has no joins and only references the one table.
public function runForResultSet()
{
$params = [];
// Need to ensure that when criteria is empty - we don't run
if (count($this->segmentCriteria) <= 0) {
return;
}
$query = Recipient::with('recipientTags', 'contactDetails', 'contactPreferences', 'recipientTags.tagGroups');
foreach ($this->segmentCriteria as $criteria) {
$parts = explode('.', $criteria['field']);
$fieldObject = SegmentTableWhiteListFields::where('field', '=', $parts[1])->get();
foreach ($fieldObject as $whiteList) {
$params[0] = [$criteria->value];
$dateArgs = ((strtoupper($parts[1]) == "AGE" ? false : DatabaseHelper::processValue($criteria)));
if ($dateArgs != false) {
$query->whereRaw(
DatabaseHelper::generateOperationAsString(
$parts[1],
$criteria,
true
),
[$dateArgs['prepared_date']]
);
} else {
// Need to check for empty value as laravel's whereRaw will not run if the provided
// params are null/empty - In which case we need to use whereRaw without params.
if (!empty($criteria->value)) {
$query->whereRaw(
\DatabaseHelper::generateOperationAsString(
$parts[1],
$criteria
),
$params[0]
);
} else {
$query->whereRaw(
\DatabaseHelper::generateOperationAsString(
$parts[1],
$criteria
)
);
}
}
}
}
// Include any tag criteria
foreach ($this->segmentRecipientTagGroupCriteria as $criteria) {
$startTagLoopTime = microtime(true);
switch (strtoupper($criteria->operator)) {
// IF NULL check for no matching tags based on the tag group
case "IS NULL":
$query->whereHas(
'recipientTags',
function ($subQuery) use ($criteria) {
$subQuery->where('recipient_tag_group_id', $criteria->recipient_tag_group_id);
},
'=',
0
);
break;
// IF NOT NULL check for at least 1 matching tag based on the tag group
case "IS NOT NULL":
$query->whereHas(
'recipientTags',
function ($subQuery) use ($criteria) {
$subQuery->where('recipient_tag_group_id', $criteria->recipient_tag_group_id);
},
'>=',
1
);
break;
default:
$query->whereHas(
'recipientTags',
function ($subQuery) use ($criteria) {
$dateArgs = (DatabaseHelper::processValue($criteria));
$subQuery->where('recipient_tag_group_id', $criteria->recipient_tag_group_id);
if ($dateArgs != false) {
$subQuery->whereRaw(
DatabaseHelper::generateOperationAsString(
'name',
$criteria,
true
),
[$dateArgs['prepared_date']]
);
} else {
// Need to check for empty value as laravel's whereRaw will not run if the provided
// params are null/empty - In which case we need to use whereRaw without params.
if (!empty($criteria->value)) {
$subQuery->whereRaw(
\DatabaseHelper::generateOperationAsString(
'name',
$criteria
),
[$criteria->value]
);
} else {
$subQuery->whereRaw(
\DatabaseHelper::generateOperationAsString(
'name',
$criteria
)
);
}
}
},
'>=',
1
);
}
}
//$collection = $query->get(); // slow when dealing with > 25k rows
$collection = DB::select($query->toSql(), $query->getBindings()); // fast but loses joins / relations
// return the response
return \ApiResponse::respond($collection);
}
By lost relational information do you mean relations eagerly loaded the name of which you passed to with()?
This information was not lost, as it was never in the query. When you load relations like that, Eloquent runs separate SQL queries to fetch related objects for the objects from your main result set.
If you want columns from those relations to be in your result set, you need to explicitely add joins to your query. You can find information about how to do this in the documentation: https://laravel.com/docs/5.1/queries#joins

Categories