Computing with nested values from subqueries in Laravel - laravel

I'm struggling for days now with the following issue in Laravel:
I've got three models:
Equipment (has many batches)
Batch (belongs to equipment, and a location, and has many transactions)
Transaction (belongs to a batch)
In the batch model I have a scope to calculate the current quantity in that batch based on the transactions since the last transaction with the type "COUNT" (there are three types, COUNT, IN and OTU):
public function scopeWithCurrentQt($query)
{
$query->addSelect(['current_qt' => Transaction::selectRaw('SUM(hospital_transactions.qt) AS qt_sum')
->whereColumn('batch_id', '=', 'hospital_batches.id')
->where('issued_at','>=',function ($subquery) {
$subquery->select('hospital_transactions.issued_at')
->from('hospital_transactions')
->whereColumn('batch_id', '=', 'hospital_batches.id')
->where('hospital_transactions.type','=','COUNT')
->latest()
->take(1);
})
])->withCasts(['current_qt' => 'integer']);
}
Now I want the Equipment model to calculate the total quantities based on the current quantities from the batches. In the code you also see a division in various locations, that's because there should be a distinction between those as well.
Now this can be achieved pretty easily by appending an attribute to the model:
public function getQtActualAttribute(): object
{
$locations = $this->getHospitalStandardLocations();
$qt_inventory = $this->batches->whereNotIn('location_id',$locations->raft)
->whereNotIn('location_id',$locations->fak)
->sum('current_qt');
$qt_fak = $this->batches->whereIn('location_id',$locations->fak)
->sum('current_qt');
$qt_raft = $this->batches->whereIn('location_id',$locations->raft)
->sum('current_qt');
return (object) [
'check' => $locations->ignore_fak ? $qt_inventory : $qt_inventory + $qt_fak,
'inventory' => $qt_inventory,
'fak' => $qt_fak,
'raft' => $qt_raft,
'total' => $qt_inventory + $qt_fak + $qt_raft
];
}
But the difficulty is, I can't filter on these values, unless I'm loading all the models, which is not preferable. So I came to thing, let's add a scope with subqueries as well.
I have tried A LOT of options. The latest version I have is this:
public function scopeWithQts($query){
$locations = $this->getHospitalStandardLocations();
$query->addSelect([
'qt_inventory' => function ($query) use ($locations) {
$query->selectRaw('sum(current_qt)')
->from('hospital_batches')
->whereNotIn('location_id', $locations->raft)
->whereNotIn('location_id', $locations->fak)
->whereColumn('equipment_id', 'hospital_equipment.id');
},
'qt_fak' => function ($query) use ($locations) {
$query->selectRaw('sum(current_qt)')
->from('hospital_batches')
->whereIn('location_id', $locations->fak)
->whereColumn('equipment_id', 'hospital_equipment.id');
},
'qt_raft' => function ($query) use ($locations) {
$query->selectRaw('sum(current_qt)')
->from('hospital_batches')
->whereIn('location_id', $locations->raft)
->whereColumn('equipment_id', 'hospital_equipment.id');
}
]);
return $query;
}
But basically, all are resulting in the following error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'current_qt' in 'field list'
The function from the controller is the following:
$equipment = Equipment::with(['batches','batches.location'])
->orderByCode()
->withExpiryDates()
->withQts()
->when($expiryDates, function ($query) use ($expiringBefore){
$query->whereHas('batches', function($query) use ($expiringBefore){
$query->expiringBefore($expiringBefore);
});
});
Anybody a suggestion how to solve this? Thanks!

Related

Laravel multi nested withCount()

I have an Appointment Table which can have many appointments for a Property table.
Property has many appointments with specific type. Then a property can have many structures and each structure can have many payments.
I need to find sum of the payments for each structure. it works for the first nested relation.
Here is my code
$appointments = Appointment::whereUserId(auth()->id())
->where('appointment_type','evaluation')
->whereHas('property.structures', function ($qq) {
$qq->where('production_milestones','post_closing');
})
->with([
'property',
'property.structures',
'property.structures.payments',
'property.structures.estimates' => function ($query) {
return $query->where('is_signed', 1);
},
'property.structures.estimates.customizeProposal',
'property.structures.structureAssessment'
])
->withCount([
'payments as sum' => function($query) {
$query->select(DB::raw('SUM(amount)'));
}
])
->get();

Laravel 5 with eloquent relation callback function returning wrong records

