PHP: Need an alternative to eval() for dynamically building multidimensional array - php

Okay, so I know that using eval() isn't great, but I haven't been able to come up with a better solution to my problem, and until recently, there wasn't a performance reason not to use it. However, I am now passing enough data to the function that it is taking unacceptably long.
The function that is being called is:
public static function makeAMultiDimensionalArrayWithSumsBasedOnMultipleFields($inArray, $dimensionFieldNames, $sumFieldNameArray, $staticFieldNameArray = array())
{
$outArray = array();
// Just in case the array has indices, sort it so array_pop works as expected.
ksort($dimensionFieldNames);
foreach ($inArray as $row)
{
// make sure each row in the inArray has all keys specified by $dimensionFieldNames
$allFieldsPresent = TRUE;
foreach ($dimensionFieldNames as $keyFieldName)
{
if (!array_key_exists($keyFieldName, $row))
{
// Note that alternatively we could set the field to a specified default value.
$allFieldsPresent = FALSE;
}
}
if ($allFieldsPresent)
{
$indexString = '';
$keyFieldNameArrayCopy = $dimensionFieldNames;
foreach ($dimensionFieldNames as $keyFieldName)
{
$indexString .= "['" . $row[$keyFieldName] . "']";
// lets sum values
foreach ($sumFieldNameArray as $sumFieldName)
{
eval ('$outArray' . $indexString . '[' . $sumFieldName . '] += $row[' . $sumFieldName . '];');
}
foreach ($staticFieldNameArray as $staticFieldName)
{
eval ('$outArray' . $indexString . '[' . $staticFieldName . '] = $row[' . $staticFieldName . '];');
}
}
}
}
return $outArray;
}
It is being called like this:
makeAMultiDimensionalArrayWithSumsBasedOnMultipleFields($data, $dimensionArray, $sumArray, $staticArray);
And the variables being passed to the function are similar to:
$dimensionArray = array("firstLevelID", "secondLevelID", "thirdLevelID", "fourthLevelID", "fifthLevelID");
$sumArray = array("revenue", "cost", "profit", "sales", "inquires", "cost", "walkins");
$staticArray = array("date", "storeID", "storeName", "productID", "productName", "managerID", "managerName", "salesperson");
So I want to rewrite the function so that I'm not using eval() any more. I've spent a considerable amount of time on this, and feel that it's time to seek some advice.
The goal is to take an array of arrays, and turn it into a multidimensional array based on the dimensions in the $dimensionArray.
I don't want to bore you with too many details right now, so please ask if you need more or have any other questions

Wow, okay. First time through I missed your indexing concatenation. Try this:
if ($allFieldsPresent) {
$keys = array();
foreach ($dimensionFieldNames as $keyFieldName) {
$keys[] = $row[$keyFieldName];
// lets sum values
foreach ($sumFieldNameArray as $sumFieldName)
self::deepAssign($outArray, $keys, $sumFieldName, $row[$sumFieldName], true);
foreach ($staticFieldNameArray as $staticFieldName)
self::deepAssign($outArray, $keys, $staticFieldName, $row[$staticFieldName]);
}
}
protected static function deepAssign(&$array, $keys, $fieldName, $value, $sum = false) {
$target =& $array;
foreach ($keys as $key) {
if (!isset($target[$key]))
$target[$key] = array();
$target =& $target[$key];
}
if($sum)
$target[$fieldName] += $value;
else
$target[$fieldName] = $value;
}

You should look at create_function() (docs here)

Related

Convert string to multidimensional array without eval

I saved template-variables in the DB, e.g.: slider.item1.headline1 = "Headline1".
I am using symfony framework. If I just pass "slider.item1.headline1" to the Twig template, that won't be used because the points are interpreted as a multidimensional array (or nested object) (https://symfony.com/doc/current/templates.html#template-variables).
I've now built a routine that converts the string into a multidimensional array.
But I don't like the eval(), PHP-Storm doesn't like it either, marks it as an error.
How could this be solved in a nicer way (without eval)?
Here my method:
protected function convertTemplateVarsFromDatabase($tplvars): array
{
$myvar = [];
foreach ($tplvars as $tv)
{
$handle = preg_replace('/[^a-zA-Z0-9._]/', '_', $tv['handle']);
$tplsplit = explode('.', $handle);
$mem = "";
foreach ($tplsplit as $eitem)
{
$mem .= "['" . $eitem . "']";
}
$content = $tv['htmltext'];
eval('$myvar' . $mem . ' = $content;');
}
return $myvar;
}
You can indeed avoid eval here. Maintain a variable that follows the existing array structure of $myvar according to the path given, and let it create any missing key while doing so. This is made easier using the & syntax, so to have a reference to a particular place in the nested array:
function convertTemplateVarsFromDatabase($tplvars): array
{
$myvar = [];
foreach ($tplvars as $tv)
{
$handle = preg_replace('/[^a-zA-Z0-9._]/', '_', $tv['handle']);
$tplsplit = explode('.', $handle);
$current = &$myvar;
foreach ($tplsplit as $eitem)
{
if (!isset($current[$eitem])) $current[$eitem] = [];
$current = &$current[$eitem];
}
$current = $tv['htmltext'];
}
return $myvar;
}

