I'm creating JSON encoded data from PHP arrays that can be two or three levels deep, that look something like this:
[grandParent] => Array (
[parent] => Array (
[child] => myValue
)
)
The method I have, which is simply to create the nested array manually in the code requires me to use my 'setOption' function (which handles the encoding later) by typing out some horrible nested arrays, however:
$option = setOption("grandParent",array("parent"=>array("child"=>"myValue")));
I wanted to be able to get the same result by using similar notation to javascript in this instance, because I'm going to be setting many options in many pages and the above just isn't very readable, especially when the nested arrays contain multiple keys - whereas being able to do this would make much more sense:
$option = setOption("grandParent.parent.child","myValue");
Can anyone suggest a way to be able to create the multidimensional array by splitting the string on the '.' so that I can json_encode() it into a nested object?
(the setOption function purpose is to collect all of the options together into one large, nested PHP array before encoding them all in one go later, so that's where the solution would go)
EDIT: I realise I could do this in the code:
$options['grandparent']['parent']['child'] = "myValue1";
$options['grandparent']['parent']['child2'] = "myValue2";
$options['grandparent']['parent']['child3'] = "myValue3";
Which may be simpler; but a suggestion would still rock (as i'm using it as part of a wider object, so its $obj->setOption(key,value);
This ought to populate the sub-arrays for you if they haven't already been created and set keys accordingly (codepad here):
function set_opt(&$array_ptr, $key, $value) {
$keys = explode('.', $key);
// extract the last key
$last_key = array_pop($keys);
// walk/build the array to the specified key
while ($arr_key = array_shift($keys)) {
if (!array_key_exists($arr_key, $array_ptr)) {
$array_ptr[$arr_key] = array();
}
$array_ptr = &$array_ptr[$arr_key];
}
// set the final key
$array_ptr[$last_key] = $value;
}
Call it like so:
$opt_array = array();
$key = 'grandParent.parent.child';
set_opt($opt_array, $key, 'foobar');
print_r($opt_array);
In keeping with your edits, you'll probably want to adapt this to use an array within your class...but hopefully this provides a place to start!
What about $option = setOption("grandParent", { parent:{ child:"myValue" } });?
Doing $options['grandparent']['parent']['child'] will produce an error if $options['grandparent']['parent'] was not set before.
The OO version of the accepted answer (http://codepad.org/t7KdNMwV)
$object = new myClass();
$object->setOption("mySetting.mySettingsChild.mySettingsGrandChild","foobar");
echo "<pre>".print_r($object->options,true)."</pre>";
class myClass {
function __construct() {
$this->setOption("grandparent.parent.child","someDefault");
}
function _setOption(&$array_ptr, $key, $value) {
$keys = explode('.', $key);
$last_key = array_pop($keys);
while ($arr_key = array_shift($keys)) {
if (!array_key_exists($arr_key, $array_ptr)) {
$array_ptr[$arr_key] = array();
}
$array_ptr = &$array_ptr[$arr_key];
}
$array_ptr[$last_key] = $value;
}
function setOption($key,$value) {
if (!isset($this->options)) {
$this->options = array();
}
$this->_setOption($this->options, $key, $value);
return true;
}
}
#rjz solution helped me out, tho i needed to create a array from set of keys stored in array but when it came to numerical indexes, it didnt work. For those who need to create a nested array from set of array indexes stores in array as here:
$keys = array(
'variable_data',
'0',
'var_type'
);
You'll find the solution here: Php array from set of keys
Related
In $data I have the following values:
Result
[{"id":2,"author":"example#mail.com","id_orders":502},
{"id":2,"author":"example#mail.com","id_orders":503},
{"id":2,"author":"example#mail.com","id_orders":505},
{"id":3,"author":"second-example#mail.com","id_orders":502},
{"id":3,"author":"second-example#mail.com","id_orders":503},
{"id":3,"author":"second-example#mail.com","id_orders":505},
{"id":4,"author":"third-example#mail.com","id_orders":502},
{"id":4,"author":"third-example#mail.com","id_orders":503},
{"id":4,"author":"third-example#mail.com","id_orders":505}]
I want unique results for id and id_orders. I want 3 out of these 9 results. I have tried this, but it helps on one id_orders condition.
PHP code
$result = json_decode($data, true);
$unique_array = [];
foreach($result as $element) {
$hash = $element['id_orders'];
$unique_array[$hash] = $element;
}
$data = array_values($unique_array);
Do you know how it can be different to make it work for two?
You can do it by keeping track of the values that were already used. Disclaimer: this solution will only produce a clear result for cases where the number of unique values for both criteria is the same.
$uniqueArray = [];
$usedValues = [
'id' => [],
'id_orders' => [],
];
foreach ($result as $element) {
if (!in_array($element['id'], $usedValues['id']) && !in_array($element['id_orders'], $usedValues['id_orders'])) {
$uniqueArray[] = $element;
$usedValues['id'][] = $element['id'];
$usedValues['id_orders'][] = $element['id_orders'];
}
}
Basically, what's happening here is that we're using $usedValues to store all the unique values we've already used and comparing against it using in_array. When we iterate through the objects, any object with an id or id_orders that has already been used will be skipped. The pairings will be done in order of appearance in the array.
I've gone an extra mile to try and make this code a bit more generic:
* Finds elements with a unique combination of values under given keys.
*
* Assumes all elements in the array are arrays themselves and that the
* subarrays have the same structure. Assumes subarray elements are not
* objects (uses strict comparison).
*/
function uniqueCombination(array $arrayToFilter, array $keysToFilterOn): array
{
if (empty($arrayToFilter) || empty($keysToFilterOn)) {
throw new \InvalidArgumentException(
'Parameters of uniqueCombination must not be empty arrays'
);
}
// get the keys from the first element; others are assumed to be the same
$keysOfElements = array_keys(reset($arrayToFilter));
$keysPresentInBoth = array_intersect($keysToFilterOn, $keysOfElements);
// no point in running the algorithm if none of the keys are
// actually found in our array elements
if (empty($keysPresentInBoth)) {
return [];
}
$result = [];
$usedValues = array_combine(
$keysPresentInBoth,
array_fill(0, count($keysPresentInBoth), [])
);
foreach ($arrayToFilter as $element) {
if (!isAlreadyUsed($usedValues, $element)) {
$result[] = $element;
foreach ($keysPresentInBoth as $keyToUse) {
$usedValues[$keyToUse][] = $element[$keyToUse];
}
}
}
return $result;
}
function isAlreadyUsed(array $usedValues, array $element): bool
{
foreach ($usedValues as $usedKey => $usedValue) {
if (in_array($element[$usedKey], $usedValue)) {
return true;
}
}
return false;
}
In its core, this is the same algorithm, but made dynamic. It allows a variable number of keys to filter on (that's why they're passed separately as an argument), so the $usedValues array is created dynamically (using the first element's keys as its own keys, filled with empty arrays) and all the keys must be compared in loops (hence the separate function to check if an element's value had already been used).
It could probably be tweaked here or there as I haven't tested it thoroughly, but should provide satisfactory results for most structures.
I'm trying two combine two arrays based on the values names.
Here what I'm trying to do :
$array1 = ("fifa21","pes21","halflife2","carma2");
$array2 = ("fifa21_cover","pes21_cover","halflife2_cover");
I want to loop through both arrays and if value from array1 match value from array 2 it will list both values like this :
fifa21 - fifa21_cover
pes21 - pes21_cover
halflife2 - halflife2_cover
carma2 - default_cover
if not find anything in the array2, a default cover will be used.
Note: the arrays will be filled dynamically based in a scanDir (two different folders search) and the arrays can be sorted differently than shown in the example above.
This code will loop through array1 and check to see if the item cover exists in array2, if it does then it will echo out in the format you wanted.
$array1 = array("fifa21","pes21","halflife2","carma2");
$array2 = array("fifa21_cover","pes21_cover","halflife2_cover");
foreach ($array1 as $item) {
if (in_array($item."_cover", $array2)) {
echo "<p>".$item." - ".$item."_cover</p>\n";
} else {
// use default cover...
}
}
(because it is lunch time and I need to write some code)
Given two arrays:
$array1 = ["fifa21", "pes21", "halflife2", "carma2"];
$array2 = ["fifa21_cover", "pes21_cover", "halflife2_cover"];
A function such as this can walk through and look for items that start with those values:
function merge_arrays(array $array1, array $array2): array
{
$output = [];
array_walk(
$array1,
static function ($key) use ($array2, &$output) {
$items = array_filter($array2, static fn($f) => 0 === mb_strpos($f, $key));
if (count($items) === 1) {
$output[$key] = reset($items);
return;
}
if (count($items) === 0) {
$output[$key] = 'default_cover';
return;
}
throw new RuntimeException('More than one item found starting with key ' . $key);
}
);
return $output;
}
When called as:
print_r(merge_arrays($array1, $array2));
It will output:
(
[fifa21] => fifa21_cover
[pes21] => pes21_cover
[halflife2] => halflife2_cover
[carma2] => default_cover
)
For your use-case, it might be overly complicated when compared to a for loop, but it does the job.
Another version that more-blindly assumes only a one-to-one match could use the ?? operator for a null check, although I wouldn't recommend this. The more terse and compact that code looks, the more I find that it is usually very hard to reason about, especially in the future:
function merge_arrays(array $array1, array $array2): array
{
$output = [];
array_walk(
$array1,
static function ($key) use ($array2, &$output) {
$output[$key] = array_filter($array2, static fn($f) => 0 === mb_strpos($f, $key))[0] ?? 'default_cover';
}
);
return $output;
}
All code snipping provided works, however if the array values have extension it always assumes the default cover. I can figure it how using my code by removing the extension for compare purpose, and then I add it back.
Inside my loop I use this :
$keyvaluetemp = substr($keyvalue, 0, strrpos($keyvalue, "."));
the above it inside my first ForEach (where I check the first array), and then in the covers array I remove the "_cover" by nothing :
$keyvalue2temp = (str_replace("_cover", "",$keyvalue2));
then I use the following code inside the second ForEach to add the data to a DB:
// if fantart match the game file name so we associate it to the game and save to the DB
if ( stripos ( $keyvalue2temp , $keyvaluetemp ) !== FALSE )
{
$sql = "UPDATE listgames SET FanArt='$keyvalue2' WHERE Name='$keyvalue'";
$con->exec($sql);
}
// if fanart is empty we add a default picture
$checkifempty = "SELECT `FanArt` FROM `listgames` WHERE `Name` = '$keyvalue'";
$runsql = $con->query($checkifempty);
$row = $runsql->fetch(PDO::FETCH_ASSOC);
$fanartkey = $row['FanArt'];
if($fanartkey == '')
{
$sql = "UPDATE listgames SET FanArt='nopic.JPG' WHERE Name='$keyvalue'";
$con->exec($sql);
}
Later in my code I print the results from the DB. I don't know if this is the best approach, however it does exactly what I want :)
I have a PHP array that I'd like to duplicate but only copy elements from the array whose keys appear in another array.
Here are my arrays:
$data[123] = 'aaa';
$data[423] = 'bbb';
$data[543] = 'ccc';
$data[231] = 'ddd';
$data[642] = 'eee';
$data[643] = 'fff';
$data[712] = 'ggg';
$data[777] = 'hhh';
$keys_to_copy[] = '123';
$keys_to_copy[] = '231';
$keys_to_copy[] = '643';
$keys_to_copy[] = '712';
$keys_to_copy[] = '777';
$copied_data[123] = 'aaa';
$copied_data[231] = 'ddd';
$copied_data[643] = 'fff';
$copied_data[712] = 'ggg';
$copied_data[777] = 'hhh';
I could just loop through the data array like this:
foreach ($data as $key => $value) {
if ( in_array($key, $keys_to_copy)) {
$copied_data[$key] = $value;
}
}
But this will be happening inside a loop which is retrieving data from a MySQL result set. So it would be a loop nested within a MySQL data loop.
I normally try and avoid nested loops unless there's no way of using PHP's built-in array functions to get the result I'm looking for.
But I'm also weary of having a nested loop within a MySQL data loop, I don't want to keep MySQL hanging around.
I'm probably worrying about nested loop performance unnecessarily as I'll never be doing this for more than a couple of hundred rows of data and maybe 10 keys.
But I'd like to know if there's a way of doing this with built-in PHP functions.
I had a look at array_intesect_key() but that doesn't quite do it, because my $keys_to_copy array has my desired keys as array values rather than keys.
Anyone got any ideas?
Cheers, B
I worked it out - I almost had it above.I thought I'd post the answer anyway for completeness. Hope this helps someone out!
array_intersect_key($data, array_flip($keys_to_copy))
Use array_flip() to switch $keys_to_copy so it can be used within array_intersect_keys()
I'll run some tests to compare performance between the manual loop above, to this answer. I would expect the built-in functions to be faster but they might be pretty equal. I know arrays are heavily optimised so I'm sure it will be close.
EDIT:
I have run some benchmarks using PHP CLI to compare the foreach() code in my question with the code in my answer above. The results are quite astounding.
Here's the code I used to benchmark, which I think is valid:
<?php
ini_set('max_execution_time', 0);//NOT NEEDED FOR CLI
// BUILD RANDOM DATA ARRAY
$data = array();
while ( count($data) <= 200000) {
$data[rand(0, 500000)] = rand(0, 500000);
}
$keys_to_copy = array_rand($data, 100000);
// FOREACH
$timer_start = microtime(TRUE);
foreach ($data as $key => $value) {
if ( in_array($key, $keys_to_copy)) {
$copied_data[$key] = $value;
}
}
echo 'foreach: '.(microtime(TRUE) - $timer_start)."s\r\n";
// BUILT-IN ARRAY FUNCTIONS
$timer_start = microtime(TRUE);
$copied_data = array_intersect_key($data, array_flip($keys_to_copy));
echo 'built-in: '.(microtime(TRUE) - $timer_start)."s\r\n";
?>
And the results...
foreach: 662.217s
array_intersect_key: 0.099s
So it's much faster over loads of array elements to use the PHP array functions rather than foreach. I thought it would be faster but not by that much!
Why not load the entire result set into an array, then begin processing with nested loops?
$query_result = mysql_query($my_query) or die(mysql_error());
$query_rows = mysql_num_rows($query_result);
for ($i = 0; $i < $query_rows; $i++)
{
$row = mysql_fetch_assoc($query_result);
// 'key' is the name of the column containing the data key (123)
// 'value' is the name of the column containing the value (aaa)
$data[$row['key']] = $row['value'];
}
foreach ($data as $key => $value)
{
if ( in_array($key, $keys_to_copy))
{
$copied_data[$key] = $value;
}
}
Problem:
Extract data from an object/array and represent this data using a multidimensional array with a unique key generated from the inner loop.
I always find myself building multidimensional arrays like this:
$final_array = array();
foreach ($table as $row) {
$key = null;
$data = array();
foreach ($row as $col => $val) {
/* Usually some logic goes here that does
some data transformation / concatenation stuff */
if ($col=='my_unique_key_name') {
$key = $val;
}
$data[$col] = $val;
}
if (!is_null($key) {
if (!isset($final_array[$key]) {
$final_array[$key] = array();
}
$final_array[$key][] = $data;
}
}
I can't help but wonder if I'm constantly doing this out of habit, but it feels kind of verbose with all the key-checking and whatnot. Is there a native function I am not utilizing? Can this be refactored into something more simple or am I overthinking this?
Why are you always doing that? Doesn't seem the common kind of stuff one works with on a day to day basis... Anyway, that's kinda cryptic (an example would be nice) but have you though of using an MD5 hash of the serialized dump of the array to uniquely define a key?
$key = md5(serialize($value));
How can you do this? My code seen here doesn't work
for($i=0;i<count($cond);$i++){
$cond[$i] = $cond[$i][0];
}
It can be as simple as this:
$array = array_map('reset', $array);
There could be problems if the source array isn't numerically index. Try this instead:
$destinationArray = array();
for ($sourceArray as $key=>$value) {
$destinationArray[] = $value[0]; //you may want to use a different index than '0'
}
// Make sure you have your first array initialised here!
$array2 = array();
foreach ($array AS $item)
{
$array2[] = $item[0];
}
Assuming you want to have the same variable name afterwards, you can re-assign the new array back to the old one.
$array = $array2;
unset($array2); // Not needed, but helps with keeping memory down
Also, you might be able to, dependant on what is in the array, do something like.
$array = array_merge(array_values($array));
As previously stated, your code will not work properly in various situation.
Try to initialize your array with this values:
$cond = array(5=>array('4','3'),9=>array('3','4'));
A solution, to me better readable also is the following code:
//explain what to do to every single line of the 2d array
function reduceRowToFirstItem($x) { return $x[0]; }
// apply the trasnformation to the array
$a=array_map('reduceRowTofirstItem',$cond);
You can read the reference for array map for a thorough explanation.
You can opt also for a slight variation using array_walk (it operate on the array "in place"). Note that the function doesn't return a value and that his parameter is passed by reference.
function reduceToFirstItem(&$x) { $x=$x[0]; }
array_walk($cond, 'reduceToFirstItem');
That should work. Why does it not work? what error message do you get?
This is the code I would use:
$inArr;//This is the 2D array
$outArr = array();
for($i=0;$i<count($inArr);$i++){
$outArr[$i] = $inArr[$i][0];
}