Laravel Collection Transform/Map Method Inconsistent Behaviour - php

In my HTML frontend, I have a jQuery DataTable displaying all records fetched via AJAX from the database - a rather pretty straight forward thing. I use the Laravel Collection's ->transform(function($o){ . . . }) to iterate through the collection and return it in an array-esque manner. Just think of the following piece of code in a controller:
$cAllRecords = DatabaseRecord::all();
if(!empty($aData['sFilterIds']))
{
$cAllRecords = $cAllRecords->whereIn('creator', explode(',', $aData['sFilterIds']));
}
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})]);
I'm actually just filtering for records created by certain user IDs (the whereIn() call in line 4. However, the response sent back to the client looks different for different users filtered leading the jQuery table to show 'no records available', as it had received an malformed answer from the server. For one user, the response looks like this:
{
"data":[
[
1,
"2019-05-29 16:44:53",
"2019-05-29 16:44:53",
"<a href=\"#\">edit<\/a>"
]
]
}
This is a correctly formed server response and will show up in the table regularly. Great! Now something that drives me insane - the same code for another user (ID 1, while the first request was for user ID 2) returns this:
{
"data":{
"1":[
3,
"2019-05-29 17:08:49",
"2019-05-29 17:08:49",
"<a href=\"#\">edit<\/a>"
]
}
}
which, pretty obviously, is malformed and is not correctly parsed by the datatable. OK, now combing them two filters and filtering for user ID 1 and 2 will, again, return the response correctly formatted:
{
"data":[
[
1,
"2019-05-29 16:44:53",
"2019-05-29 16:44:53",
"<a href=\"#\">edit<\/a>"
],
[
3,
"2019-05-29 17:08:49",
"2019-05-29 17:08:49",
"<a href=\"#\">edit<\/a>"
]
]
}
I tried a number of things, none of which had worked since it's merely guessing why it could work with one user and not with another. (Things like reversing the order of IDs to be filtered, etc., but I found out that the filtering is not the problem. It MUST be the transform, which behaves inconsistent.)
Any ideas on why this happens and how to tackle it? I mean, it's not the only way to achieve what I'm after, I was using ->each() and array_push for all the time before but wanted to get rid of it for the sake of making use of Laravel's helpers (or possibilites) - the manual iteration and array pushing process worked out seamlessly before, and even other parts of the app work well with the Collection transform over array iteration and pushing. Why doesn't it here?
Update: The ->map() collection method behaves exactly same. Map, as opposed by transform, does not alter the collection itself. However, this should not be a relevant part within this application any way. I really can't understand what's going wrong. Is this possibly Laravel's fault?

Please note that transform method returns a Illuminate\Support\Collection.
It's better that you call all() after the transform to get an array result.
Like this:
...
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})->all()]);

#Cvetan Mihaylov's answer made me look at all the available collection methods (https://laravel.com/docs/5.8/collections#available-methods) and I found ->values() to return the values reindexed. And - that did the trick! :-)
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})->values()]);

Related

Laravel ->put() issue - Mixed content (JSON vs. not JSON)

