how to group by the relation entity in laravel - php

i have 2 models order and city and each order has one city now what i want to do is to show the user how many orders per each city he has . and the result for example would be some thing like below :
Illuminate\Support\Collection {#2220 ▼
#items: array:17 [▼
"new-york" => 80
"city-number2" => 20
"city-number3" => 10
"city-number4" => 5
]
}
what i have tried so far is to work with the laravels withcount on order like below :
$orders = Order::withCount('orders')->where('shop_id', $shop->id)->get();
but no luck with it because it returns the count of all cities not each city individually .
thanks in advance

You can do this query from the other direction easily:
$cities = City::withCount(['orders' => fn ($q) => $q->where('shop_id', $shop->id)])->get();
foreach ($cities as $city) {
echo "{$city->name}: {$city->orders_count}";
}
If you need to restrict which cities are listed you can constrain that further if needed.

this problem is based on one to many relationships in DBMS, in laravel there is an inbuild option that comes with eloqut (one city has many orders and one order belongs to one city)
first of all, you need to implement the cities model and order model
inside cities model define a function like this
public function orders()
{
return $this->belongsTo(Order::class, 'city_id');
}
and in order model define a function like this
public function city()
{
return $this->hasMany(City::class, 'order_id');
}
then in the controller to find how many orders per each city use the query like this with laravel eloquent
$cities= City::withCount(['orders' => fn ($q) => $q->where('shop_id', $shop->id)])->get();
foreach ($cities as $city) {
echo $city->orders_count;
}
The withCount method which will place a {relation}_count attribute on the resulting models:

the controller to find how many orders per each city use the query like this with laravel eloquent.
$orders = Order::with('cities')
->select('city_id',\DB::raw('count(*) as city'))
->groupby('city_id')
->get();

Related

Laravel/Php - Nested Model calling using Eloquent ORM

Assume the following, I have a:
Course Model:
belongsToMany User Model
belongsToMany Exam Model
HasMany Question Model
HasMany Answers Model
Course Exam Results Table
id
user_id
course_id
exam_id
question_id
answer_id
Is there a better way to get the results without having to directly query the table?
I am currently doing the following:
public function displayResult($user, $course, $exam) {
$course_exam_results = \App\CourseExamResult::where([
'user_id' => $user->id,
'course_id' => $course->id,
'exam_id' => $exam->id
])
->get();
dd($course_exam_results);
}
Would love to do something better like auth()->user()->course(??)->exam(??)->results. I thought about using laravel hasManyThrough but I don't think I can make it work in this case as it only accepts 2 Models while I might have to call 3-5 models at a time.
Thank you in advance.
If you want to use multiple wheres in Laravel Eloquent, you must do like example below:
You must give to where() an array.
Every search has two arguments so it is an array in an array.
$course_exam_results = \App\CourseExamResult::where([
['user_id', auth()->user()->id],
['course_id', $course->id],
['exam_id', $exam->id]
])
->firstOrFail();

Laravel: pivot table extra field in model