Here is User Model
public function userpackages()
{
return $this->hasMany('App\UserPackages');
}
Trying to get users packages form specific month and year but it returning all records.
$users = User::with(['team', 'userpackages' => function($package) use($month,$year) {
$package->whereMonth('created_at', $month)->whereYear('created_at', $year);
}])->get();
Fetching
foreach ($users as $key => $user) {
$userpackages = $user->userpackages;
}
If I'm understanding correctly, you are filtering the eager load, but this does not affect the models returned. You need to repeat the filter using whereHas() to limit the models that are returned. In addition, functions like whereDate() can be very inefficient, especially if the column is indexed. I suggest using whereBetween() when searching a date range.
$date = Carbon::createFromFormat("Y-m", "$year-$month");
$range = [$date->startOfMonth(), $date->endOfMonth()];
$users = User::with('team')
->with(['userpackages' => fn ($q) => $q->whereBetween('created_at', $range)])
->whereHas('userpackages', fn ($q) => $q->whereBetween('created_at', $range)
->get();
To explain further:
User::with('userpackages') returns all users, each with all of their packages.
User::with(['userpackages' => 'some condition']) returns all users, each with some of their packages
User::whereHas('userpackages', 'some condition') returns some users, each with all of their packages
User::(['userpackages' => 'some condition'])->whereHas('userpackages', 'some condition') returns some users, each with some of their packages.

Laravel - Get array with relationship

I have an ajax call that returns an array:
$reports = Report::where('submission_id', $submissionID)
->where('status', 'pending')
->get(['description','rule']);
return [
'message' => 'Success.',
'reports' => $reports,
];
From this array, I only want to return the fields 'description' and 'rule'. However I also want to return the owner() relationship from the Report model. How could I do this? Do I have to load the relationship and do some kind of array push, or is there a more elegant solution?
You can use with() to eager load related model
$reports = Report::with('owner')
->where('submission_id', $submissionID)
->where('status', 'pending')
->get(['id','description','rule']);
Note you need to include id in get() from report model to map (owner) related model
you will have probably one to many relationship with Reports and owners table like below
Report Model
public function owner() {
return $this->belongsTo('App\Owner');
}
Owner Model
public function reports() {
return $this->hasMany('App\Report');
}
your controller code
$reports = Report::with('owner')->
where('submission_id', $submissionID)->where('status', 'pending')->get()
return [
'message' => 'Success.',
'reports' => $reports,
];
This is what I ended up going with:
$reports = Report::
with(['owner' => function($q)
{
$q->select('username', 'id');
}])
->where('submission_id', $submissionID)
->where('status', 'pending')
->select('description', 'rule','created_by')
->get();
The other answers were right, I needed to load in the ID of the user. But I had to use a function for it to work.

Laravel filter relation's data by other relations data

I am trying to filter my tickets.tips.drawDates relation's data by other relation's column (results.draw_date) in an eager-loading query. Does anybody have any advice on how to accomplish that?
$products = Product::with([
'results' => function ($query) use ($drawDates) {
return $query->whereBetween('draw_date', $drawDates);
},
'tickets' => function ($query) use ($drawDateFrom) {
return $query->whereDate('valid_until', '>=', $drawDateFrom)->where('status', 'pending');
},
'tickets.tips',
'tickets.tips.drawDates' => function($query) {
return $query->whereNull('status')->whereDate('draw_date', 'HERE SHOULD BE draw_date COLUMN FROM results RELATION');
},
'prizes'
])->get();
You could try with the whereColumn() function, it is used to verify that two columns are equal, something like that:
return $query->whereNull('status')->whereColumn('draw_date', 'results.draw_date');

ActiveRecord where and order on via-table

I have three database table:
product (id, name)
product_has_adv (product,advantage,sort,important)
advantage (id, text)
In ProductModel I defined this:
public function getAdvantages()
{
return $this->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
->viaTable('product_has_advantage', ['product' => 'id']);
}
I get the advantages without any problems.
But now I need to add a where product_has_advantage.important = 1 clausel and also sort the advantages by the sort-columen in the product_has_advantage-table.
How and where I have to realize it?
Using via and viaTable methods with relations will cause two separate queries.
You can specify callable in third parameter like this:
public function getAdvantages()
{
return $this->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
->viaTable('product_has_advantage', ['product' => 'id'], function ($query) {
/* #var $query \yii\db\ActiveQuery */
$query->andWhere(['important' => 1])
->orderBy(['sort' => SORT_DESC]);
});
}
The filter by important will be applied, but the sort won't since it happens in first query. As a result the order of ids in IN statement will be changed.
Depending on your database logic maybe it's better to move important and sort columns to advantage table.
Then just add condition and sort to the existing method chain:
public function getAdvantages()
{
return $this->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
->viaTable('product_has_advantage', ['product' => 'id'])
->andWhere(['important' => 1])
->orderBy(['sort' => SORT_DESC]);
}
Using viaTable methods with relations will cause two separate queries, but if you don't need link() method you can use innerJoin in the following way to sort by product_has_advantage table:
public function getAdvantages()
{
$query = AdvantageModel::find();
$query->multiple = true;
$query->innerJoin('product_has_advantage','product_has_advantage.advantage = advantage.id');
$query->andWhere(['product_has_advantage.product' => $this->id, 'product_has_advantage.important' => 1]);
$query->orderBy(['product_has_advantage.sort' => SORT_DESC]);
return $query;
}
Note than $query->multiple = true allows you to use this method as Yii2 hasMany relation.
Just for reference https://github.com/yiisoft/yii2/issues/10174
It's near impossible to ORDER BY viaTable() columns.
For Yii 2.0.7 it returns set of ID's from viaTable() query,
and final/top query IN() clause ignores the order.
For who comes here after a while and don't like above solutions, I got it working by joining back to the via table after the filter via table.
Example for above code:
public function getAdvantages()
{
return $this->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
->viaTable('product_has_advantage', ['product' => 'id'])
->innerJoin('product_has_advantage','XXX')
->orderBy('product_has_advantage.YYY'=> SORT_ASC);
}
Take care about changing XXX with the right join path and YYY with the right sort column.
First you need to create a model named ProductHasAdv for junction table (product_has_adv) using CRUD.
Then create relation in product model and sort it:
public function getAdvRels()
{
return $this->hasMany(ProductHasAdv::className(), ['product' => 'id'])->
orderBy(['sort' => SORT_ASC]);;
}
Then create second relationship like this:
public function getAdvantages()
{
$adv_ids = [];
foreach ($this->advRels as $adv_rel)
$adv_ids[] = $adv_rel->advantage;
return $this->hasMany(Advantage::className(), ['id' => 'advantage'])->viaTable('product_has_adv', ['product' => 'id'])->orderBy([new Expression('FIELD (id, ' . implode(',', $adv_ids) . ')')]);
}
This will sort final result using order by FIELD technique.
Don't forget to add:
use yii\db\Expression;
line to head.
I`ve managed this some how... but it needs additional work after.
The point is that you have to query many-to-many relation first from source model and after that inside that closure you should query your target model.
$query = Product::find();
$query->joinWith([
'product_has_adv' => function ($query)
{
$query->alias('pha');
$query->orderBy('pha.sort ASC');
$query->joinWith(['advantage ' => function ($query){
$query->select([
'a.id',
'a.text',
]);
$query->alias('a');
}]);
},
]);
Then you just have to prettify the sorted result to your needs.
The result for each row would look like
"product_has_adv": [
{
"product": "875",
"advantage": "true",
"sort": "0",
"important": "1",
"advantage ": {
"id": "875",
"text": "Some text..",
}
},
As explained by #arogachev, the viaTable uses two separate queries, which renders any intermediate orderBy obsolete
You could replace the viaTable with an innerJoin as follows, in a similar solution to #MartinM
public function getAdvantages()
{
return $this->hasMany(AdvantageModel::class, ['pha.product' => 'id'])
->innerJoin('product_has_advantage pha', 'pha.advantage = advantage.id')
->andWhere(['pha.important' => 1])
->orderBy(['pha.sort' => SORT_ASC]);
}
By adjusting the result of hasMany, you are adjusting the query for the target class - AdvantageModel::find(); product_has_advantage can be joined via the advantage identity
The second parameter of hasMany, link, can be viewed as [ query.column => $this->attribute ], which you can now support via the joined product_has_advantage and its product identity
Note, when using viaTable, the link parameter can be viewed as if the intermediate query is complete and we are starting from there; [ query.column => viaTable.column ]
hence ['id', 'advantage'] in your question
public function getAdvantages()
{
return $this
->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
->viaTable('product_has_advantage', ['product' => 'id'])
->andWhere(['important' => 1])
->orderBy(['sort' => SORT_DESC]);
}

Resources