laravel collection nested query - laravel

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

Related

Laravel SortBy Collection

I have an eloquent query, 'status' is a custom column created by $collection->filter,
then I have an array with custom fields.
Collection SortByDesc
$collection = $collection->sortByDesc(function($contacts) {
$priority = $this->priority($contacts->status);
$calendar_date = $leads->calendar;
return [
$priority, <-- order it's ok
$calendar_date <-- Here need Calendar by ASC
];
})->values();
Array
public function priority($status)
{
return array_search($status, array_reverse(
[
'closed',
'waiting',
'danger'
]));
}
How can I have an alternate sort by desc and asc?

Laravel OrderBy Errors with 5.8 Upgrade

Having trouble with Laravel 5.8 because the orderBy commands stopped working. How would I rewrite these two areas of a controller so it stops throwing an error?
public function index(Request $request)
{
$clients = Client::orderBy('identifier', 'name')->paginate(15);
return view('admin.clients.index')->with('clients', $clients);
}
public function closed()
{
$sortBy = 'name';
$query = Client::onlyTrashed()->orderBy($sortBy, $sortBy == 'created_at' ? 'name' : 'asc')
->orderBy('created_at', 'name');
$projects = $query->get();
return view('admin.clients.index')->with(['clients' => $projects, 'sortBy' => $sortBy]);
}
orderBy(string $column, string $direction = 'asc')
orderBy has two parameters, the second one is direction.
Before version 5.8, if the direction value you passed is not asc, it will automatically set desc as direction value:
$this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
'column' => $column,
'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc',
];
After Laravel 5.8+, if the direction value is not asc or desc, it will show the error:
if (! in_array($direction, ['asc', 'desc'], true)) {
throw new InvalidArgumentException('Order direction must be "asc" or "desc".');
}
So if you want to order by two column, you can use two orderBy:
$clients = Client::orderBy('identifier')->orderBy('name')->paginate(15);
# Raw SQL:
# select * from clients order by identifier asc, name asc

Pluck with multiple columns?

