I can access anywhere inside the multi-dimensional an array via reference method. And I can change the its value. For example:
$conf = array(
'type' => 'mysql',
'conf' => array(
'name' => 'mydatabase',
'user' => 'root',
'pass' => '12345',
'host' => array(
'127.0.0.1',
'88.67.45.123',
'129.34.123.55'
),
'port' => '3306'
)
);
$value = & $this->getFromArray('type.conf.host');
$value = '-- changed ---';
// result
$conf = array(
'type' => 'mysql',
'conf' => array(
'name' => 'mydatabase',
'user' => 'root',
'pass' => '12345',
'host' => '-- changed ---'
'port' => '3306'
)
);
BUT, I can't destroy the that section:
// normally success
unset($conf['type']['conf']['host']);
// fail via reference
$value = & $this->getFromArray('type.conf.host');
unset($value);
Is there a solution?
Ok, better answer I think. In order to unset , you should get a reference to the container array, then unset the element within the array;
i.e.
$value = & $this->getFromArray('type.conf');
unset $value['host'];
References are not like hard-links. If you unset a reference, this will not unset the origin value.
<?php
$a = 5;
xdebug_debug_zval('a'); // a: (refcount=1, is_ref=0), int 5
$b = &$a;
xdebug_debug_zval('a'); // a: (refcount=2, is_ref=1), int 5
xdebug_debug_zval('b'); // b: (refcount=2, is_ref=1), int 5
unset($b);
xdebug_debug_zval('a'); // a: (refcount=1, is_ref=0), int 5
Why not write a little Config class which abstracts the data (array)? Since objects are always passed by reference you won't need to handle this at your own.
class Config
{
// ...
}
$config = new Config(array(
'db' => array(
'name' => 'mydatabase',
'user' => 'root',
'pass' => '12345',
)
));
$config->get('db.user');
$config->set('db.user', 'newuser');
$config->unset('db.user');
//...
Here is my function for unsetting nested keys
public function unsetKey(string $dotSeparatedKey)
{
$keys = explode('.', $dotSeparatedKey);
$pointer = &$this->data;
$current = false; // just to make code sniffer happy
// we traverse all but the last key
while (($current = array_shift($keys)) && (count($keys) > 0)) {
// if some key is missing all the subkeys will be already unset
if (!array_key_exists($current, $pointer)) {
// is already unset somewhere along the way
return;
}
// set pointer to new, deeper level
// called for all but last key
$pointer = &$pointer[$current];
}
// handles empty input string
if ($current) {
// we finally unset what we wanted
unset($pointer[$current]);
}
}
Creating some functions for my framework, think they halp you.
1. Function set value in array using reference for navigation
- if reference ending by name/key, this name/key will be equal setting value
- if reference ending by delimiter, last name/key will be array with setting value
function array_reference_set($input_arr=array(),$reference='',$delimiter='->',$set_var=''){
switch ($reference){
case (is_string($reference)):
$reference = array_reverse(explode($delimiter, $reference),true);
break;
case (!is_array($reference)):
return $input_arr;
}
$key = array_pop($reference);
if (count($reference)<1){
if($key!=''){
$input_arr[$key] = $set_var;
}elseif (!is_array($input_arr) && $key==''){
$input_arr = array($set_var);
}elseif ($key==''){
$input_arr[] = $set_var;
}
}else{
if (!is_array($input_arr)){
$input_arr = array($key=>array());
}
if (isset($input_arr[$key])){
$input_arr[$key] = $this->array_reference_set($input_arr[$key],$reference,$delimiter,$set_var);
}else{
$input_arr[$key] = $this->array_reference_set(array(),$reference,$delimiter,$set_var);
}
}
return $input_arr;
}
$arr = array_reference_set(array(),'a->b->c','->','test');
//equal
$arr = array('a'=>array('b'=>array('c'=>'test')));//or
$arr['a']['b']['c'] = 'test';
$arr = array_reference_set(array(),'a->b->c->','->','test');
//equal
$arr = array('a'=>array('b'=>array('c'=>array('test'))));//or
$arr['a']['b']['c'][] = 'test';
2. Function set unset value from array using reference
- if reference ending is delimiter, then will be unset varible with name/key befor delimiter
- one moment of using this function: you need update array by returned result of function (in the end of code example)
function array_reference_unset($input_arr=array(),$reference='',$delimiter='->'){
switch ($reference){
case (is_string($reference)):
$reference = array_reverse(explode($delimiter, $reference),true);
break;
case (!is_array($reference)):
return $input_arr;
}
$key = array_pop($reference);
if (count($reference)<1 && is_string($key)){
if ($key!=''){
unset($input_arr[$key]);
}else{
return false;
}
}else{
if (isset($input_arr[$key])){
$ret = $this->array_reference_unset($input_arr[$key],$reference,$delimiter);
if ($ret!==false){
$input_arr[$key] = $ret;
}else{
unset ($input_arr[$key]);
}
}
}
return $input_arr;
}
$arr = array('a'=>array('b'=>array('c'=>'test')));// test subject
$arr = array_reference_unset($arr,'a->b->c','->');//and
$arr = array_reference_unset($arr,'a->b->c->','->');
//equal
unset($arr['a']['b']['c']);
p.s. sorry for my pure English
Related
Given a string that contains values separated by dots:
property.entry.item
What is the best way to convert that to a key for an associative array?
$result['imported_data']['property']['entry']['item']
The string may be of any length, with any number of dots and contain an value:
people.arizona.phoenix.smith
I've tried the following without success:
//found a dot, means we are expecting output from a previous function
if( preg_match('[.]',$value)) {
//check for previous function output
if(!is_null($result['import'])) {
$chained_result_array = explode('.',$value);
//make sure we have an array to work with
if(is_array($chained_result_array)) {
$array_key = '';
foreach($chained_result_array as $key) {
$array_key .= '[\''.$key.'\']';
}
}
die(print_r(${result.'[\'import\']'.$array_key}));
}
}
I was thinking I could convert the string to a variable variable, but I get an array to string conversion error.
You can explode the string into an array and loop through the array. (DEMO)
/**
* This is a test array
*/
$testArray['property']['entry']['item'] = 'Hello World';
/**
* This is the path
*/
$string = 'property.entry.item';
/**
* This is the function
*/
$array = explode('.', $string);
foreach($array as $i){
if(!isset($tmp)){
$tmp = &$testArray[$i];
} else {
$tmp = $tmp[$i];
}
}
var_dump( $tmp ); // output = Hello World
Split the string into parts, and itterate the array, accessing each element in turn:
function arrayDotNotation($array, $dotString){
foreach(explode('.', $dotString) as $section){
$array = $array[$section];
}
return $array;
}
$array = ['one'=>['two'=>['three'=>'hello']]];
$string = 'one.two.three';
echo arrayDotNotation($array, $string); //outputs hello
Live example: http://codepad.viper-7.com/Vu8Hhy
You should really check to see if keys exist before you reference them. Otherwise, you're going to spew a lot of warnings.
function getProp($array, $propname) {
foreach(explode('.', $propname) as $node) {
if(isset($array[$node]))
$array = &$array[$node];
else
return null;
}
return $array;
}
Now you can do things like:
$x = array(
'name' => array(
'first' => 'Joe',
'last' => 'Bloe',
),
'age' => 27,
'employer' => array(
'current' => array(
'name' => 'Some Company',
)
)
);
assert(getProp($x, 'age') == 27);
assert(getProp($x, 'name.first') == 'Joe');
assert(getProp($x, 'employer.current.name') == 'Some Company');
assert(getProp($x, 'badthing') === NULL);
assert(getProp($x, 'address.zip') === NULL);
Or, if you are only interested in the import section of the tree:
getProp($x['import'], 'some.path');
While looking for a solution to identify duplicates in an array I stumpled upon many kinds of solutions counting on array_count_values or array_unique. But all of these solutions doesn't care about objects in the array.
array_count_values throws an E_WARNING for every value which isn't a string or an integer.
array_unique does take care about elements with various types if the option SORT_REGULAR has been set. But take a look at the use case as follows.
class Foo
{
private $value;
public function __construct( $value )
{
$this->value = $value;
}
}
$f1 = new Foo( 42 );
$f2 = $f1;
$f3 = new Foo( 42 );
$f4 = new Foo( '42' );
$f5 = new Foo( 'Bar' );
$a = [ $f1, $f2, $f3, $f4, $f5 ];
After a unification with array_unqiue I expected to get an array with 4 elements [ $f1, $f3, $f4, $f5 ]. But it states out, that array_unqiue is working loose-typed and I got [ $f1, $f5 ] which isn't the result I need.
In my case I wrote a collection working like a set. I can pass some initial elements. These elements should be validated. If one element is a duplicate an exception have to be thrown. In order of the loose-typed array_unqiue I came up with this solution (which can be adapted very easy to unify an array).
$boundN = count( $elements );
$boundM = $boundN - 1;
for ( $m = 0; $m < $boundM; $m++ )
{
for ( $n = $m + 1; $n < $boundN; $n++ )
{
if ( $elements[ $m ] === $elements[ $n ] )
{
throw new DuplicateElementException( 'The initial values contain duplicates.' );
}
}
}
At least I minified the iterations in the inner loop. One can assume, that all passed elements in the outer loop are validated and don't have to be validated again.
My question is: Is there a shorter algorithm equal to algorithms like Quick Search or something?
In your example, it's the specific instance of each object which is unique. The spl_object_id method can get a unique identifier for each object, and you can use those as keys in an associative array to collapse for duplicates. There are a few shorthand ways to write it, but a self-contained example might be:
<?php
class Foo {
private $data;
public function __construct($data) {
$this -> data = $data;
}
}
$f1 = new Foo( 42 );
$f2 = $f1;
$f3 = new Foo( 42 );
$f4 = new Foo( '42' );
$f5 = new Foo( 'Bar' );
$a = [ $f1, $f2, $f3, $f4, $f5 ];
$b = obj_unique($a);
print_r($b);
function obj_unique(array $not_unique) {
$tmp = [];
foreach($not_unique as $value) {
$tmp[spl_object_id($value)] = $value;
}
return array_values($tmp);
}
This creates the following output, which is missing the duplicate values.
Array
(
[0] => Foo Object
(
[data:Foo:private] => 42
)
[1] => Foo Object
(
[data:Foo:private] => 42
)
[2] => Foo Object
(
[data:Foo:private] => 42
)
[3] => Foo Object
(
[data:Foo:private] => Bar
)
)
This idea could be trivially modified to throw an exception if the array already contains the key.
if(contains_duplicates($a)) {
throw new Exception("Duplicates are bad etc etc ...");
}
function contains_duplicates(array $test) {
$tmp = [];
foreach($test as $value) {
$key = spl_object_id($value);
if(array_key_exists($key, $tmp)) {
// duplicates
return true;
}
$tmp[$key] = $value;
}
// no duplicates
return false;
}
The === operator on an Object has the same behaviour as this. It is an instance-wise comparison, not a comparison of the contents of the object, which is something you should be aware of.
This looks like the XY problem.
Since your code is looking for duplicate instances (===) rather than just objects containing the same data, these objects must be instantiated at run time. Since you are using a numerically indexed array it suggests you are not concerned with preserving information in the array index. Hence the most appropriate solution would be to apply a method of array indexing that ensures uniqueness as you add entries to the array:
$f1 = new Foo( 42 );
$f2 = $f1;
$f3 = new Foo( 42 );
$f4 = new Foo( '42' );
$f5 = new Foo( 'Bar' );
$a = [
spl_object_hash($f1)=>$f1,
spl_object_hash($f2)=>$f2,
spl_object_hash($f3)=>$f3,
spl_object_hash($f4)=>$f4,
spl_object_hash($f5)=>$f5
];
I have the following code:
function filterUsers(array $setOfAllUsers) {
if (empty($setOfAllUsers)) {
return array(array(), array());
}
$activeUsers = array();
$inactiveUsers = array();
foreach($setOfAllUsers as $userRow) {
$var = ($userRow['IsActive'] ? '' : 'in') . 'activeUsers';
$$var[$userRow['CID']]['Label'] = $userRow['UserLabel'];
// Error happens here ---^
$$var[$userRow['CID']]['UserList'][$userRow['UID']] = array(
'FirstName' => $userRow['FName'],
'LastName' => $userRow['LName'],
... More data
);
}
return array($activeUsers, $inactiveUsers);
}
I get the following error: Warning: Illegal string offset 'Label' in ...
How can I fix this? I tried defining Label part first like this: $$var[$userRow['CID']] = array(); $$var[$userRow['CID']]['Label'] = ''; but did not work.
To make things clear what I am trying to achieve is this:
if ($userRow['IsActive']) {
$activeUsers[$userRow['CID']]['Label'] = $userRow['UserLabel'];
$activeUsers[$userRow['CID']]['UserList'][$userRow['UID']] = array(
'FirstName' => $userRow['FName'],
'LastName' => $userRow['LName'],
... More data
);
} else {
$inactiveUsers[$userRow['CID']]['Label'] = $userRow['UserLabel'];
$inactiveUsers[$userRow['CID']]['UserList'][$userRow['UID']] = array(
'FirstName' => $userRow['FName'],
'LastName' => $userRow['LName'],
... More data
);
}
Instead of repeating above in if/else I wanted to achieve it using $$
Try using ${$var} instead of $$var.
EDIT
From PHP Manual (http://php.net/manual/en/language.variables.variable.php):
In order to use variable variables with arrays, you have to resolve an
ambiguity problem. That is, if you write $$a[1] then the parser needs
to know if you meant to use $a[1] as a variable, or if you wanted $$a
as the variable and then the [1] index from that variable. The syntax
for resolving this ambiguity is: ${$a[1]} for the first case and
${$a}[1] for the second.
function filterUsers(array $setOfAllUsers) {
if (empty($setOfAllUsers)) {
return array(array(), array());
}
$users = array(
'inactiveUsers' => array(),
'activeUsers' => array()
);
foreach($setOfAllUsers as $userRow) {
$status = ($userRow['IsActive'] ? '' : 'in') . 'activeUsers';
$users[$status][$userRow['CID']] = array();
$users[$status][$userRow['CID']]['Label'] = $userRow['UserLabel'];
$users[$status][$userRow['CID']]['UserList'] = array(
$userRow['UID'] => array(
'FirstName' => $userRow['FName'],
'LastName' => $userRow['LName'],
)
);
}
return $users;
}
Try the following:
$myUsers = $$var;
$myUsers[...][...] = ...;
The problem with using $$var[...][...] is that first $var[...][...] is evaluated and then it tries to find the variable named $$var[...][...].
Don't use var-vars like this. You run into a PHP syntax glitch. And even if there wasn't a syntax glitch, you shouldn't be using var-vars in the first place. They make for near-impossible-to-debug code.
$x = 'foo';
$foo = array();
$$x[0] = 1;
var_dump($x); // string(3) "foo"
var_dump($foo); // array(0) { }
$$x[1][2] = 3;
PHP Notice: Uninitialized string offset: 2 in php shell code on line 1
Note how the array didn't get modified by the $$x[0] assignment, and how the 2+ dimensional assigment causes "undefined string offset". Your var-var isn't being treated as an array - it's being treated as a STRING, and failing, because it's a syntax glitch. You can treat strings as arrays, but not using var vars:
$bar = 'baz';
$bar[1] = 'q';
echo $bar; // output: bqz
There appears to be NO way to use a var-var as an array, especially a multi-dimensional array.
I am trying to check to see if any of the fields have been field out and if NONE have been field out return back to page with error. Even if I have a field filled it still returns acting like no fields are selected.
Controller
public function getServices() {
$user = User::find(Auth::user()->id);
$input = [
'rooms' => Input::get('rooms'),
'pr_deodorizer' => Input::get('pr_deodorizer'),
'pr_protectant' => Input::get('pr_protectant'),
'pr_sanitizer' => Input::get('pr_sanitizer'),
'fr_couch' => Input::get('fr_couch'),
'fr_chair' => Input::get('fr_chair'),
'pr_sectional' => Input::get('pr_sectional'),
'pr_ottoman' => Input::get('pr_ottoman'),
'pr_tile' => Input::get('pr_tile'),
'pr_hardwood' => Input::get('pr_hardwood')
];
$empty = 'No services were selected';
$var = $input['rooms']&& $input['pr_deodorizer']&&
$input['pr_protectant']&& $input['pr_sanitizer']&&
$input['fr_couch']&& $input['fr_chair']&&
$input['pr_sectional']&& $input['pr_ottoman']&&
$input['pr_tiles']&& $input['pr_hardwood'];
if(empty($var)){
return Redirect::to('book/services')->withErrors($empty)->withInput();
}
foreach($input as $services)
{
$service = new Service();
$service->userID = $user->id;
$service->services = $services;
$service->save();
}
return Redirect::to('book/schedule');
}
I have tried !isset() but I still cannot get it to work.
if you want to check if variable is empty, you should use empty() function not &&
when you using && string "0" is casted to false, this may be not what you expecting.
if you want to detect if any of keys in array is empty use this function:
function arrayEmpty($keys, $array) {
$keys = explode(" ", trim($keys));
foreach($keys as $key) {
if (!isset($array[$key]) || empty($array[$key])) return true; // isset prevents notice when $key not exists
}
return false;
}
use example:
$array = array( "foo" => "bar" );
arrayEmpty("foo", $array); // false
arrayEmpty("foo bar", $array); // $array["bar"] not exists, returns true
I want to make a class for parsing flat-file database information into one large analogous multidimensional array. I had the idea of formatting the database in a sort of python-esque format as follows:
"tree #1":
"key" "value"
"sub-tree #1":
"key" "value"
"key #2" "value"
"key #3" "value"
I am trying to make it parse this and build and array while parsing it to throw the keys/values into, and I want it to be very dynamic and expandable. I've tried many different techniques and I've been stumped in each of these attempts. This is my most recent:
function parse($file=null) {
$file = $file ? $file : $this->dbfile;
### character variables
# get values of
$src = file_get_contents($file);
# current character number
$p = 0;
### array variables
# temp shit
$a = array();
# set $ln keys
$ln = array("q"=>0,"k"=>null,"v"=>null,"s"=>null,"p"=>null);
# indent level
$ilvl = 0;
### go time
while (strlen($src) > $p) {
$chr = $src[$p];
# quote
if ($chr == "\"") {
if ($ln["q"] == 1) { // quote open?
$ln["q"] = 0; // close it
if (!$ln["k"]) { // key yet?
$ln["k"] = $ln["s"]; // set key
$ln["s"] = null;
$a[$ln["k"]] = $ln["v"]; // write to current array
} else { // value time
$ln["v"] = $ln["s"]; // set value
$ln["s"] = null;
}
} else {
$ln["q"] = 1; // open quote
}
}
elseif ($chr == "\n" && $ln["q"] == 0) {
$ln = array("q"=>0,"k"=>null,"v"=>null,"s"=>null,"p"=>null);
$llvl = $ilvl;
}
# beginning of subset
elseif ($chr == ":" && $ln["q"] == 0) {
$ilvl++;
if (!array_key_exists($ilvl,$a)) { $a[$ilvl] = array(); }
$a[$ilvl][$ln["k"]] = array("#mbdb-parent"=> $ilvl-1 .":".$ln["k"]);
$ln = array("q"=>0,"k"=>null,"v"=>null,"s"=>null,"p"=>null);
$this->debug("INDENT++",$ilvl);
}
# end of subset
elseif ($chr == "}") {
$ilvl--;
$this->debug("INDENT--",$ilvl);
}
# other characters
else {
if ($ln["q"] == 1) {
$ln["s"] .= $chr;
} else {
# error
}
}
$p++;
}
var_dump($a);
}
I honestly have no idea where to go from here. The thing troubling me most is setting the multidimensional values like $this->c["main"]["sub"]["etc"] the way I have it here. Can it even be done? How can I actually nest the arrays as the data is nested in the db file?
This is all going to depend on how human-readable you want your "flat file" to be.
Want human-readable?
XML
Yaml
Semi-human-readable?
JSON
Not really human-readable?
Serialized PHP (also PHP-only)
Mysql Dump
Writing your own format is going to be painful. Unless you want to do this purely for the academic experience, then I say don't bother.
Looks like JSON might be a happy medium for you.
$configData = array(
'tree #1' => array(
'key' => 'value'
, 'sub-tree #1' => array(
'key' => 'value'
, 'key #2' => 'value'
, 'key #3' => 'value'
)
)
);
// Save config data
file_put_contents( 'path/to/config.json', json_format( json_encode( $configData ) ) );
// Load it back out
$configData = json_decode( file_get_contents( 'path/to/config.json' ), true );
// Change something
$configData['tree #1']['sub-tree #1']['key #2'] = 'foo';
// Re-Save (same as above)
file_put_contents( 'path/to/config.json', json_format( json_encode( $configData ) ) );
You can get the json_format() function here, which just pretty-formats for easier human-reading. If you don't care about human-readability, you can skip it.
Well, you could use serialize and unserialize but that would be no fun, right? You should be using formats specifically designed for this purpose, but for sake of exercise, I'll try and see what I can come up with.
There seems to be two kinds of datatypes in your flatfile, key-value pairs and arrays. key-value pairs are denoted with two sets of quotes and arrays with one pair of quotes and a following colon. As you go through the file, you must parse each row and determine what it represents. That's easy with regular expressions. The hard part is to keep track of the level we're going at and act accordingly. Here's a function that parses the tree you provided:
function parse_flatfile($filename) {
$file = file($filename);
$result = array();
$open = false;
foreach($file as $row) {
$level = strlen($row) - strlen(ltrim($row));
$row = rtrim($row);
// Regular expression to catch key-value pairs
$isKeyValue = preg_match('/"(.*?)" "(.*?)"$/', $row, $match);
if($isKeyValue == 1) {
if($open && $open['level'] < $level) {
$open['item'][$match[1]] = $match[2];
} else {
$open = array('level' => $level - 1, 'item' => &$open['parent']);
if($open) {
$open['item'][$match[1]] = $match[2];
} else {
$result[$match[1]] = $match[2];
}
}
// Regular expression to catch arrays
} elseif(($isArray = preg_match('/"(.*?)":$/', $row, $match)) > 0) {
if($open && $open['level'] < $level) {
$open['item'][$match[1]] = array();
$open = array('level' => $level, 'item' => &$open['item'][$match[1]], 'parent' => &$open['item']);
} else {
$result[$match[1]] = array();
$open = array('level' => $level, 'item' => &$result[$match[1]], 'parent' => false);
}
}
}
return $result;
}
I won't go into greater detail on how that works, but it short, as we progress deeper into the array, the previous level is stored in a reference $open and so on. Here's a more complex tree using your notation:
"tree_1":
"key" "value"
"sub_tree_1":
"key" "value"
"key_2" "value"
"key_3" "value"
"key_4" "value"
"key_5" "value"
"tree_2":
"key_6" "value"
"sub_tree_2":
"sub_tree_3":
"sub_tree_4":
"key_6" "value"
"key_7" "value"
"key_8" "value"
"key_9" "value"
"key_10" "value"
And to parse that file you could use:
$result = parse_flatfile('flat.txt');
print_r($result);
And that would output:
Array
(
[tree_1] => Array
(
[key] => value
[sub_tree_1] => Array
(
[key] => value
[key_2] => value
[key_3] => value
)
[key_4] => value
[key_5] => value
)
[tree_2] => Array
(
[key_6] => value
[sub_tree_2] => Array
(
[sub_tree_3] => Array
(
[sub_tree_4] => Array
(
[key_6] => value
[key_7] => value
[key_8] => value
[key_9] => value
[key_10] => value
)
)
)
)
)
I guess my test file covers all the bases, and it should work without breaking. But I won't give any guarantees.
Transforming a multidimensional array to flatfile using this notation will be left as an exercise to the reader :)