Let's assume I've got a User model and the Country model.
User could have up to four countries simultaneously (home country and three countries he'd like to visit).
Using Eloquent, I'd create a pivot able country_user with an extra field of type, that would correspond to the choice of the user, which country he'd put in the first place etc.
So once again, there is:
User
--id
Country
--id
country_user
--id
--user_id
--country_id
--type
I need to get all users who have, let's say, Canada as a country type 1, UK as a country type 2, France as a country type 3 and New Zealand as a country type 4. If I understand correctly then I can't use wherePivot to do this, only if I get a User::all() and iterate through this collection (which doesn't make much sense and puts unnecessary load on the DB).
What could be the proper way to do this with a minimum possible amount of queries to DB?
Or do I get it all wrong and there should be another approach to this task at all?
You can still search on the pivot table using the relationship you have defined between a User and Country. If for the sake of this example we call that relationship countries and that you want to find people with the following $criteria (sticking to your example):
$criteria = [
'canada' => 1,
'uk' => 2,
'france' => 3,
'new zealand' => 4,
];
So the key in the above is the Country and the value is the type.
To find Users that have those exact requirements, you could do the following:
$users = User::whereHas('countries');
collect($criteria)->each(function ($type, $country) use ($users) {
$users->whereHas('countries', function (Builder $query) use ($type, $country) {
$query->where(['name' => $country, 'type' => $type]);
})->get();
});
Then do something with your $users.
$users->get();
You can the country relationship of HasMany
So, in users it would be this
public function countries() {
return $this->hasMany('country_user_class', 'foreign_key_id_here')->latest()->take(4); // take 4 since the user can have 4 countries
}
The query would look something like this.
User::whereHas('countries', function($query) {
$query->where('type', [1,2,3,4]);
})->get();
This would give you the result. This is how you can achieve it using Query. Such a problem can always be solved by Query. Let me know if you've any more question regarding this.

How to get pivot data inside eloquent withCount function callback?

I'm trying to use a pivot variable of a parent relationship inside the eloquent withCount() method.
Background:
There is a application with a ManyToMany relationship between Users and Clusters. Users can send messages within a cluster. To keep track of the unread message count for a user in a specific cluster i keep track of the last read message id in the join table, like so:
table: cluster_user
cluster_id | user_id | last_read_message_id
-------------------------------------------
1 | 59 | 3
2 | 62 | 8
The User() model has a belongsToMany() relation with the Cluster() model
The Cluster() model has a belongsToMany() relation with the User() model
The Cluster() model has a hasMany() relation with the Messages() model
The Message() model has a belongsTo() relation with the Cluster() model
Now I would like to list all the clusters of the authenticated user including a unread message count.
Currently I'm stuck on this:
$clusters = Auth::user()->clusters()->withCount(['messages' => function ($query) {
$query->where('messages.id', '>', '???');
}])->get();
I've already tried:
$clusters = Auth::user()->clusters()->withCount(['messages' => function ($query) {
$query->where('messages.id', '>', 'cluster_user.last_read_message_id');
}])->get();
But this gives me a total count of all the messages in stead of the ones with an id higher than x.
If I hardcode an id, like this:
$clusters = Auth::user()->clusters()->withCount(['messages' => function ($query) {
$query->where('messages.id', '>', '3');
}])->get();
Then I get the correct unread message count.
So can somebody tell me how to use the pivot variable 'last_read_message_id' of the user()->cluster() relationship inside the withCount() callback function with the following in mind:
I'ts crucial to use as little queries as possible.
The unread message count must be a part of the cluster() collection because I'm returning a ClusterResource later on, like so:
return ClusterResource::collection($clusters);
which includes the unread message count.
class ClusterResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'name' => $this->name,
'unread_messages_count' => $this->whenPivotLoaded('cluster_user', $this->messages_count)
];
}
}
Thnx!
Found the answer due to a comment of #cbaconnier.
Placing DB::raw('cluster_user.last_read_message_id') on the spot is working. I't not neat, but it works.
Full example:
$clusters = Auth::user()
->clusters()
->withCount(['messages' => function ($query) {
$query->where('messages.id', '>', DB::raw('cluster_user.last_read_message_id'));
}])
->get();
Good question! I think you should be able to do use the withPivot method on your relationship and then use the pivot attribute in your query.
For example, in your Cluster model where you define the cluster_user relationship, do:
function cluster_user() {
return $this->belongsToMany('App\User')
->withPivot('last_read_message_id');
}
And then in your query you could use whereColumn to compare the columns. Something like this:
$clusters = Auth::user()
->clusters()
->withCount(['messages' => function ($query) {
$query->whereColumn('messages.id', '>',
'pivot.last_read_message_id');
}])
->get();
Search for "Retrieving Intermediate Table Columns" on the Eloquent relationships documentation for more information.

How to access Pivot in Laravel?

I'm having problems accessing the Pivot relationship.
#relations: array:1 [
"orders_material" => Collection {#250
#items: []
}
]
I want to get the orders_material that have pivot values null
This is the query :
$material = Material::with(['orders_material' => function($query) use ($begin_date, $end_date, $begin_hour, $hour_final)
{
$query->whereBetween('Orders_has_Material.date_begin',[$begin_date, $end_date])
->whereBetween('Orders_has_Material.date_final',[$begin_date, $end_date])
->where('Orders_has_Material.Material_id', null)
->where('Orders_has_Material.Orders_id', null);
}])
->get();
dd($material);
public function orders_material()
{
return $this->belongsToMany('App\Orders', 'Orders_has_Material', 'Material_id', 'Orders_id');
}
First of all, as I see here u have many columns in ur pivot table like date_begin and date_final so here u need to edit ur relationship models like this :
public function orders_material()
{
return $this->belongsToMany('App\Orders', 'Orders_has_Material', 'Material_id', 'Orders_id')->withPivot('date_begin', 'date_final', 'column3');
}
and to access to them just u need to get $material of orders or the inverse.
which means, as u get the $material in ur query, i think to want to access to the orders related to that $material and show up some information inside ur pivot table. u can access to them like this (if ur relationship models are correct of course):
foreach($material->orders as $order)
{
echo $order->pivot->date_begin;
echo $order->pivot->date_final;
}
for more information read the doc many to many laravel with pivot table

