Laravel improve exists() boolean queries with eager loading - php

I'm trying to get rid of queries that look like this:
SELECT EXISTS(
SELECT *
FROM `ignorings`
WHERE `ignorings`.`user_id` = 80101 AND
`ignorings`.`user_id` IS NOT NULL AND
(`ignorable_id` = 79141)
) AS `exists`
SELECT EXISTS(
SELECT *
FROM `favorites`
WHERE `favorites`.`favorited_id` = 341 AND
`favorites`.`favorited_id` IS NOT NULL AND
`favorites`.`favorited_type` = '' App\Review
'' AND `user_id` = 80101
) AS `exists`
I'm trying to do it by eager loading:
auth()->user()->load(['favorite', 'ignoring']);
Here is my User model:
public function ignoring()
{
return $this->hasMany(Ignoring::class);
}
public function isIgnoring($userId)
{
return $this->ignoring()
->where(['ignorable_id' => $userId])
->exists();
}
Here is my blade file:
#if (! auth()->user() || ! auth()->user()->isIgnoring($review->user->id))
How can I get rid of these queries by eager loading a boolean? I want to load all the auth()->users() ignoring relationships, so to speak.

Here is what I came up with so far:
In the Controller method:
if (auth()->user()) {
$ignorings = auth()->user()->ignoring()->get();
foreach ($ignorings as $ignoring) {
$ignoringArray[] = $ignoring->ignorable_id;
}
$favoriteReviews = auth()->user()->favorite()->where('favorited_type', 'App\Review')->get();
foreach ($favoriteReviews as $favoriteReview) {
$favoriteReviewArray[] = $favoriteReview->favorited_id;
}
$favoriteReviewComments = auth()->user()->favorite()->where('favorited_type', 'App\ReviewComment')->get();
foreach ($favoriteReviewComments as $favoriteReviewComment) {
$favoriteReviewCommentArray[] = $favoriteReviewComment->favorited_id;
}
}
if (empty($ignoringArray)) {
$reviews = Review::with([
'product',
'user',
'favorites',
'reviewComments',
'reviewComments.favorites'
])
->where('created_at', '>=', Carbon::today()->subDays(7))
->paginate(20);
} elseif (! empty($ignoringArray)) {
$reviews = Review::whereNotIn('user_id', $ignoringArray)
->with('product', 'user', 'favorites', 'reviewComments.favorites')
->with([
'reviewComments' => function ($query) use ($ignoringArray) {
$query->whereNotIn('user_id', $ignoringArray);
}
])
->where('created_at', '>=', Carbon::today()->subDays(7))
->paginate(20);
}
return view('new-reviews.index', [
'reviews' => $reviews,
'favoriteReviewArray' => $favoriteReviewArray ?? null,
'favoriteReviewCommentArray' => $favoriteReviewCommentArray ?? null
]);
In the blade file:
#if(in_array($reviewComment->id, $favoriteReviewCommentArray ?? []))
#if(in_array($review->id, $favoriteReviewArray ?? []))
It's not pretty but it works.

You can easily use Model::withExists('relation') and $model->loadExists('releation') functions.
so you can use if like this
if($model->relation_exists){
//do something
}

Related

How to pass 2 parameters in a scopeFilter query

