Yii2: Join condition parameters are null when using "with" - activerecord

I have a many to many relation:
Parent(id) <- Link(parent,child,sort) -> Product(sku)
I want to get a sorted list of Products from the Parent model
On the parent model I have a couple of functions:
public function getLinksDown()
{
return $this->hasMany( DBLink::className(), ['parent' => 'id'])
->from(['linksDown' => DBLink::tableName()]);
}
public function getProducts()
{
return $this->hasMany(DBProduct::className(), ['sku' => 'child'])
->from(['products' => DBProduct::tableName()])
->via('linksDown')
->innerJoin(
['linx' => DBLink::tableName()],
'linx.child = products.sku AND linx.parent=:parent',
[':parent' => $this->id]
)
->orderBy(['linx.order' => SORT_ASC]);
}
If I call the relation directly it works fine.
$model = Parent::findOne(1234);
$products = $model->products;
Products is a list of products sorted correctly.
If the relation is called as a part of a "with" it fails, $this->id will be null which means no products are returned.
Parent::find()->with('products')->all();

After much head scratching, I have decided the only solution, is to abandon the Many to Many relation where I need a sorted list of products, and do something like this:
$model = Parent::find()->where(['id' => 1234])->with('linksDown.products')->one();
and reference each product through the linksDown relation, instead of directly from the parent model.
foreach( $model->linksDown as $link ) {
dosomethingwith($link->product);
}
In order to make this work the sort will need to be applied to the linksDown relation:
public function getLinksDown()
{
return $this->hasMany( DBLink::className(), ['parent' => 'id'])
->from(['linksDown' => DBLink::tableName()])
->orderBy([linksDown.order => SORT_ASC]);
}

Related

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.

How to parse an array within a query builder's get()

I have my Country model and a State Model. Only countries that have states can show states, or return [];
I would like to return this in one hit into my response object, yet I can't wrap my head around getting the id of the current country that is running in the query, I built this just now:
return response()->responseObject([
'code' => 200,
'status' => true,
'message' => 'Retrieved List of Available Countries',
'data' => Country::where('allow_registration', Country::REGISTRATION_ALLOWED)->get([
'id',
'name',
'iso',
'iso3',
'prefix',
'states' => function($query){}
])
]);
And I have another function waiting for this:
public static function getStates($id)
{
$states = State::where('country_id', $id);
return $states ? $states : [];
}
Am I approaching this right? What do I have to put in my sub query function in order for current country ID? Or did I approach this wrong and there is a more eloquent way of implementing?
in your country model create states relation
$this->hasMany(State::class);
then you can get all countries with its states by that :
Country::with('states')->get();

Laravel 5.4 How to update pivot table?

I have next structure pivot table: products_equipment_value(id, product_id, equipment_id, value_id).How to update table fields equipment_id and value_id ?
public function equipments()
{
return $this->belongsToMany('App\Equipment', ' product_equipment_value');
}
public function values()
{
return $this->belongsToMany('App\Value', ' product_equipment_value', 'product_id', 'value_id')
}
Use(not work)
$product->equipments()->detach();
foreach($data['eq'] as $key => $val){
$product->equipments()->attach([
'equipment_id' => $key,
'value_id' =>$val
]);
}
You should use withPivot function on the relations.
public function equipments()
{
return $this->belongsToMany('App\Equipment', 'product_equipment_value')->withPivot('value_id');
}
Then when you attach the models
$product->equipments()
->attach([
$key => ['value_id' =>$val],
]);
Referring to Many to Many Eloquent's relationship (https://laravel.com/docs/5.5/eloquent-relationships#many-to-many) you can sync IDs like this :
$product->equipments()->sync([array_of_equipments_ids]);
Example:
$product->equipments()->sync([3, 8, 9, 24]);
If you defined the Many to Many inverse relationship you can also do this that way from the equipment instance :
$equipment->values()->sync([array_of_values_ids]);
If you have extra columns with your pivot table you can add them like this :
public function equipments(){
return $this->belongsToMany('App\Equipment', 'product_equipment_value')->withPivot('extra_column1');
}
And so :
$product->equipments()->sync([1 => ['extra_column1' => 'Value for this column']])
Note that you can use sync() or attach() methods to construct many-to-many associations. You can take a look here : https://stackoverflow.com/a/23969879/8620746

UpdateExistingPivot for multiple ids

In order to update single record in pivot table I use updateExistingPivot method. However it takes $id as the first argument. For example:
$step->contacts()->updateExistingPivot($id, [
'completed' => true,
'run_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
But how can I update multiple existing rows in pivot table at once?
There's an allRelatedIds() method in the BelongsToMany relation that you can access, which will return a Collection of the related model's ids that appear in the pivot table against the initial model.
Then a foreach will do the job:
$ids = $step->contacts()->allRelatedIds();
foreach ($ids as $id){
$step->contacts()->updateExistingPivot($id, ['completed' => true]);
}
You can update only by using a looping statement as there updateExistingPivot function only accept one dimensional params, See the core function for laravel 5.3.
File: yoursite\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Relations\BelongsToMany.php
Function: updateExistingPivot
public function updateExistingPivot($id, array $attributes, $touch = true)
{
if (in_array($this->updatedAt(), $this->pivotColumns)) {
$attributes = $this->setTimestampsOnAttach($attributes, true);
}
$updated = $this->newPivotStatementForId($id)->update($attributes);
if ($touch) {
$this->touchIfTouching();
}
return $updated;
}
So, You should follow the simple process:
$step = Step::find($stepId);
foreach(yourDataList as $youData){
$step->contacts()->updateExistingPivot($youData->contract_id, [
'completed' => true,
'run_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
}

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