Modifying object in array is changing the session value - php

I have following piece of code in PHP Laravel that has array of objects. Each object has 2 attributes. Id and name. This whole object is being saved in session. Then after saving, I am removing the id from each object in the array. This is also modifying the session.
Am I doing something wrong?
$array = [];
$obj = new \stdClass();
$obj->id = "1";
$obj->name = "Test";
array_push($array, $obj);
$request->session()->put('val', $array);
foreach($array as $key => $val) {
unset($array[$key]->id);
}
echo "<pre>";
print_r($request->session()->get('val'));
echo "</pre>";
here, I noticed that id attribute is gone from session.

Issue arises because the same copy of the object is being manipulated. To solve this, you will need to clone the object and then unset and send in response keeping data in session intact.
$cloned_data = [];
foreach($array as $key => $val) {
$new_obj = clone $val;
unset($new_obj->id);
$cloned_data[] = $new_obj;
}
print_r($cloned_data);

For sending only the names to the frontend you can use collection pluck to get a new array of just names
$dataForFrontend = collect($array)->pluck('name')->all();
This is changing the view of data to be send to front end. [0] => 'Test'
If you need to key it by name - something like [0]['name'] => 'Test'
$dataForFrontend = collect($data)->map(
fn($item) => ['name' => $item->name]
)->all();
If there's need to retain the array elements as objects of original type, essentially same as #nice_dev's solution
$dataForFrontend = collect($array)->map(function($item) {
$clone = clone $item;
unset($clone->id);
return $clone;
})->all();
To retain the array elements as objects - not of original type but as objects of std class
$dataForFrontend = collect($data)->map(
fn($item) => (object)['name' => $item->name]
)->all();

Related

In PHP how do I remove duplicates in an array of objects where a duplicate is defined as a subset of key-value pairs having the same value

