Cakephp: How to get 2nd level conditional contain without intermediate entities - cakephp-3.x

I execute the following request:
$agPoi = $this->Agpois->get($agPoiId, [
'contain' => [
'Agpoititles.Agthemelanguages' => function ($q) use ($languageId) {
return $q->where([
'language_id' => $languageId,
]);
}
]
]);
Surprisingly, it returns me all agpoititles with only one containing a not empty aghtemelanguage object for which 'language_id' => $languageId.
But I'm not interested by agpoititles not containing an agthemelanguage object.
How to modify the request to only the ones I want?
EDIT
Agpois hasMany Agpoititles
AgpoiTitles belongsTo Agthemelanguages
Agthemelanguages hasOne Agpoititles
SO I expect to get agPoi with THE Agpoititles for which its agthemelanguage->language_id = $languageId
Edit 2
So I tried to use matching() and it works if you pay attention to look for the result in _matchingData as it's explained in the doc ;).
$agPoi = $this->Agpois->find()
->contain(['Agpoititles.Agthemelanguages'])
->matching('Agpoititles.Agthemelanguages', function ($q) use ($languageId, $poiId) {
return $q->where([
'Agpois.id' => $poiId,
'language_id' => $languageId
]);
}
)
->first();

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 change result of laravel relationship

I have an one to one relationship in laravel same as following:
public function Category()
{
return $this->belongsTo(Categories::class);
}
and run this eloquent query:
Product::with('category')->first();
this query return:
{
"name": "Green Book",
"category": {
"id": 1,
"name": "Book"
}
}
but I went this data:
{
"name": "Green Book",
"category": "Book"
}
Is it possible to do this without using a loop?
First of all it seems like you have a 1-X relationship which you are mistakenly using as a many to one. Your belongsTo should be hasOne since the your items have one category not the other way around.
You can use the $appends property to append a custom field and make it behave as though it's part of your model:
Rename your relationship and add a mutator and accessor:
public function categoryRelationship()
{
return $this->hasOne(Categories::class);
}
public function getCategoryAttribute() {
return $this->categoryRelationship->name;
}
public function setCategoryAttribute($value) {
$this->categoryRelationship->name = $value;
}
You can also choose to add an event to automatically save your relationship when the model is being saved to ensure it works transparently:
protected static function booted()
{
static::saving(function ($myModel) {
$myModel->categoryRelationship->save();
});
}
}
Finally you add the $appends property to ensure your new attribute is alwasy included in the model as though it's a native one.
protected $appends = [ 'category' ];
// This is so you don't end up also showing the relationship
protected $hidden = [ 'categoryRelationship' ];
You can use leftJoin
return \App\Product::leftJoin('categories', 'products.category_id', '=', 'categories.id')
->select('products.*', 'categories.title as category')->get();
According to Laravel Doc Eager Loading Specific Columns
You may use the following
Product::with('category:id,name')->first();
Or you may do it yourself :
$product = Product::with('category')->first();
$product->category = $product->category->name;
I get same result
so, i tried but didn't get right result.
how to change the result of laravel eloquent relation.
I found one way and used transformer.
we can change the result in transformer.
$result = Event::with(['eventType'])
->where('id', $id)
->where('user_id', auth()->user()->id)
->first();
return $this->trans->transform($result);
this is the result and i used transformer as follow.
public function transform(Event $Event)
{
return [
'id' => $Event->id,
'company_id' => $Event->company_id,
'calendar_id' => $Event->calendar_id,
'case_id' => $Event->case_id,
'user_id' => $Event->user_id,
'title' => $Event->title,
'description' => $Event->description,
'duration' => $Event->duration,
'alert_at' => $Event->alert_at,
'alert_email' => $Event->alert_email,
'email_sent' => $Event->email_sent,
'alert_popup' => $Event->alert_popup,
'popup_triggered' => $Event->popup_triggered,
'created_by' => $Event->created_by,
'completed' => $Event->completed,
'alert_offset' => $Event->alert_offset,
'icon' => $Event->icon,
'color' => $Event->color,
'snoozed' => $Event->snoozed,
'type_id' => $Event->type_id,
'datetime' => $Event->at,
'endtime' => $Event->end_time,
'event_type_name'=>$Event->eventType->name,
];
}
then, we can get right result. Focuse on 'event_type_name'=>$Event->eventType->name,
But i have one problem yet.
now, the result is only 1 row. it is just first().
but if i use get(), i can't use transformer.
of course, i can loop the result using foreach().
but i think it is not right way.
what is better way?
Please answer.

