开发者

PHP: Modifying array with unknown structure at runtime; what is the most elegant solution?

PROBLEM

I have a function that takes in a nested array where the structure and nesting of the array is unknown at run-time. All that is known is some of the target fieldnames and desired values of some of the leafs.

QUESTIONS

1) I am hoping to modify this unknown structure and still have the code be readable and easily understood by fellow programmers. What (if any) solution will allow me to do things like this in PHP?

// Pseudo-code for things I would like to be able to do
// this is kinda like the same thing as XPATH, but for native PHP array

// find *every* fname with value of "Brad" and change it to "Brian"
$mydata->find_all('*:fname')->where_value_eq('Brad')->set_equal_to('Brian');

// find *the first* fave_color and set it to "Green"
$mydata->find('*:fave_color')->get(0)->set_equal_to('Green');

2) If there is nothing out there that will let me do this, is there something, anything, that at least comes close to the spirit of what I am hoping to accomplish here?

SAMPLE ARRAY

$mydata = array(
   'people' => array(
      array('fname'=>'Alice'),
      array('fname'=>'Brad'),
      array('fname'=>'Chris'),
    ),
   'animals' => array(
      array('fname'=>'Dino'),
      array('fname'=>'Lassie'),
      array('fname'=>'Brad'),
    ),
    'settings' => array(
      'user_prefs'=>array(
        'localhost'=>array(
          'fave_color'=>'blue',
        ),
      ),
    ),
    'places' => array(
      array('state'=>'New york',
            'cities'=>array(
              'name'=>'Albany',
              'name'=>'Buffalo',
              'name'=>'Corning',
            ),
   开发者_如何学Go         'state'=>'California',
            'cities'=>array(
              'name'=>'Anaheim',
              'name'=>'Bakersfield',
              'name'=>'Carlsbad',
            ),            
      ),
    ),
);


Although I maintain that you should stick with explicit manipulation as in my previous answer, boredom and intrigue got the better of me ;)

It probably has holes (and clearly lacks docs) but if you insist on this route, it should get you started:

class Finder {
    protected $data;

    public function __construct(&$data) {
        if (!is_array($data)) {
            throw new InvalidArgumentException;
        }

        $this->data = &$data;
    }

    public function all() {
        return $this->find();
    }

    public function find($expression = null) {
        if (!isset($expression)) {
            return new Results($this->data);
        }

        $results = array();
        $this->_find(explode(':', $expression), $this->data, $results);
        return new Results($results);
    }

    protected function _find($parts, &$data, &$results) {
        if (!$parts) {
            return;
        }

        $currentParts = $parts;
        $search = array_shift($currentParts);
        if ($wildcard = $search == '*') {
            $search = array_shift($currentParts);
        }

        foreach ($data as $key => &$value) {
            if ($key === $search) {
                if ($currentParts) {
                    $this->_find($currentParts, $value, $results);
                } else {
                    $results[] = &$value;
                }
            } else if ($wildcard && is_array($value)) {
                $this->_find($parts, $value, $results);
            }
        }
    }
}

class Results {
    protected $data;

    public function __construct(&$data) {
        $this->data = $data;
    }

    public function get($index, $limit = 1) {
        $this->data = array_slice($this->data, $index, $limit);
        return $this;
    }

    public function set_equal_to($value) {
        foreach ($this->data as &$datum) {
            $datum = $value;
        }
    }

    public function __call($method, $args) {
        if (!preg_match('/^where_?(key|value)_?(eq|contains)$/i', $method, $m)) {
            throw new BadFunctionCallException;
        }

        if (!isset($args[0])) {
            throw new InvalidArgumentException;
        }

        $operand = $args[0];
        $isKey = strtolower($m[1]) == 'key';
        $method = array('Results', '_compare' . (strtolower($m[2]) == 'eq' ? 'EqualTo' : 'Contains'));

        $ret = array();
        foreach ($this->data as $key => &$datum) {
            if (call_user_func($method, $isKey ? $key : $datum, $operand)) {
                $ret[] = &$datum;
            }
        }

        $this->data = $ret;
        return $this;
    }

    protected function _compareEqualTo($value, $test) {
        return $value == $test;
    }

    protected function _compareContains($value, $test) {
        return strpos($value, $test) !== false;
    }
}

$finder = new Finder($mydata);
$finder->find('*:fname')->where_value_eq('Brad')->set_equal_to('Brian');
$finder->find('*:fave_color')->get(0)->set_equal_to('Green');
$finder->find('places:*:cities:*:name')->where_value_contains('ba')->set_equal_to('Stackoton');

print_r($mydata);


There's certainly no native solution for this and the syntax is rather strange. If you want the code to "be readable and easily understood by fellow programmers" please stick to methods that we're used to working with ;)

foreach ($mydata as $type => &$data) {
    foreach ($data as &$member) {
        if (isset($member['fname']) && $member['fname'] == 'Brad') {
            $member['fname'] = 'Brian';
        }
    }
}

It's admittedly more verbose, but there's much less chance of confusion.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