I'm having an issue with using Laravels put() function, as I want put JSON content in this one single scenario.
$datatable->GroupsCollection = $datatable->GroupsCollection->put($job, '{"grade":'.$grade.'}' );
But when trying to create 'fake' JSON, the inserted value will be:
{\"grade\":'VALUE_OF_$GRADE'}
I've tried using str_replace() and stripslashes() to cut out the backwardslashes, but no bueno.
I've Googled around, and reading something about a cast was needed in the Model.
So I put in this:
protected $casts = [
'dvalue' => 'array',
];
This result in breaking existing functionality of the code.
public function getGroupsCollectionAttribute()
{
return collect($this->dvalue ? $this->dvalue['groups'] : null);
}
public function setGroupsCollectionAttribute($value)
{
$currentValue = $this->dvalue ?? new Collection();
$this->dvalue['groups'] = $currentValue->$value;
}
I 'fixed' the get, but I'm not sure how I should format the 'set' function with this new cast and setting it to an array.
Worth to notice is that we have mixed content in the DB-rows, so it's not always JSON.
Any easier way to go around this?
Ending up fixing it by simply creating an array like this:
$grade_json = array("grade" => $grade);
$datatable->GroupsCollection = $datatable->GroupsCollection->put($job, $grade_json);

Laravel - pluck with specified keys

I have a line of code similar to the following:
Sport::pluck('id', 'name)
I am dealing with frontend JavaScript that expects a list in this format:
var list = [
{ text: 'Football', value: 1 },
{ text: 'Basketball', value: 2 },
{ text: 'Volleyball', value: 3 }
...
]
I am trying to figure out how I can somehow transform the id and name values that I pluck from my model to a format similar to the Javascript list.
If that's unclear, I am looking to end up with an associative array that contains two keys: text and value, where text represents the name field on my model, and where value represents the id of the model - I hope this makes sense.
How would I approach this?
I initially tried something like this (without checking the documentation)
Sport::pluck(["id" => "value", "name" => "text]);
But that isn't how you do it, which is quite clear now. I've also tried some map-related snippet, which I cannot seem to Ctrl-z to.
Any suggestions?
Another method is to use map->only():
Sport::all()->map->only('id', 'name');
The purpose of pluck is not what you intend to do,
Please have a look at below examples,
Sport::selectRaw("id as value, name as text")->pluck("text","value");
// ['1' => 'Football', '2'=>'BasketBall','3'=>'Volleyball',...]
Syntax
$plucked = $collection->pluck('name', 'product_id');
// ['prod-100' => 'Desk', 'prod-200' => 'Chair']
Please see the documentation.
Your output is possible using simple code.
Sport::selectRaw('id as value, name as text')->get();
You could use map.(https://laravel.com/docs/5.8/collections#method-map)
$mapped = Sport::all()->map(function($item, $index) {
return [
"id" => $item["id"],
"name" => $item["text"]
];
});
This is the easiest way. Actually Laravel offers a better way for it. You can use api resources to transform your data from eloquent for the frontend:
https://laravel.com/docs/5.8/eloquent-resources
Try with toArray function:
Sport::pluck('id', 'name)->toArray();
Then you can return your result with json_encode php function;

Elegently handle property of non-object errors that surface from different locations in code

My app reads from a DB that get's written by another API, now in some outlandish cases (that actually happened today) it wrote a customer id of 0, which ofcourse, does not exist.
I am looking for an elegant 'from-the-top' model or even presenter solution for handling erroneous ID's that do not exist.
So instead of finding every $whatever->customer->id in my app and then writing in an isset()/empty() ternary function, I am looking to pacify this error in a more elegant way where any customer instantiation/eloquent object would send the string "NA" to a non existent object, so even if an email/phone/etc or any other column of customer model, it would return a simple "NA" string.
I am struggling to find an eloquent solution that would provide 1 point of change.
you can use withDefault() modifier on your relationship.
example:
use Illuminate\Database\Eloquent\Model;
class Whatever extends Model {
public function customer() {
return $this->belongsTo(Customer::class, 'customer_id', 'id')
->withDefault([
'id' => 'NA',
'name' => 'Unknown'
// etc
]);
}
}
I would suggest you take a look at a Laravel class that most people don't know about. That is Fluent.
It allows you to do stuff like this:
$fluent = new Fluent([
'one' => 1,
'two => 2,
]);
echo $fluent->get('one'); // returns 1
echo $fluent->get('three'); // returns null
echo $fluent->get('three', 3); // returns 3
As you can imagine, it's perfect to use with third-party APIs and data that sometimes provide unexpected results. You can also do a lot more with Fluent.
Alternatively, you could look into Laravel helpers such as array_get(). From the documentation:
The array_get function retrieves a value from a deeply nested array using "dot" notation:
$array = ['products' => ['desk' => ['price' => 100]]];
$price = array_get($array, 'products.desk.price');
// 100
The array_get function also accepts a default value, which will be returned if the specific key is not found:
$discount = array_get($array, 'products.desk.discount', 0);
// 0

Laravel Response::json() with numeric check

When performing an eloquent query on a model (using the MySQL driver) which has some numeric fields and then return a json response of the results, the json appears to pass numeric values as strings rather than numbers.
E.g.
$properties = Model::find(6);
return Response::json($properties);
Returns something like:
{
"name": "A nice item",
"value": "160806.32"
}
When it should return:
{
"name": "A nice item",
"value": 160806.32
}
In normal php you can use the JSON_NUMERIC_CHECK to solve this but there appears to be no such option for the Response::json() method. How can I ensure numeric fields are returned as numbers rather than strings?
You can actually pass that option over. If we take a look at the source code for the JsonResponse class you can pass json_encode options as the last parameter.
It would look something like this
return Response::json($properties, 200, [], JSON_NUMERIC_CHECK);
Alternatively you could do this:
return Response::make(
$properties->toJson(JSON_NUMERIC_CHECK),
200,
['Content-Type' => 'application/json']
);
Note: if $properties is not an Elequoent model then it must at least implement the JsonableInterface
as well as:
return Response::make(
json_encode($properties->toArray(), JSON_NUMERIC_CHECK),
200,
['Content-Type' => 'application/json']
);
The toJson() method in Eloquent just wraps json_encode() and passes it the array of your model. I'd recommend using one of the first two options.
Use method setEncodingOptions of JsonResponse:
return response()->json($properties)->setEncodingOptions(JSON_NUMERIC_CHECK);

Map Reduce To Get Most popular tags

I have a problem that I need some help on but I feel I'm close. It involves Lithium and MongoDB Code looks like this:
http://pastium.org/view/0403d3e4f560e3f790b32053c71d0f2b
$db = PopularTags::connection();
$map = new \MongoCode("function() {
if (!this.saved_terms) {
return;
}
for (index in this.saved_terms) {
emit(this.saved_terms[index], 1);
}
}");
$reduce = new \MongoCode("function(previous, current) {
var count = 0;
for (index in current) {
count += current[index];
}
return count;
}");
$metrics = $db->connection->command(array(
'mapreduce' => 'users',
'map' => $map,
'reduce' => $reduce,
'out' => 'terms'
));
$cursor = $db->connection->selectCollection($metrics['result'])->find()->limit(1);
print_r($cursor);
/**
User Data In Mongo
{
"_id" : ObjectId("4e789f954c734cc95b000012"),
"email" : "example#bob.com",
"saved_terms" : [
null,
[
"technology",
" apple",
" iphone"
],
[
"apple",
" water",
" beryy"
]
] }
**/
I am having a user savings terms they search on and then I am try to get the most populars terms
but I keep getting errors like :Uncaught exception 'Exception' with message 'MongoDB::__construct( invalid name '. does anyone have any idea how to do this or some direction?
First off I would not store this in the user object. MongoDb objects have an upper limit of 4/16MB (depending on version). Now this limit is normally not a problem, but when logging inline in one object you might be able to reach it. However a more real problem is that every time you need to act on these objects you need to load them into RAM and it becomes consuming. I dont think you want that on your user objects.
Secondly arrays in objects are not sortable and have other limitations that might come back to bite you later.
But, if you want to have it like this (low volume of searches should not be a problem really) you can solve this most easy by using a group query.
A group query is pretty much like a group query in sql, so its a slight trick as you need to group on something most objects share. (An active field on users maybe).
So, heres a working group example that will sum words used based on your structure.
Just put this method in your model and do MyModel::searchTermUsage() to get a Document object back.
public static function searchTermUsage() {
$reduce = 'function(obj, prev) {
obj.terms.forEach(function(terms) {
terms.forEach(function(term) {
if (!(term in prev)) prev[term] = 0;
prev[term]++;
});
});
}';
return static::all(array(
'initial' => new \stdclass,
'reduce' => $reduce,
'group' => 'common-value-key' // Change this
));
}
There is no protection against non-array types in the terms field (you had a null value in your example). I removed it for simplicity, its better to probably strip this before it ends up in the database.

Categories