How to manipulate a Laravel collection with related models and return a customized instance?

My model relation
return $this->hasMany('App\Models\Opportunity')->with('user');
My Attempt
$project = Project::find(1);
$$opportunities = $project->opportunities
->where('status', "confirmed");
$opportunities->each(function ($opportunity) {
return $opportunity->get('user');
});
Goal
My goal is to return the data in the following structure:
Opportunities:
Opportunity:
Status,
Amount
Currency
Name
Note that the user is a subset of the opportunity itself.
Problem
This returns a 1024 SQL error.
Ideally
It would be ideal if I can return all this information with the query itself.
Call get() method on your query to get its results first:
$oppurtunities = $project->opportunities()
->where('status', "confirmed")
->get();
You have eager loaded the user instance for each opportunity so just call $opportunity->user to return each opportunity's user:
$project = Project::find(1);
$opportunities = $project
->opportunities()
->where('status', "confirmed")
->get();
$filtered = $opportunities->map(function ($opportunity) {
return [
'status' => $opportunity->status,
'amount' => $opportunity->amount_pledged,
'currency' => $opportunity->currency,
'name' => optional($opportunity->user)->full_name
];
})->all();

laravel collection nested query

I'm new with laravel collection . i just want to make this query with collection
DB::select(SELECT id, user_id, created_at,latitude,longitude
FROM coordinates
WHERE created_at IN (
SELECT MAX(created_at)
FROM coordinates
GROUP BY user_id));
according to the document https://laravel.com/docs/6.x/collections
this what i did and it gives me all the data
$this->data['information'] = collect($response->json()['items']);
Edit:
simply i need to produce that query (that i mentioned above) by using Collection.
This is the contents of $response->json()['items']
"#items: array:17 [▼ 0 => array:9 [▼ "id" => 977777779 "platform_id" => "msaeed" "platform_type" => 1 "status" => 1 "longitude" => 1.1 "latitude" => 1.1 "created_by" => "NMF" "created_at" => "2019-10-27T21:00:00Z" "updated_at" => "2019-10-28T07:58:36Z""
any help would be appreciated
Here is the query.
$data = collect($response->json()['items']);
$data = $data->whereIn('created_at', $data->sortByDesc('created_at')
->groupBy('user_id')
->pluck('0.created_at'))
->onlyKeys(['id', 'user_id', 'created_at', 'latitude', 'longitude']);
The equivalent for the subquery:
SELECT MAX(created_at)
FROM coordinates
GROUP BY user_id
is
$data->sortByDesc('created_at') // we use sortByDesc to produce the same result as `MAX`.
->groupBy('user_id')
->pluck('0.created_at') // then to get the max value, just get the first array after grouping.
To achieve the select statement, we need to create a little macro, i've named it onlyKeys:
use Illuminate\Support\Arr;
Collection::macro('onlyKeys', function ($keys) {
return $this->map(function ($value) use ($keys) {
$tmpValue = (array) $value;
if (is_array($tmpValue)) {
$value = Arr::only($tmpValue, $keys);
}
return $value;
});
});
You can register the macro in your AppServiceProvider.php.
PS. Why you need to querying using Collection? Is that data not come from DB?
You want to collect array recursive
Unfortunately laravel does not have this feature:
you can write it on your own.
if you have helpers file you can write this method:
function recursive_collect($array){
foreach ($array as $key => $value) {
if (is_array($value)) {
$value = recursive_collect($value);
$array[$key] = $value;
}
}
return collect($array);
}
Or You can implement macro in your AppServiceProvider:
\Illuminate\Support\Collection::macro('recursive', function () {
return $this->map(function ($value) {
if (is_array($value) || is_object($value)) {
return collect($value)->recursive();
}
return $value;
});
});
Hope this helps you

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