Resolve a multi dimensional array into fully specified endpoints

I need to turn each end-point in a multi-dimensional array (of any dimension) into a row containing the all the descendant nodes using PHP. In other words, I want to resolve each complete branch in the array. I am not sure how to state this more clearly, so maybe the best way is to give an example.
If I start with an array like:
$arr = array(
'A'=>array(
'a'=>array(
'i'=>1,
'j'=>2),
'b'=>3
),
'B'=>array(
'a'=>array(
'm'=>4,
'n'=>5),
'b'=>6
)
);
There are 6 end points, namely the numbers 1 to 6, in the array and I would like to generate the 6 rows as:
A,a,i,1
A,a,j,2
A,b,2
B,a,m,3
B,a,n,4
B,b,2
Each row contains full path of descendants to the end-point. As the array can have any number of dimensions, this suggested a recursive PHP function and I tried:
function array2Rows($arr, $str='', $out='') {
if (is_array($arr)) {
foreach ($arr as $att => $arr1) {
$str .= ((strlen($str)? ',': '')) . $att;
$out = array2Rows($arr1, $str, $out);
}
echo '<hr />';
} else {
$str .= ((strlen($str)? ',': '')) . $arr;
$out .= ((strlen($out)? '<br />': '')) . $str;
}
return $out;
}
The function was called as follows:
echo '<p>'.array2Rows($arr, '', '').'</p>';
The output from this function is:
A,a,i,1
A,a,i,j,2
A,a,b,3
A,B,a,m,4
A,B,a,m,n,5
A,B,a,b,6
Which apart from the first value is incorrect because values on some of the nodes are repeated. I have tried a number of variations of the recursive function and this is the closest I can get.
I will welcome any suggestions for how I can get a solution to this problem and apologize if the statement of the problem is not very clear.
You were so close with your function... I took your function and modified is slightly as follows:
function array2Rows($arr, $str='', $csv='') {
$tmp = $str;
if (is_array($arr)) {
foreach ($arr as $att => $arr1) {
$tmp = $str . ((strlen($str)? ', ': '')) . $att;
$csv = array2Rows($arr1, $tmp, $csv);
}
} else {
$tmp .= ((strlen($str)? ', ': '')) . $arr;
$csv .= ((strlen($csv)? '<br />': '')) . $tmp;
}
return $csv;
}
The only difference is the introduction of a temporary variable $tmp to ensure that you don't change the $str value before the recursion function is run each time.
The output from your function becomes:
This is a nice function, I can think of a few applications for it.
The reason that you are repeating the second to last value is that in your loop you you are appending the key before running the function on the next array. Something like this would work better:
function array2Rows($arr, &$out=[], $row = []) {
if (is_array($arr)) {
foreach ($arr as $key => $newArray) {
if (is_array($newArray)) {
$row[] = $key; //If the current value is an array, add its key to the current row
array2Rows($newArray, $out, $row); //process the new value
} else { //The current value is not an array
$out[] = implode(',',array_merge($row,[$key,$newArray])); //Add the current key and value to the row and write to the output
}
}
}
return $out;
}
This is lightly optimized and utilizes a reference to hold the full output. I've also changed this to use and return an array rather than strings. I find both of those changes to make the function more readable.
If you wanted this to return a string formatted similarly to the one that you have in your function, replace the last line with
return implode('<br>', $out);
Alternatively, you could do that when calling, which would be what I would call "best practice" for something like this; e.g.
$result = array2Rows($arr);
echo implode('<br>', $result);
Note, since this uses a reference for the output, this also works:
array2Rows($arr, $result);
echo implode('<br>', $result);

Perform operations (code) on each item of an array we are imploding without traversing the array twice?

Is there a way to perform operations on each items of an array we are imploding without traversing the array twice?
I've run into lambda-based solutions but it traverses the array twice (unless I'm wrong):
$array = array('some','boring','items');
$func = function($arr){
$return = array();
foreach ($arr as $item) {
$return[] = ucfirst($item);
}
return $return;
};
echo ' ' . implode('#', $func($array));
A pretty old report exists on PHP bugtracker but no practical solution were given.
I would like to avoid recoding implode like such:
$iter = new ArrayIterator($array);
while ($iter->valid()) {
echo ucfirst($iter->current());
$iter->next();
if ($iter->valid()) {
echo '#';
}
}
Sure, why not. Just use function call in-place, like:
$array = array('some','boring','items');
$result = substr(array_reduce($array, function(&$cur, $x)
{
return $cur.='#'.ucfirst($x);
}, ''), 1);
Alternatively (if you want to avoid even string overhead when doing substr()) - use
$result = ucfirst(array_shift($array)).array_reduce($array, function(&$cur, $x)
{
return $cur.='#'.ucfirst($x);
}, '');
-less "beautiful" - but certainly will use each element only once.

Variable erase in foreach when concatenating