How to order by pivot table data in Laravel's Eloquent ORM

In my Database, I have:
tops Table
posts Table
tops_has_posts Table.
When I retrieve a top on my tops table I also retrieve the posts in relation with the top.
But what if I want to retrieve these posts in a certain order ?
So I add a range field in my pivot table tops_has_posts and I my trying to order by the result using Eloquent but it doesn't work.
I try this :
$top->articles()->whereHas('articles', function($q) {
$q->orderBy('range', 'ASC');
})->get()->toArray();
And this :
$top->articles()->orderBy('range', 'ASC')->get()->toArray();
Both were desperate attempts.
Thank you in advance.
There are 2 ways - one with specifying the table.field, other using Eloquent alias pivot_field if you use withPivot('field'):
// if you use withPivot
public function articles()
{
return $this->belongsToMany('Article', 'tops_has_posts')->withPivot('range');
}
// then: (with not whereHas)
$top = Top::with(['articles' => function ($q) {
$q->orderBy('pivot_range', 'asc');
}])->first(); // or get() or whatever
This will work, because Eloquent aliases all fields provided in withPivot as pivot_field_name.
Now, generic solution:
$top = Top::with(['articles' => function ($q) {
$q->orderBy('tops_has_posts.range', 'asc');
}])->first(); // or get() or whatever
// or:
$top = Top::first();
$articles = $top->articles()->orderBy('tops_has_posts.range', 'asc')->get();
This will order the related query.
Note: Don't make your life hard with naming things this way. posts are not necessarily articles, I would use either one or the other name, unless there is really need for this.
For Laravel 8.17.2+ you can use ::orderByPivot().
https://github.com/laravel/framework/releases/tag/v8.17.2
In Laravel 5.6+ (not sure about older versions) it's convenient to use this:
public function articles()
{
return $this->belongsToMany('Article', 'tops_has_posts')->withPivot('range')->orderBy('tops_has_posts.range');
}
In this case, whenever you will call articles, they will be sorted automaticaly by range property.
In Laravel 5.4 I have the following relation that works fine in Set model which belongsToMany of Job model:
public function jobs()
{
return $this->belongsToMany(Job::class, 'eqtype_jobs')
->withPivot(['created_at','updated_at','id'])
->orderBy('pivot_created_at','desc');
}
The above relation returns all jobs that the specified Set has been joined ordered by the pivot table's (eqtype_jobs) field created_at DESC.
The SQL printout of $set->jobs()->paginate(20) Looks like the following:
select
`jobs`.*, `eqtype_jobs`.`set_id` as `pivot_set_id`,
`eqtype_jobs`.`job_id` as `pivot_job_id`,
`eqtype_jobs`.`created_at` as `pivot_created_at`,
`eqtype_jobs`.`updated_at` as `pivot_updated_at`,
`eqtype_jobs`.`id` as `pivot_id`
from `jobs`
inner join `eqtype_jobs` on `jobs`.`id` = `eqtype_jobs`.`job_id`
where `eqtype_jobs`.`set_id` = 56
order by `pivot_created_at` desc
limit 20
offset 0
in your blade try this:
$top->articles()->orderBy('pivot_range','asc')->get();
If you print out the SQL query of belongsToMany relationship, you will find that the column names of pivot tables are using the pivot_ prefix as a new alias.
For example, created_at, updated_at in pivot table have got pivot_created_at, pivot_updated_at aliases. So the orderBy method should use these aliases instead.
Here is an example of how you can do that.
class User {
...
public function posts(): BelongsToMany {
return $this->belongsToMany(
Post::class,
'post_user',
'user_id',
'post_id')
->withTimestamps()
->latest('pivot_created_at');
}
...
}
You can use orderBy instead of using latest method if you prefer. In the above example, post_user is pivot table, and you can see that the column name for ordering is now pivot_created_at or pivot_updated_at.
you can use this:
public function keywords() {
return $this->morphToMany(\App\Models\Keyword::class, "keywordable")->withPivot('order');
}
public function getKeywordOrderAttribute() {
return $this->keywords()->first()->pivot->order;
}
and append keyword attribiute to model after geting and use sortby
$courses->get()->append('keyword_order')->sortBy('keyword_order');

Categories