Override eloquent relationship result data - laravel

I use Laravel-Metable package in my project. This package return collection object using in key meta name and on value eloquent object.
Here you can see package data result screenshot.
How I can override result data and get this type of array data:
$meta = [
[
'id' => 1,
'key' => "Meta Name",
'value' => "Meta Value"
],
[
'id' => 2,
'key' => "Meta Name",
'value' => "Meta Value"
],
];
I will load my models meta with lazy loading:
use Metable;
protected $with = ['meta'];

You can use the collection map method for that so it should be something like this:
$result = $metaItems->map(function($meta) {
return [
'id' => $meta->id,
'key' => $meta->key,
'value' => $meta->value
];
})->values();
// then $result->toArray(); should give you the expected result

Related

Laravel Elasticsearch JSON Mapping Issue

I'm currently using Laravel v7.2, have the babenkoivan/scout-elasticsearch-driver installed (4.2) and am using AWS Elasticsearch 7.1. I have several tables mapped in my application that are working fine but am having issues with a nested mapping that was previously working and is now broken.
I'm saving data into a table and having that table data copied into AWS Elasticsearch. I'm using MySQL 5.6 so I am using a TEXT column to store JSON data. Data in the table looks as follows:
'id' => 1,
'results' => [{"finish":1,"other_id":1,"other_data":1}]
I have my model setup with the following mapping:
protected $mapping = [
'properties' => [
'results' => [
'type' => 'nested',
'properties' => [
'finish' => [
'type' => 'integer'
],
'other_id' => [
'type' => 'integer'
],
'other_data' => [
'type' => 'integer'
]
]
],
]
];
And if it's of any use, the toSearchableArray:
public function toSearchableArray()
{
$array = [
'id' => $this->id,
'results' => $this->results
];
return $array;
}
I have no problem creating this index and it worked up until about a couple of months ago. I don't know exactly when, as it wasn't a high priority item and may have occurred around an AWS ES update but not sure why this in particular would break. I receive the following error now:
{"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"object mapping for [results] tried to parse field [results] as object, but found a concrete value"}],"type":"mapper_parsing_exception","reason":"object mapping for [results] tried to parse field [results] as object, but found a concrete value"},"status":400}
I've tried also storing the data in the table as such, thinking it was breaking due to the potential array, but it was to no avail:
'id' => 1,
'results' => {"finish":1,"other_id":1,"other_data":1}
I'm at a loss for what else to try to get this working again.
EDIT: Here is the entire model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use ScoutElastic\Searchable;
class ResultsModel extends Model
{
use Searchable;
protected $indexConfigurator = \App\MyIndexConfiguratorResults::class;
protected $searchRules = [
//
];
protected $mapping = [
'properties' => [
'results' => [
'type' => 'nested',
'properties' => [
'finish' => [
'type' => 'integer'
],
'other_id' => [
'type' => 'integer'
],
'other_data' => [
'type' => 'integer'
]
]
],
]
];
public function searchableAs()
{
return 'results_index';
}
public function toSearchableArray()
{
$array = [
'id' => $this->id,
'results' => $this->results
];
return $array;
}
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'results_table';
}
Here is the \App\MyIndexConfiguratorResults::class
<?php
namespace App;
use ScoutElastic\IndexConfigurator;
use ScoutElastic\Migratable;
class MyIndexConfiguratorResults extends IndexConfigurator
{
use Migratable;
protected $name = "results_index";
/**
* #var array
*/
protected $settings = [
//
];
}
This is all that is needed to have Laravel automatically update AWS ES each time the table is updated. For the initial load, I would SSH in and run the following command to have it create the index. This, as well as elastic:migrate and any update/insert into the table produces the mapping error.
php artisan elastic:create-index results_index
Finally figured this out so will share the solution for anyone that runs into this. Turns out to be a fairly simple fix, though I'm not sure how it even worked in the first place so that part is still baffling.
I created a brand new index and update the mappings accordingly to add "id" and remove the type "nested" from the "results" piece. (Adding the "nested" type was adding two "results" to the index - one that contained all my nested data, the other just being "object".)
protected $mapping = [
'properties' => [
'id' => [
'type' => 'integer'
],
'results' => [
'properties' => [
'finish' => [
'type' => 'integer'
],
'other_id' => [
'type' => 'integer'
],
'other_data' => [
'type' => 'integer'
]
]
],
]
];
Then I simply added json_decode to the toSearchableArray() function as so:
public function toSearchableArray()
{
$array = [
'id' => $this->id,
'results' => json_decode($this->results, true)
];
return $array;
}
Voila. It successfully created the index and imported the data in a manner with which I can query the nested object.
Reading through the docs, the type field seems to have been removed. Scroll down to 7.x to see. Also, it seems you need to delete the index and re-add it in order for the new map to work according to this page.

Laravel: How can I assertJson an array

I am creating a feature test for a Seminar. Everything is working great; I am trying to update my feature test to account for the seminar dates.
Each Seminar can have one or many dates, so I am saving these values as a json field:
// migration:
...
$table->json('dates');
...
Here is what my Seminar model looks like:
// Seminar.php
protected $casts = [
'dates' => 'array',
];
When saving the seminar, I am returning a json resource:
if ($seminar->save()) {
return response()->json(new SeminarResource($seminar), 200);
...
Using Postman, my Seminar looks a like this:
...
"capacity": 100,
"dates": [
"2020-10-15",
"2020-10-16"
],
...
So far so good!
In my test, I am testing that a seminar can be created.
$http->assertStatus(201)
->assertJson([
'type' => 'seminars',
'id' => (string)$response->id,
'attributes' => [
'dates' => $response->attributes->dates, // General error: 25 column index out of range
I've tried to convert the array to a string, or json_encode the value in the resource. I don't think that's the correct way since I am already casting the value as an array in the model.
How can I assert that my dates is returning an array?
+"dates": array:2 [
0 => "2020-10-15"
1 => "2020-10-16"
]
Thank you for your suggestions!
EDIT
When I dd($response->attributes->dates); this is what I'm getting (which is correct).
array:2 [
0 => "2020-10-15"
1 => "2020-10-16"
]
What I'm not sure is how to assert an array like that. Since I'm using faker to generate the date, I don't really know (or care) what the date is, just want to assert that it is in fact an array.
I've tried something like:
'dates' => ['*'],
However, that just adds another element to the array.
EDIT 2
If I make the array a string,
'dates' => json_encode($response->attributes->dates),
I'll get an error like this:
--- Expected
+++ Actual
## ##
- 'dates' => '["2020-10-15","2020-10-16"]',
+ 'dates' =>
+ array (
+ 0 => '2020-10-15',
+ 1 => '2020-10-16',
+ ),
In my database, the values are stored like this:
["2020-10-15","2020-10-16"]
My actual test looks like this:
$http->assertStatus(201)
->assertJsonStructure([
'type', 'id', 'attributes' => [
'name', 'venue', 'dates', 'description', 'created_at', 'updated_at',
],
])
->assertJson([
'type' => 'workshops',
'id' => (string)$response->id,
'attributes' => [
'name' => $response->attributes->name,
'venue' => $response->attributes->venue,
'dates' => $response->attributes->dates,
'description' => $response->attributes->description,
'created_at' => (string)$response->attributes->created_at,
'updated_at' => (string)$response->attributes->updated_at,
],
]);
$this->assertDatabaseHas('workshops', [
'id' => $response->id,
'name' => $response->attributes->name,
'venue' => $response->attributes->venue,
'dates' => $response->attributes->dates,
'description' => $response->attributes->description,
]);

YII2: custom sorting in search model

Please, help me with such a problem:
1) I have default search model of Users.
2) I need a list of users. And first in this list always must be user with login 'admin', and second - with login 'finance', and then all others sorted by id.
My method in UserController
public function actionUsersList() {
$searchModel = new UserSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->renderPartial('users-list', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
As I understood I have to change params of search in this line, to add sort conditions
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
But how exactly can I do this?
You can do that by adding following code to your action:
$dataProvider->sort->attributes['id'] = [
'asc' => [
new \yii\db\Expression("FIELD(login, 'finance', 'admin') DESC"),
'id' => SORT_ASC,
],
'desc' => [
new \yii\db\Expression("FIELD(login, 'finance', 'admin') DESC"),
'id' => SORT_DESC,
],
'label' => $searchModel->getAttributeLabel('id'),
];
$dataProvider->sort->defaultOrder = ['id' => SORT_ASC];
The field function returns the position of first parameter among other parameters or 0 if the value is not present among them. So for 'admin' it will return 2, for 'finance' 1 and for others 0. If you order DESC by that you will get the required order.
Other option is to add this definitions for sort into the search method of UserSearch model as suggested in mahsaa's answer. It depenends if you want to use this sorting in different actions.
In UserSearch class, add sort to ActiveDataProvider:
$dataProvider = new ActiveDataProvider([
'query' => $query,
'sort' => [
'defaultOrder' => [
'login' => SORT_ASC,
'id' => SORT_DESC,
],
]]);
It first sorts by login and then id.

How to access data from an array when it's inside an std object? Error: Illegal string offset 'id'

How to access this array object in Laravel 6, using Eloquent?
[line_items] => Array
(
[0] => stdClass Object
(
[id] => 4088662196333
[variant_id] => 29653605285997
$external = DB::table('orders')->pluck('import_order_data');
...
foreach ($external as $key => $val) {
...
Cart::updateOrCreate([
'line_item_id' => ['line_item_id' => $val['id']],
],
ErrorException
Illegal string offset 'id'
If I change it to:
'line_item_id' => ['line_item_id' => $val->id],
I get error:
ErrorException
Trying to get property 'id' of non-object
If I change it to:
'line_item_id' => ['line_item_id' => $val['line_items']->id],
I get error:
Illegal string offset 'line_items'
EDIT:
The problem was:
protected $casts = [
'import_order_data' => 'array',
];
Now I can access it like this:
dd($val['line_items'][0]['id']);
Which provides:
4092309209197
or
dd($val['line_items']);
which provides:
array:1 [▼
0 => array:26 [▼
"id" => 4092309209197
"sku" => "1605"
"name" => "Printer Ink"
Any better options on accessing the data?
EDIT:
Answer:
foreach ($val['line_items'] as $index => $lineItem) {
dd($lineItem['id']);
Which provides:
4092309209197
Is this a reasonable way to do it?
If I understand correctly, your array looks like this:
$arr = [
[
"id" => 4092309209197,
"sku" => "1605",
"name" => "Printer Ink",
....etc.....
]
];
Fine. First, since the outer array only has one item, you can do this:
$innerArr = $arr[0];
Now you've got this:
$innerArr = [
"id" => 4092309209197,
"sku" => "1605",
"name" => "Printer Ink",
....etc.....
]
And you can access it like this:
echo $innerArr["id"];
echo $innerArr["sku"];
....etc....
Or like this:
foreach($innerArr as $key => $val){
echo $key.": ".$val."\r\n";
}

Map array values to collection of items

How would one do the following elegantly with laravel collections ?
Map the values of the $baseMap as keys to the collection.
The baseMap :
$baseMap = [
'name' => 'new_name',
'year' => 'new_year',
];
The collection :
$items = collect([
[
'name' => 'name1',
'year' => '1000',
'not_in_basemap' => 'foo'
],
[
'name' => 'name2',
'year' => '2000',
'not_in_basemap' => 'foo'
],
//...
]);
The end result :
$result =[
[
'new_name' => 'name1',
'new_year' => '1000',
],
[
'new_name'=> 'name2',
'new_year' => '2000',
],
];
I know how to do it in plain php , just wondering what a nice collection version would be. Thanks!
I tried to find collection methods, or php functions, but without success. Some dirty code that works with different keys from both sides (items and basemap).
$result = $items->map(function($item) use ($baseMap) {
$array = [];
foreach($baseMap as $oldKey => $newKey){
if(isset($item[$oldKey])){
$array[$newKey] = $item[$oldKey];
}
}
return $array;
});
$result = $result->toArray();
Thanks to #vivek_23 and #IndianCoding for giving me idea's I ended up with the following :
I made a small edit to make sure the mapping and the items keys lined up.
so you don't have to worry of misalignment and all in laravel collection !
$baseMap = collect($baseMap)->sortKeys();
$result = $items->map(function ($item) use ($baseMap) {
return $baseMap->values()
->combine(
collect($item)->sortKeys()->intersectByKeys($baseMap)
)
->all();
});
Use intersectByKeys to filter your baseMap keys with $items values.
$result = $items->map(function($item,$key) use ($baseMap){
return array_combine(array_values($baseMap),collect($item)->intersectByKeys($baseMap)->all());
});
dd($result);
Update:
In a pure collection way,
$baseMapCollect = collect($baseMap);
$result = $items->map(function($item,$key) use ($baseMapCollect){
return $baseMapCollect->values()->combine(collect($item)->intersectByKeys($baseMapCollect->all())->values())->all();
});
dd($result);
Here are my two cents, using map. Don't know how dynamic your collection should be, but knowing the keys I would do the following:
$baseMap = [
'name' => 'new_name',
'year' => 'new_year',
];
$items = collect([
[
'name' => 'name1',
'year' => '1000',
'not_in_basemap' => 'foo'
],
[
'name' => 'name2',
'year' => '2000',
'not_in_basemap' => 'foo'
],
])->map(function($item, $key) use ($baseMap) {
return [
$baseMap['name'] => $item['name'],
$baseMap['year'] => $item['year']
];
});

Resources