When i use pluck with multiple columns i get this:
{"Kreis 1 \/ Altstadt":"City","Kreis 2":"Enge","Kreis 3":"Sihifeld","Kreis 4":"Hard","Kreis 5 \/ Industriequartier":"Escher Wyss","Kreis 6":"Oberstrass","Kreis 7":"Witikon","Kreis 8 \/ Reisbach":"Weinegg","Kreis 9":"Altstetten","Kreis 10":"Wipkingen","Kreis 11":"Seebach","Kreis 12 \/ Schwamendingen":"Hirzenbach"
But i need this?
["Rathaus","Hochschulen","Lindenhof","City","Wollishofen","Leimbach","Enge","Alt-Wiedikon","Friesenberg","Sihifeld","Werd","Langstrasse","Hard","Gewerbechule","Escher Wyss","Unterstrass","Oberstrass","Fluntern","Hottingen","Hirslanden","Witikon","Seefeld","M\u00fchlebach","Weinegg","Albisrieden","Altstetten","H\u00f6ngg","Wipkingen","Affoltern","Oerlikon","Seebach","Saatlen","Schwamendingen-Mitte","Hirzenbach"]
Any suggestion how can i do that? This is my method:
public function autocomplete_districts(Request $request)
{
$district = $request->input('query');
// $ass = /DB::table('districts')->select(array('district', 'region'))->get();
// dd($ass);
$data = Districts::whereRaw('LOWER(district) like ?', [strtolower('%'.$district . '%')])->orWhereRaw('LOWER(region) like ?', [strtolower('%'.$district . '%')])->pluck('region','district');
return response()->json($data);
}
You should use select() with get() and then later on modify the object as you need.
So instead of: ->pluck('region','district');
use: ->select('region','district')->get();
pluck() is advised when you need value of one column only.
And as far as possible, you should have your models singular form not plural (Districts) - to follow Laravel nomenclature.
Cos that is how pluck works. Instead try this.
$data = Districts::whereRaw('LOWER(district) like ?', [strtolower('%'.$district . '%')])->orWhereRaw('LOWER(region) like ?', [strtolower('%'.$district . '%')])->select('region', 'district')->get();
$data = collect($data->toArray())->flatten()->all();
In my case I wanted to pluck 2 values from an array of Eloquent models and this worked:
$models->map->only(['state', 'note'])->values()
That's shorter version of
$models->map(fn($model) => $model->only(['state', 'note']))->values()
This is an issue I constantly have faced and has led me to create the following solution that can be used on models or arrays.
There is also support for dot syntax that will create a multidimensional array as required.
Register this macro within the AppServiceProvider (or any provider of your choice):
use Illuminate\Support\Arr;
/**
* Similar to pluck, with the exception that it can 'pluck' more than one column.
* This method can be used on either Eloquent models or arrays.
* #param string|array $cols Set the columns to be selected.
* #return Collection A new collection consisting of only the specified columns.
*/
Collection::macro('pick', function ($cols = ['*']) {
$cols = is_array($cols) ? $cols : func_get_args();
$obj = clone $this;
// Just return the entire collection if the asterisk is found.
if (in_array('*', $cols)) {
return $this;
}
return $obj->transform(function ($value) use ($cols) {
$ret = [];
foreach ($cols as $col) {
// This will enable us to treat the column as a if it is a
// database query in order to rename our column.
$name = $col;
if (preg_match('/(.*) as (.*)/i', $col, $matches)) {
$col = $matches[1];
$name = $matches[2];
}
// If we use the asterisk then it will assign that as a key,
// but that is almost certainly **not** what the user
// intends to do.
$name = str_replace('.*.', '.', $name);
// We do it this way so that we can utilise the dot notation
// to set and get the data.
Arr::set($ret, $name, data_get($value, $col));
}
return $ret;
});
});
This can then be used in the following way:
$a = collect([
['first' => 1, 'second' => 2, 'third' => 3],
['first' => 1, 'second' => 2, 'third' => 3]
]);
$b = $a->pick('first', 'third'); // returns [['first' => 1, 'third' => 3], ['first' => 1, 'third' => 3]]
Or additionally, on any models you may have:
$users = User::all();
$new = $users->pick('name', 'username', 'email');
// Might return something like:
// [
// ['name' => 'John Doe', 'username' => 'john', 'email' => 'john#email.com'],
// ['name' => 'Jane Doe', 'username' => 'jane', 'email' => 'jane#email.com'],
// ['name' => 'Joe Bloggs', 'username' => 'joe', 'email' => 'joe#email.com'],
// ]
It is also possible to reference any relationship too using the dot notation, as well as using the as [other name] syntax:
$users = User::all();
$new = $users->pick('name as fullname', 'email', 'posts.comments');
// Might return something like:
// [
// ['fullname' => 'John Doe', 'email' => 'john#email.com', 'posts' => [...]],
// ['fullname' => 'Jane Doe', 'email' => 'jane#email.com', 'posts' => [...]],
// ['fullname' => 'Joe Bloggs', 'email' => 'joe#email.com', 'posts' => [...]],
// ]
My solution in LARAVEL 5.6:
Hi, I've just had the same problem, where I needed 2 columns combined in 1 select list.
My DB has 2 columns for Users: first_name and last_name.
I need a select box, with the users full name visible and the id as value.
This is how I fixed it, using the pluck() method:
In the User model I created a full name accessor function:
public function getNameAttribute() {
return ucwords($this->last_name . ' ' . $this->first_name);
}
After that, to fill the select list with the full name & corresponding database id as value, I used this code in my controller that returns the view (without showing users that are archived, but you can change the begin of the query if you like, most important are get() and pluck() functions:
$users = User::whereNull('archived_at')
->orderBy('last_name')
->get(['id','first_name','last_name'])
->pluck('name','id');
return view('your.view', compact('users'));
Now you can use the $users in your select list!
So first, you GET all the values from DB that you will need,
after that you can use any accessor attribute defined for use in your PLUCK method,
as long as all columns needed for the accessor are in the GET ;-)
As far as now Laravel didn't provide such macro to pick specific columns, but anyway Laravel is out of the box and lets us customize almost everything.
Tested in Laravel 8.x
in AppServiceProvider.php
use Illuminate\Support\Collection;
// Put this inside boot() function
Collection::macro('pick', function (... $columns) {
return $this->map(function ($item, $key) use ($columns) {
$data = [];
foreach ($columns as $column) {
$data[$column] = $item[$column] ?? null;
}
return $data;
});
});
Usage
$users = App\Models\User::all();
$users->pick('id','name');
// Returns: [['id' => 1, 'name' => 'user_one'],['id' => 2, 'name' => 'user_two']]
Important notes:
Do not use this macro for a really HUGE collection (You better do it on Eloquent
select or MySQL query select)
Laravel: To pluck multi-columns in the separate arrays use the following code.
$Ads=Ads::where('status',1);
$Ads=$Ads->where('created_at','>',Carbon::now()->subDays(30));
$activeAdsIds=$Ads->pluck('id'); // array of ads ids
$UserId=$Ads->pluck('user_id'); // array of users ids
I have created the model scope
More about scopes:
https://laravel.com/docs/5.8/eloquent#query-scopes
https://medium.com/#janaksan_/using-scope-with-laravel-7c80dd6a2c3d
Code:
/**
* Scope a query to Pluck The Multiple Columns
*
* This is Used to Pluck the multiple Columns in the table based
* on the existing query builder instance
*
* #author Manojkiran.A <manojkiran10031998#gmail.com>
* #version 0.0.2
* #param \Illuminate\Database\Eloquent\Builder $query
* #param string $keyColumn the columns Which is used to set the key of array
* #param array $extraFields the list of columns that need to plucked in the table
* #return \Illuminate\Support\Collection
* #throws Illuminate\Database\QueryException
**/
public function scopePluckMultiple( $query, string $keyColumn, array $extraFields):\Illuminate\Support\Collection
{
//pluck all the id based on the query builder instance class
$keyColumnPluck = $query->pluck( $keyColumn)->toArray();
//anonymous callback method to iterate over the each fileds of table
$callBcakMethod = function ($eachValue) use ($query)
{
$eachQuery[$eachValue] = $query->pluck( $eachValue)->toArray();
return $eachQuery;
};
//now we are collapsing the array single time to get the propered array
$extraFields = \Illuminate\Support\Arr::collapse( array_map($callBcakMethod, $extraFields));
// //iterating Through All Other Fields and Plucking it each Time
// foreach ((array)$extraFields as $eachField) {
// $extraFields[$eachField] = $query->pluck($eachField)->toArray();
// }
//now we are done with plucking the Required Columns
//we need to map all the values to each key
//get all the keys of extra fields and sets as array key or index
$arrayKeys = array_keys($extraFields);
//get all the extra fields array and mapping it to each key
$arrayValues = array_map(
function ($value) use ($arrayKeys) {
return array_combine($arrayKeys, $value);
},
call_user_func_array('array_map', array_merge(
array(function () {
return func_get_args();
}),
$extraFields
))
);
//now we are done with the array now Convert it to Collection
return collect( array_combine( $keyColumnPluck, $arrayValues));
}
So now the testing part
BASIC EXAMPLE
$basicPluck = Model::pluckMultiple('primaryKeyFiles',['fieldOne', 'FieldTwo']);
ADVANCED EXAMPLE
$advancedPlcuk = Model::whereBetween('column',[10,43])
->orWhere('columnName','LIKE', '%whildCard%')
->Where( 'columnName', 'NOT LIKE', '%whildCard%')
->pluckMultiple('primaryKeyFiles',['fieldOne', 'FieldTwo']);
But it returns the \Illuminate\Support\Collection, so if you need to convert to array
$toArrayColl = $advancedPluck->toArray();
if you need to convert to json
$toJsonColl = $advancedPluck->toJson();
To answer the specific question of "how to return multiple columns using (something like) pluck" we have to remember that Pluck is a Collection member function. So if we're sticking to the question being asked we should stick with a Collection based answer (you may find it more beneficial to develop a model-based solution, but that doesn't help solve the question as posed).
The Collection class offers the "map" member function which can solve the posed question:
$data = Districts::whereRaw('LOWER(district) like ?', [strtolower('%'.$district . '%')])->orWhereRaw('LOWER(region) like ?', [strtolower('%'.$district . '%')])
->map(function ($item, $key, $columns=['region','district']) {
$itemArray = [];
foreach($columns as $column){
$itemArray[$column] = $item->$column;
}
return ($itemArray);
});
dd($data);
This should give you a collection where each element is a 2 element array indexed by 'region' and 'district'.
Laravel 8.x, try to use mapWithKeys method instead of pluck, for example:
$collection->mapWithKeys(function ($item, $key) {
return [$key => $item['firstkey'] . ' ' . $item['secondkey']];
});
Expanding on #Robby_Alvian_Jaya_Mulia from above who gave me the idea. I needed it to also work on a relationship. This is just for a single relationship, but it would probably be easy to nest it more.
This needs to be put into AppServiceProvider.php
use Illuminate\Support\Collection;
// Put this inside boot() function
Collection::macro('pick', function (... $columns) {
return $this->map(function ($item, $key) use ($columns) {
$data = [];
foreach ($columns as $column) {
$collection_pieces = explode('.', $column);
if (count($collection_pieces) == 2) {
$data[$collection_pieces[1]] = $item->{$collection_pieces[0]}->{$collection_pieces[1]} ?? null;
} else {
$data[$column] = $item[$column] ?? null;
}
}
return $data;
});
});
Usage:
$users = App\Models\User::has('role')->with('role')->all();
$users->pick('id','role.name');
// Returns: [['id' => 1, 'name' => 'role_name_one'],['id' => 2, 'name' => 'role_name_two']]
Hope this is helpful to someone. Sorry I didn't add this to under #Robby's answer. I didn't have enough reputation.
Pluck returned only the value of the two columns which wasnt ideal for me, what worked for me was this :
$collection->map->only(['key1', 'key2'])->values()

Cakephp: How to get 2nd level conditional contain without intermediate entities

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();

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