I have looked over the internet for this but the problem only occurs with somebody when trying to pass an array to foreach and alter it inside. My problem is much easier and confusing.
I have 2 global variables:
$type="";
$rule="";
And i have this code:
foreach($cartasxml->children() as $child) {
$str="insert into cards (title,cost,color,loyalty,type,pow,tgh,hand,life,rules,set,rarity,number,artist,flavor,cost) values('{{{".$child->name."}}}',";
if(isset($child->typelist))
{
foreach($child->typelist as $a)
$type .= "|" . str_replace("{","[",str_replace("}","]",$a->type));
$str.="'{{{".substr($type,1)."}}}',";
}
else
$str.="NULL,";
And at the end of foreach i do:
$type="";
$rule="";
So, when i see the value of $type by printing $str it only shows the last one i included. Maybe an escope problem, but i still don't know how to solve it.
Somebody?
May be you can use array to push all the values into it
$type_array = array();
$str_array = array();
foreach($cartasxml->children() as $child) {
$str="insert into cards (title,cost,color,loyalty,type,pow,tgh,hand,life,rules,set,rarity,number,artist,flavor,cost) values('{{{".$child->name."}}}',";
if(isset($child->typelist))
{
foreach($child->typelist as $a)
$type .= "|" . str_replace("{","[",str_replace("}","]",$a->type));
$str.="'{{{".substr($type,1)."}}}',";
array_push($type_array,$type);
array_push($str_array,$str);
}
else
$str.="NULL,";
print_r($type_array);
print_r($str_array);
Try this:
$type = implode('|',
array_map(function($a)
{ return str_replace("{","[",str_replace("}","]",$a->type)) },
$child->typelist));
$str .= "'{{{".$type."}}}',";

How to use a foreach loop, but do something different on the last iteration?

This is probably a simple question, but how do you iterate through an array, doing something to each one, until the last one and do something different?
I have an array of names. I want to output the list of names separated by commas.
Joe, Bob, Foobar
I don't want a comma at the end of the last name in the array, nor if there is only one value in the array (or none!).
Update: I can't use implode() because I have an array of User model objects where I get the name from each object.
$users = array();
$users[] = new User();
foreach ($users as $user) {
echo $user->name;
echo ', ';
}
How can I achieve this and still use these objects?
Update: I was worrying too much about how many lines of code I was putting in my view script, so I decided to create a view helper instead. Here's what I ended up with:
$array = array();
foreach($users as $user) {
$array[] = $user->name;
}
$names = implode(', ', $array);
Use implode:
$names = array('Joe', 'Bob', 'Foobar');
echo implode(', ', $names); # prints: Joe, Bob, Foobar
To clarify, if there is only one object in the array, the ', ' separator will not be used at all, and a string containing the single item would be returned.
EDIT: If you have an array of objects, and you wanted to do it in a way other than a for loop with tests, you could do this:
function get_name($u){ return $u->name; };
echo implode(', ', array_map('get_name', $users) ); # prints: Joe, Bob, Foobar
$array = array('joe', 'bob', 'Foobar');
$comma_separated = join(",", $array);
output: joe,bob,Foobar
Sometimes you might not want to use implode.
The trick then is to use an auxiliary variable to monitor not the last, but the first time through the loop.
vis:
$names = array('Joe', 'Bob', 'Foobar');
$first = true;
$result = '';
foreach ($names as $name)
{
if (!$first)
$result .= ', ';
else
$first = false;
$result .= $name;
}
implode(', ', $array_of_names)
psuedocode....
integer sigh=container.getsize();
sigh--;
integer gosh=0;
foreach element in container
{
if(gosh!=sigh)
dosomething();
else
doLastElementStuff();
gosh++;
}
looking at all the other answers, it seems PHP has gotten a lot more syntactic S since I last wrote anything in it :D
I come accross this a lot building SQL statements etc.
$joiner = " ";
foreach ($things as $thing) {
echo " $joiner $thing \n";
$joiner = ',';
}
FOr some reason its easier to work out the logic if you think of the ",", "AND" or "OR" as an option/attribute that goes before an item. The problem then becomes how to suppress the the "," on the first line.
I personally found the fastest way (if you're into micro optimization) is:
if(isset($names[1])) {
foreach ($names as $name) {
$result .= $name . ', ';
}
$result = substr($result, 0, -2);
} else {
$result = $names[0];
}
isset($names[1]) is the fastest (albeit not so clear) way of checking the length of an array (or string). In this case, checking for at least two elements is performed.
I actually find it easier to create my comma delimited text a little differently. It's a bit more wordy, but it's less function calls.
<?php
$nameText = '';
for ($i = 0; $i < count($nameArray); $i++) {
if ($i === 0) {
$nameText = $nameArray[$i];
} else {
$nameText .= ',' . $nameArray[$i];
}
}
It adds the comma as a prefix to every name except where it's the first element if the array. I have grown fond of using for as opposed to foreach since I have easy access to the current index and therefore adjacent elements of an array. You could use foreach like so:
<?php
$nameText = '';
$nameCounter = 0;
foreach ($nameArray as $thisName) {
if ($nameCounter === 0) {
$nameText = $thisName;
$nameCounter++;
} else {
$nameText .= ',' . $thisName;
}
}

Categories