I have an array of the form:
class anim {
public $qs;
public $dp;
public $cg;
public $timestamp;
}
$animArray = array();
$myAnim = new anim();
$myAnim->qs = "fred";
$myAnim->dp = "shorts";
$myAnim->cg = "dino";
$myAnim->timestamp = 1590157029399;
$animArray[] = $myAnim;
$myAnim = new anim();
$myAnim->qs = "barney";
$myAnim->dp = "tshirt";
$myAnim->cg = "bird";
$myAnim->timestamp = 1590133656330;
$animArray[] = $myAnim;
$myAnim = new anim();
$myAnim->qs = "fred";
$myAnim->dp = "tshirt";
$myAnim->cg = "bird";
$myAnim->timestamp = 1590117032286;
$animArray[] = $myAnim;
How do I create a new array containing only the non-duplicates (and the latest entry where duplicates are found) of $animArray, where a duplicate is defined as:
one where $myAnim->dp has the same value as that of another array element's $myAnim->dp AND the $myAnim->cg from the first and the $myAnim->cg from the second have the same value as each other.
In the example above, only the first element is unique by that definition.
I'm hoping there's an elegant solution. I've been through all the array functions in the PHP manual but can't see how it could be achieved.
I could loop through each array element checking if $myAnim->dp has the same value as that of another array element's $myAnim->dp, saving the matches into a new array and then looping through that new array, checking for its $myAnim->cg matching the $myAnim->cg of any other element in that new array.
A more elegant solution would allow me to to change which combination of key-value pairs determine whether there's a duplicate, without having to recast much code.
Does such a solution exist?
Thanks for helping this novice :)
While there is nothing built-in that can be used directly out of the box, there isn't a lot of code necessary to handle an arbitrary number of properties to consider for uniqueness. By keeping track of each unique property in a lookup array, we can build an array where the leaf nodes (i.e. the ones that isn't arrays themselves) are the objects.
We do this by keeping a reference (&) to the current level in the array, then continue building our lookup array for each property.
function find_uniques($list, $properties) {
$lookup = [];
$unique = [];
$last_idx = count($properties) - 1;
// Build our lookup array - the leaf nodes will be the items themselves,
// located on a level that matches the number of properties to look at
// to consider a duplicate
foreach ($list as $item) {
$current = &$lookup;
foreach ($properties as $idx => $property) {
// last level, keep object for future reference
if ($idx == $last_idx) {
$current[$item->$property] = $item;
break;
} else if (!isset($current[$item->$property])) {
// otherwise, if not already set, create empty array
$current[$item->$property] = [];
}
// next iteration starts on this level as its current level
$current = &$current[$item->$property];
}
}
// awr only calls the callback for leaf nodes - i.e. our items.
array_walk_recursive($lookup, function ($item) use (&$unique) {
$unique[] = $item;
});
return $unique;
}
Called with your data above, and the requirement being that uniques and the last element of duplicates being returned, we get the following result:
var_dump(find_uniques($animArray, ['dp', 'cg']));
array(2) {
[0] =>
class anim#1 (4) {
public $qs =>
string(4) "fred"
public $dp =>
string(6) "shorts"
public $cg =>
string(4) "dino"
public $timestamp =>
int(1590157029399)
}
[1] =>
class anim#3 (4) {
public $qs =>
string(4) "fred"
public $dp =>
string(6) "tshirt"
public $cg =>
string(4) "bird"
public $timestamp =>
int(1590117032286)
}
}
Which maps to element [0] and element [2] in your example. If you instead want to keep the first object for duplicates, add an isset that terminates the inner loop if property value has been seen already:
foreach ($properties as $idx => $property) {
if ($idx == $last_idx) {
if (isset($current[$item->$property])) {
break;
}
$current[$item->$property] = $item;
} else {
$current[$item->$property] = [];
}
// next iteration starts on this level as its current level
$current = &$current[$item->$property];
}
It's important to note that this has been written with the assumption that the array you want to check for uniqueness doesn't contain arrays themselves (since we're looking up properties with -> and since we're using array_walk_recursive to find anything that isn't an array).
This was fun:
array_multisort(array_column($animArray, 'timestamp'), SORT_DESC, $animArray);
$result = array_intersect_key($animArray,
array_unique(array_map(function($v) { return $v->dp.'-'.$v->cg; }, $animArray)));
First, extract the timestamp and sort that array descending, thereby sorting the original array.
Then, map to create a new array using the dp and cg combinations.
Next, make the combination array unique which will keep the first duplicate encountered (that's why we sorted descending).
Finally, get the intersection of keys of the original array and the unique one.
In a function with dynamic properties:
function array_unique_custom($array, $props) {
array_multisort(array_column($array, 'timestamp'), SORT_DESC, $array);
$result = array_intersect_key($array,
array_unique(array_map(function($v) use ($props) {
return implode('-', array_map(function($p) use($v) { return $v->$p; }, $props));;
},
$array)));
return $result;
}
$result = array_unique_custom($animArray, ['dp', 'cg']);
Another option would be to sort it ascending and then build an array with a dp and cg combination as the key, which will keep the last duplicate:
array_multisort(array_column($animArray, 'timestamp'), SORT_ASC, $animArray);
foreach($animArray as $v) {
$result[$v->dp.'-'.$v->cg] = $v;
}
In a function with dynamic properties:
function array_unique_custom($array, $props) {
array_multisort(array_column($array, 'timestamp'), SORT_ASC, $array);
foreach($array as $v) {
$key = implode(array_map(function($p) use($v) { return $v->$p; }, $props));
$result[$key] = $v;
}
return $result;
}
$result = array_unique_custom($animArray, ['dp', 'cg']);
//Create an array with dp and cg values only
$new_arr = [];
foreach($animArray as $key=>$item) {
$new_arr[] = $item->dp.','.$item->cg;
}
$cvs = array_count_values($new_arr);
$final_array = [];
foreach($cvs as $cvs_key=>$occurences) {
if ($occurences == 1) {
$filter_key = array_keys($new_arr, $cvs_key)[0];
$final_array[$filter_key] = $animArray[$filter_key];
}
}
The final result would be (from your example) in $final_array:
[0] => anim Object
(
[qs] => fred
[dp] => shorts
[cg] => dino
[timestamp] => 1590157029399
)
Some explanation:
//Create a new array based on your array of objects with the attributes dp and cg
//with a comma between them
$new_arr = [];
foreach($animArray as $key=>$item) {
$new_arr[] = $item->dp.','.$item->cg;
}
/*
$new_arr now contains:
[0] => shorts,dino
[1] => tshirt,bird
[2] => tshirt,bird
*/
//Use builtin-function array_count_values to get the nr of occurences for
//each item in an array
$cvs = array_count_values($new_arr);
/*
$cvs would contain:
(
[shorts,dino] => 1
[tshirt,bird] => 2
)
*/
//Iterate through the $cvs array.
//Where there are only one occurence (no duplicates)
//create a final array $final_array
$final_array = [];
foreach($cvs as $cvs_key=>$occurences) {
if ($occurences == 1) {
/*
array_keys with second argument $csv_key searches for key with
with the key from $cvs-key
so basically search for:
shorts,dino and retrieve the key 0 (first element)
*/
$filter_key = array_keys($new_arr, $cvs_key)[0];
/*
Add a new item to the $final_array based on the key in
the original array $animArray
if you don't want the original key in the new array
you could just do $final_array[] instead of
$final_array[$filter_key]
*/
$final_array[$filter_key] = $animArray[$filter_key];
}
}
You said you would like to have some kind of functionality test different attributes. I believe it would just be making a function/method where you pass in two values to the arguments $attr1 ('dp'?), $attr2('cg'?) or similar.
UPDATE
I had not grasped that you wanted the last value as well. This actually seemed as an easier task. Maybe I am missing something but it was fun to come up with a different approach than other answers :-)
//Create an array with dp and cg values only
$new_arr = [];
foreach($animArray as $key=>$item) {
$new_arr[] = $item->dp.','.$item->cg;
}
//Sort keys descending order
krsort($new_arr);
//Because of sending order of keys above, the unique values would return the
//last item of the duplicates
$new_arr2 = array_unique($new_arr);
//Switch order of keys back to normal (ascending)
ksort($new_arr2);
//Create a new array based on the keys set in $new_arr2
//
$final_arr = [];
foreach($new_arr2 as $key=>$item) {
$final_arr[] = $animArray[$key];
}
The output of $final_arr[] would be (in your example)
Array
(
[0] => anim Object
(
[qs] => fred
[dp] => shorts
[cg] => dino
[timestamp] => 1590157029399
)
[1] => anim Object
(
[qs] => fred
[dp] => tshirt
[cg] => bird
[timestamp] => 1590117032286
)
)

Cannot foreach over an array of objects in a Laravel POST

I have an array of objects which have been posted from a Vue Axios function, which I wish to loop over and save into a database. They are answers to a question.
I have passed in $data which is the array of answer objects (each has a content, correct and mark property), and the $id of the question they belong to. When I return $data, it shows me the array of objects with all the correct properties. When I return $data[0], I can access the first object. But when I try and foreach as below, it complains that $content doesn't exist. Running count() on $data also errors. What is wrong here?
Route::post('answers/{id}', function (Request $data, $id) {
foreach ($data as $value) {
$post[] = [
'user_id' => 1,
'question_id' => $id,
'content' => $value->content,
'correct' => $value->correct,
'mark' => $value->mark
]);
}
Answer::save($post);
});
You are trying to iterate over the hole $request object, which is an instance of the Request class. To access the received values first get them:
// To get all the data
$data = $request->all();
// or..
// To get just a specific value
$data = $request->get('key');
// or..
// only a list of allowed elements
$data = $request->only('here', 'goes', 'your', 'keys');
So, in case your frontend are sending an array of items under the key items. Just get them like mentioned above:
$items = $request->get('items');
Then you can use the foreach():
$items = $request->get('items');
foreach($items as $item)
{
// your operations
}
You can read more about Retrieving Input, in the documentation.