How do I go about passing two parameters in a filter query?
The below works but I want to use the $filters[$serviceDate ] instead of hardcoding the date in the last line
'filters' => Request::all('serviceDate', 'mealType'),
public function scopeFilter($query, array $filters)
{
$query
->when( $filters['mealType'] ?? null, function ($query, $mealType) {
$query->whereDoesntHave('student_meals', fn ($query) =>
$query->where('meal_type_id', $mealType )
->where('void', false)
->where('date_served', '2022-06-19')
});
}
I've tried
->when( $filters['mealType'] ?? null, function ($query, $mealType, $serviceDate) {
$query->whereDoesntHave('student_meals', fn ($query) =>
$query->where('meal_type_id', $mealType )
->where('void', false)
->where('date_served', $serviceDate));
and get the error:
Too few arguments to function App\Models\Student::App\Models{closure}(), 2 passed in /Applications/XAMPP/xamppfiles/htdocs/Sos/vendor/laravel/framework/src/Illuminate/Conditionable/Traits/Conditionable.php on line 30 and exactly 3 expected
I've tried
->when( ($filters['mealType'] && $filters['serviceDate']) ?? null, function ($query, $mealType, $serviceDate) {
$query->whereDoesntHave('student_meals', fn ($query) =>
$query->where('meal_type_id', $mealType )
->where('void', false)
->where('date_served', $serviceDate));
and get the error:
Undefined array key "mealType"
I know I'm missing something basic but struggling to figure it out. Any help is much appreciated.
Yes, you are missing how to inherit variables from the parent scope. That's is basic knowledge about anonymous function in PHP.
You need to use ($mealType), so the correct code should be like:
public function scopeFilter($query, array $filters)
{
$mealType = $filters['mealType'] ?? null;
$serviceDate = $filters['serviceDate'] ?? null;
$query
->when($mealType, function($query) use ($mealType,$serviceDate) {
$query->whereDoesntHave('student_meals', fn($query) => $query->where('meal_type_id', $mealType)
->where('void', false)
->where('date_served', $serviceDate)
);
});
}

Pass id from first model into a Eloquent

I have a problem wanting to pass the id of Products in the subqueries.
The first code is what I have so far. The second is the way I want to do with Eloquent, but I can't.
$result = [];
Product::with(['locals.presentations'])->each(function ($product) use (&$result) {
$body['id'] = $product->id;
$body['nombre'] = $product->nombre;
$sedes = [];
$product->locals->each(function ($local) use (&$sedes, $product) {
$presentations = [];
$local->presentations->each(function ($presentation) use (&$presentations, $local, $product) {
if ($presentation->local_id == $local->id && $presentation->product_id == $product->id) {
$presentations[] = [
'local_id' => $presentation->local_id,
'product_id' => $presentation->product_id,
'presentacion' => $presentation->presentation,
'precio_default' => $presentation->price
];
}
});
...
});
return $result;
I want transform the previous code into this with Eloquent, but I can't pass the product_id into the subqueries:
$products = Product::with(['locals' => function ($locals) {
//How to get the id from Product to pass in the $presentations query ??????
$locals->select('locals.id', 'descripcion')
->with(['presentations' => function ($presentations) {
$presentations
// ->where('presentations.product_id', $product_id?????)
->select(
'presentations.local_id',
'presentations.product_id',
'presentations.id',
'presentation',
'price'
);
}]);
}])->select('products.id', 'nombre')->get();
return $products;
Product
public function locals()
{
return $this->belongsToMany(Local::class)->using(LocalProduct::class)
->withPivot(['id', 'is_active'])
->withTimestamps();
}
Local
public function presentations()
{
return $this->hasManyThrough(
Presentation::class,
LocalProduct::class,
'local_id',
'local_product_id'
);
}
You can simply use the has() method if you have set the relations correctly on the Product and Local models. This will return ONLY the products which has locals AND presentations.
If you want every product but only the locals and presentations with the product_id equals to the products.id, then you don't have to do anything. The relationship you set in your models already checks if the id matches.
$products = Product::has('locals.presentations')
->with(['locals' => function ($locals) {
$locals
->select('locals.id', 'descripcion')
->with(['presentations' => function ($presentations) {
$presentations->select(
'presentations.local_id',
'presentations.product_id',
'presentations.id',
'presentation',
'price'
);
}]);
}])->select('products.id', 'nombre')->get();

Laravel query with subqueries and foreach

I wanna to show common users verified, not out of date and unbanned:
a) all songs
b) songs by title or text
c ) songs by tag
Two additions:
The user, which is an admin, can see unverified, banned and out of date songs and the user, which is an artist, can see unverified or banned songs too, but only his own ones.
It has been exhausting for several days, 'where in where, which is in loop...' is torture xD
Could you help my with scopeByUser funtion?
Song Model:
class Song extends Model
{
use HasFactory, Likable;
public function artist()
{
return $this->belongsTo(Artist::class, 'artist_id');
}
public function tags()
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
public function scopeByUser()
{
$user = current_user();
if ($user->hasRole('admin')) {
dd("x");
return $this;
} elseif (isset($user->artist)) {
return $this->where([
'isVerified' => true,
'isOutOfDate' => false,
'isBanned' => false
])->orWhere(function () use ($user) {
foreach ($user->artist as $artist) {
$this->where([
'artist_id', $artist->id,
'isOutOfDate' => false,
'isBanned' => false
]);
}
});
} else
return $this->where([
'isVerified' => true,
'isOutOfDate' => false,
'isBanned' => false
]);
}
}
SongController:
public function index(Request $request)
{
if (request('tag')) {
$songs = Song::query()
->whereHas('tags', function ($query) {
$tag = request('tag');
$query->where('name', $tag);
})
->ByUser()
->withLikes()
->get();
} elseif ($request) {
$search = $request->input('search');
$songs = Song::query()
->where('title', 'LIKE', "%{$search}%")->orWhere('text', 'LIKE', "%{$search}%")
->ByUser()
->withLikes()
->get();
} else {
$songs = Song::latest()
->ByUser()
->withLikes()
->get();
}
return view('welcome', compact('songs'));
}
One thing that I love about Laravel Eloquent is the when() method.
Laravel Collection #when()
I know the link is in the Collection, but it work with queries.
So, let review your queries and set it as one that you can make logical test and change it.
There is a non tested code in the only purpose of showing you what you could achieve.
$user = User::with('roles')->with('artists')->find(Auth::user()->id);
$songs = Song::when($request('tag'), function($query) use($request) {
$query->whereHas('tags', function($query2) use($request('tag')) {
$query2->where('name', $request('tag'));
});
})->when($request('search'), function($query) use($request) {
$query->where('title', 'LIKE', "%{$request('search)}%")->orWhere('text', 'LIKE', "%{$request('search)}%");
})->when(!isset($request('search')) && !isset($request('tags')), function($query) {
$query->latest();
})->when(true, function($query) use($user) {//setting when to true let you do more complexe logical test
if ($user->hasRole('admin')) {
//Nothing to do...
} else if (isset($user->artist)) {
$query->where([
'isVerified' => true,
'isOutOfDate' => false,
'isBanned' => false
])->orWhere(function ($query2) use ($user) {
foreach ($user->artist as $artist) {
$query2->where([//probably change this to orWhere. You can also get the array of artist id and use whereIn('id', $arrayOfId)->where(['isOutOfDate' => false, 'isBanned' => false]);
'artist_id', $artist->id,
'isOutOfDate' => false,
'isBanned' => false
]);
}
});
} else {
$query->where([
'isVerified' => true,
'isOutOfDate' => false,
'isBanned' => false
]);
}
})->ByUser()
->withLikes()
->get();
So, get back to what you really asked... using scope... I discourage using Scope when you are doing complexe logical test. WHY??? Simply because it look like your scope is retrieving the user and I'm assuming that this method make at least one request to DataBase... so, this lead to a N+1 problem.
Scope are good for simple task. One DB request in a scope will do the request for every single models. Avoid this.
If well used, when() method will help you to build complexe query that will result on a single DataBase query.

Laravel variable to where clause

I am trying to extend one app to use my new Laravel app. In this scenario I am getting an unknown number of filters and I would like to forward them all to where() clause.
I have made something like this:
private function filterConverter($filter)
{
$f = [];
foreach ($filter as $singleFilter) {
$operator = $this->filterValues[$singleFilter['operator']];
$value = $operator == 'like' ? '%' . $singleFilter['value'] . '%' : $singleFilter['value'];
$f[] = $singleFilter['field'] . ',' . $operator . ',' . $value;
}
return $f;
}
The thing is that I am getting operators like EQUALS and CONTAINS so I need to convert them to = and LIKE.
With this code I am trying to do this:
return response(MyModel::where($filter)->get());
But it doesn't work. Is there any elegant way to resolve this?
EDIT/SOLUTION
Sorry to #HCK as I couldn't quite accept the answer since it doesn't answer my question, but it pointed me on the right track. The solution was to use key, operator, value keys in the array instead of what I had "keyless".
private function filterConverter($filters)
{
$filter = [];
foreach ($filters as $singleFilter) {
$operator = $this->filterMap[$singleFilter['operator']];
$value = $operator == 'LIKE' ? '%' . $singleFilter['value'] . '%' : $singleFilter['value'];
$filter[] = [
'key' => $singleFilter['field'],
'operator' => $operator,
'value' => $value
];
}
return $filter;
}
Not the nicest way to solve it, but this should work:
private function filterConverter($filters)
{
return collect($filters)->map(function ($filter) { // <---
if($filter['operator'] == 'CONTAINS')
{
$filter['value'] = '%' . $filter['value'] . '%';
$filter['operator'] = 'LIKE';
}
else if ($filter['operator'] == 'EQUALS')
{
$filter['operator'] = '=';
}
return collect($filter)->flatten(); // <---
})->toArray(); // <---
}
Here I'm using the Map() function of the Collection class. there is a lot of useful methods provided by this class.
You may follow this way
DB::table('users')
->where(function($query) use ($filter)
{
// You able to access $filter here
// You may able to to generate this block by loop
$query->where('votes', '>', 100)
->where('title', '<>', 'Admin');
})
->get();
Laravel Doc | Advanced Wheres
Exp-1
$filters = [
['key' => 'votes', 'operator' => '>', 'value' => 100]
];
DB::table('users')
->where(function ($query) use ($filters) {
foreach ($filters as $filter) {
if (#$filter['key'] && #$filter['operator'] && #$filter['value']) {
$query->where($filter['key'], $filter['operator'], $filter['value']);
}
}
})->get();
Exp-2
$filters = [
['key' => 'votes', 'operator' => '>', 'value' => 100]
];
DB::table('users')
->where(function ($query) use ($filters) {
foreach ($filters as $filter) {
if (#$filter['key'] && #$filter['operator'] && #$filter['value']) {
$query->whereRaw("{$filter['key']} {$filter['operator']} '{$filter['value']}'");
}
}
})->get();
You may also use scope function Ref
Exp-3 Laravel Scope
class User extends Model
{
public function scopeFilter($query, $filters)
{
foreach ($filters as $filter) {
if (#$filter['key'] && #$filter['value']) {
$query->where($filter['key'], #$filter['operator']?:"=", $filter['value']);
}
}
return $query;
}
}
// Use
User::filter($filters)->get();

How can I orderBy() a column in an eager loaded relationship in Laravel 4

How would I orderBy a relationships column in a query?
For example I have an $order variable that contains something like 'title.asc', it's then exploded and sent to the query in the model via the controller and I use $order[0] and $order[1] respectively but if the column is on the eager loaded relationships table, how could I use this?
public function scopeEvents($query, $order, $course, $supplier, $city, $venue, $eventStatus, $tutor)
{
$date = date('Y-m-d');
$query->where('active', 1)
->with('course', 'venue', 'supplier', 'eventType', 'eventStatus', 'tutor', 'delegates')
->where('start_date', '>=', $date)
->orderBy($order[0], $order[1]);
if ( ! empty($course[0]))
{
$query = $query->whereIn('course_id', $course);
}
if ( ! empty($supplier[0]))
{
$query = $query->whereIn('supplier_id', $supplier);
}
if ( ! empty($venue[0]))
{
$query->whereIn('venue_id', $venue);
}
if ( ! empty($event_status[0]))
{
$query->whereIn('event_status_id', $eventStatus);
}
if ( ! empty($tutor[0]))
{
$query->whereIn('tutor_id', $tutor);
}
}
At the moment I just get a column not found, which is to be expected.
In order to reduce load times while also being able to order by related table columns, I had to combine eager loading with joins, and use the joins for ordering and the eager loading to display data in the view. Seems to work well.
public function scopeEvents($query, $order, $course, $supplier, $city, $venue, $eventStatus, $tutor)
{
$date = date('Y-m-d');
$query->where('active', 1)
->with('course', 'venue', 'supplier', 'eventType', 'eventStatus', 'tutor', 'delegates')
->leftJoin('courses', 'courses.id', '=', 'events.course_id')
->leftJoin('suppliers', 'suppliers.id', '=', 'events.supplier_id')
->leftJoin('venues', 'venues.id', '=', 'events.venue_id')
->leftJoin('event_types', 'event_types.id', '=', 'events.event_type_id')
->leftJoin('event_statuses', 'event_statuses.id', '=', 'events.event_status_id')
->leftJoin('tutors', 'tutors.id', '=', 'events.tutor_id')
->select('events.*', DB::raw("concat(tutors.first_name,' ',tutors.last_name) as tname"), 'event_statuses.status', 'event_types.type', 'venues.name as vname', 'suppliers.name as sname', 'courses.title', 'venues.city')
->where('start_date', '>=', $date)
->orderBy($order[0], $order[1]);
if ( ! empty($course[0]))
{
$query = $query->whereIn('course_id', $course);
}
if ( ! empty($supplier[0]))
{
$query = $query->whereIn('supplier_id', $supplier);
}
if ( ! empty($venue[0]))
{
$query->whereIn('venue_id', $venue);
}
if ( ! empty($event_status[0]))
{
$query->whereIn('event_status_id', $eventStatus);
}
if ( ! empty($tutor[0]))
{
$query->whereIn('tutor_id', $tutor);
}
}

Categories