PHP paradoxical (weird) behavior. Sending array to function by reference

Could someone explain me, why this code works properly without crashing initial array structure?
function setArrayValueByPath($path, $value, &$array)
{
foreach ($path as $p) {
$array = &$array[$p];
}
$array = $value;
return true;
}
$array = [
'a' => 'v1',
'b' => 'v2',
];
setArrayValueByPath(['hello', 'world'], '!!!', $array);
echo '<pre>';
print_r($array);
echo '</pre>';
When I run the code, I see:
Array
(
[a] => v1
[b] => v2
[hello] => Array
(
[world] => !!!
)
)
Due to the line in function:
$array = $value;
it should replace $array value, but it does not happen.
My function is based on code snippets are given here: Using a string path to set nested array data
Thank you.
Let's examine this one step at a time.
The parameter $array is a local variable within the function which contains a reference to some external array being passed in.
foreach ($path as $p) {
This iterates over ['hello', 'world']
$array = &$array[$p];
Take the original array, and "index" it with $p (i.e. [hello]). This does not currently exist so it is added to the original array. Then take a reference to that new member and save it in the local variable $array. I.e. you just created a new member of the original array, and the local variable $array no longer points to the original external array.
On the second iteration, take the variable currently pointed to by $array (see just above) and index it with $p (world). This does not exist, so create it.
}
At this point $array points to the member {original array}[hello][world]. I use the syntax {original array} here because you no longer have a reference to it, only a reference to an array two levels nested within it.
$array = $value;
This sets the value of that member to !!!, giving exactly the data structure you see.

Create nested hashes from an array of elements for JSON encoding

I have produced an array of fruits stored somewhere. Say it looks like this
$myFruits = array("apples"=>1, "oranges"=>3, "bananas"=>5);
This array is then passed into a function that will return json-encoded data for an API.
First, I wanted to be able to return a list of all the types of fruits I have
{"fruits":["apples", "oranges", "bananas"]}
I used this to accomplish it
echo json_encode(array("scripts" => array_keys($scripts)));
Now, I would like each type of fruit to be contained in its own hash, as such
{"fruits":
[
{name: "apples"
},
{name: "oranges"
},
{name: "bananas"
]
}
This way I can add additional fields to each fruit object without breaking existing code that may be using previous versions of this API (eg: if I decided to add the fruit counts in there as well).
Seeing how I can just create a new array and assign my list of fruits to a key called "fruits", I tried to do the same for each inner hash:
$myFruits = array("apples"=>1, "oranges"=>3, "bananas"=>5);
$data = array();
foreach ($myFruits as $key => $value) {
// initialize an associative array for each fruit
$val = array();
array_push($val, array("name" => $key));
// add it to the list of fruits
array_push($data, $val);
}
// assign list of fruits to "fruits" key
$outData = array("fruits" => $data);
echo json_encode($outData);
But I get this instead
{"fruits":[[{"name":"apples"}],[{"name":"oranges"}],[{"name":"bananas"}]]}
There are extra square braces around each fruit hash, which I assume is because I'm using an array to store each key-value pair.
How would I get my desired output?
You're close to knowing what you're doing wrong. You're creating an array, and then just using it to add one item (another array) to it.
// initialize an associative array for each fruit
$val = array(); // this guy here is unnecessary!!
array_push($val, array("name" => $key));
// add it to the list of fruits
array_push($data, $val);
Instead, just push each individual array onto $data directly like this:
array_push($data, array("name" => $key));
DEMO
You are creating an extra level in your array, simply push a new array onto $data in each iteration:
foreach ($myFruits as $key => $value) {
$data[]=array("name" => $key, "count" => $value);
}
*edited as per your comment

PHP SplObjectStorage attach and push to top of the list

I have a little situation, I am using the SplObjectStorage object and at some point I need to attach an item but also push it to the top of the list so when I iterate through the items I will get it as first object.
i.e.
$splObj->attach($something)
$splOBj->attach($something2)
$splObj->attach($this_must_be_first);
// When I iterate
foreach($splOBj as $obj) {
// I need the FIRST item to be the $this_must_be_first
}
Am not sure if this exist for iterators but this is a simple work around with iterator_to_array and array_reverse
$splObj = new SplObjectStorage();
$last = new stdClass();
$last->element = "Last Element";
$splObj->attach(new stdClass());
$splObj->attach(new stdClass());
$splObj->attach($last);
$splArray = iterator_to_array($splObj);
$splArray = array_reverse($splArray);
foreach ($splArray as $obj)
{
var_dump($obj);
}
Output
object(stdClass)[2]
public 'element' => string 'Last Element' (length=12)
object(stdClass)[4]
object(stdClass)[3]

Categories