PHP: Building an Adjacency List through Recursive Iteration

爱⌒轻易说出口 提交于 2019-12-25 02:59:41

问题


I'm trying to build a flattened array that preserves metadata from a pretty tricky array coming from a view in my CodeIgniter project. That metadata is things like an identifier, depth, and parent node.

The data is from a query builder JavaScript library that allows a user to generate rules that will be used in business logic. I need to persist this data, and the model I've gone with to represent the tree-like nature of these rules is an adjacency list.

Here's what I have, and it does work for most cases, but it's ugly, it's made of bubble gum and duct tape, and 'most' cases are not 'all' cases. After reading the SPL docs, I suspect a RecursiveIteratorIterator may be more suited to the problem.

Sorry for the long winded post, but I'm pretty sure my approach sucks. Any advice?

Here's the input (e.g., places I would rather not be), sample image showing it in action too:

stdClass Object
(
    [condition] => OR
    [rules] => Array
        (
            [0] => stdClass Object
                (
                    [id] => Any
                    [field] => Any
                    [type] => string
                    [input] => select
                    [operator] => not equal
                    [value] => Any
                )
            [1] => stdClass Object
                (
                    [condition] => AND
                    [rules] => Array
                        (
                            [0] => stdClass Object
                                (
                                    [id] => Place
                                    [field] => Place
                                    [type] => string
                                    [input] => select
                                    [operator] => equal
                                    [value] => France
                                )
                            [1] => stdClass Object
                                (
                                    [id] => Month
                                    [field] => Month
                                    [type] => string
                                    [input] => select
                                    [operator] => equal
                                    [value] => January
                                )
                        )
                )
            [2] => stdClass Object
                (
                    [condition] => AND
                    [rules] => Array
                        (
                            [0] => stdClass Object
                                (
                                    [id] => Place
                                    [field] => Place
                                    [type] => string
                                    [input] => select
                                    [operator] => equal
                                    [value] => Rio
                                )
                            [1] => stdClass Object
                                (
                                    [id] => Month
                                    [field] => Month
                                    [type] => string
                                    [input] => select
                                    [operator] => equal
                                    [value] =>  August
                                )
                        )
                )
            [3] => stdClass Object
                (
                    [condition] => AND
                    [rules] => Array
                        (
                            [0] => stdClass Object
                                (
                                    [id] => Place
                                    [field] => Place
                                    [type] => string
                                    [input] => select
                                    [operator] => equal
                                    [value] => Liberia
                                )
                            [1] => stdClass Object
                                (
                                    [id] => Month
                                    [field] => Month
                                    [type] => string
                                    [input] => select
                                    [operator] => equal
                                    [value] => July
                                )
                            [2] => stdClass Object
                                (
                                    [condition] => OR
                                    [rules] => Array
                                        (
                                            [0] => stdClass Object
                                                (
                                                    [id] => Year
                                                    [field] => Year
                                                    [type] => string
                                                    [input] => select
                                                    [operator] => equal
                                                    [value] => 2014
                                                )
                                            [1] => stdClass Object
                                                (
                                                    [id] => Year
                                                    [field] => Year
                                                    [type] => string
                                                    [input] => select
                                                    [operator] => equal
                                                    [value] => 2015
                                                )
                                        )
                                )
                        )
                )
        )
)

Here is the desired output for persistence. (See the values at the far right of each entry for the important bits of metadata).

Array
(
    stdClass Object ( [id] => Any [field] => Any [type] => string [input] => select [operator] => not equal [value] => Any [condition] => OR [subgroup] => 0 [parent_subgroup] => )
    stdClass Object ( [id] => Place [field] => Place [type] => string [input] => select [operator] => equal [value] => France) [condition] => AND [subgroup] => 1 [parent_subgroup] => 0 )
    stdClass Object ( [id] => Month [field] => Month [type] => string [input] => select [operator] => equal [value] => January [condition] => AND [subgroup] => 1 [parent_subgroup] => 0 )
    stdClass Object ( [id] => Place [field] => Place [type] => string [input] => select [operator] => equal [value] => Rio [condition] => AND [subgroup] => 2 [parent_subgroup] => 0 )
    stdClass Object ( [id] => Month [field] => Month [type] => string [input] => select [operator] => equal [value] => August[condition] => AND [subgroup] => 2 [parent_subgroup] => 0 )
    stdClass Object ( [id] => Place [field] => Place [type] => string [input] => select [operator] => equal [value] => Liberia [condition] => AND [subgroup] => 3 [parent_subgroup] => 0 )
    stdClass Object ( [id] => Month [field] => Month [type] => string [input] => select [operator] => equal [value] =>  July[condition] => AND [subgroup] => 3 [parent_subgroup] => 0 )
    stdClass Object ( [id] => Year [field] => Year [type] => string [input] => select [operator] => equal [value] => 2014 [condition] => OR [subgroup] => 4 [parent_subgroup] => 3 )
    stdClass Object ( [id] => Year [field] => Year [type] => string [input] => select [operator] => equal [value] => 2015 [condition] => OR [subgroup] => 4 [parent_subgroup] => 3 )    
)

Note: parses this correctly. Problems would arise if I had changed the order of subgroups 2 and 3, as the subgroup of group 3, the one that has rules (Year = 2014 OR Year = 2015) has a different nesting level and severely messes up my recursion.

Here is my code:

function deserialize_criteria_group($criteria, $subgroup = null) {
    $array = array();

    if ($subgroup == null) {
        $first_run = true;
        $subgroup = 0;
        $condition = $criteria->condition;
        $criteria = $criteria->rules;
    }

    foreach ($criteria as $rule) {
        if ($rule->rules) {
            $subgroup++;
            $children = $this->deserialize_criteria_group($rule->rules, $subgroup);
            foreach($children as $child) {
                if ($child->condition == null) {
                    $child->condition = $rule->condition;
                }
                if ($child->parent_subgroup == null) {
                    $child->parent_subgroup = $first_run ? 0 : $subgroup - 1;
                }
                    array_push($array, $child);
            }
        } else {
            $rule->condition = $condition;
            $rule->subgroup = $subgroup;
            $rule->parent_subgroup = null;
            array_push($array, $rule);
        }

    }

    if ($first_run) {
        //Ensure a root node exists, if not stub one out. 
        $criteria_group = json_decode(json_encode($array), true);
        $root_encountered = $criteria_group[0]['subgroup'] > 0 ? false : true;
        if (!$root_encountered) {
            $root = array(  'subgroup'          => 0, 
                            'parent_subgroup'   => null, 
                            'condition'         => $condition);
            array_unshift($criteria_group, $root); 
            array_unshift($array, $root);
        }

        //Ensure the ALM is not broken. 
        $subgroup = 0;
        foreach($criteria_group as $c) {
            if($c['subgroup'] > $subgroup + 1) {
                $msg = "Bad Order. Halting execution.";
                print $msg; 
                log_message('error', $msg); 
                log_message('debug', 'expected: ' . $subgroup . ' actual: ' . $c['subgroup']);
                log_message('debug', print_r($criteria_group, true));
                die;
            }
            $subgroup = $c['subgroup'];
        }
    }
    return $array;
}

回答1:


Thanks to Rocket Hazmat for the assist.

EDIT: Looks like I missed some code there, apologies.

EDIT2: There were some additional issues that arose with this approach. I show the corrections below.

Solution:

<?php
class CriteriaIterator implements RecursiveIterator{
    private $data, $counter, $condition, $subgroup, $parent_subgroup;

    public function __construct($criteriaGroup, $condition, $parent_subgroup=null){
            $this->condition = $condition;
            $this->subgroup = $GLOBALS['highest_subgroup'];
            $this->parent_subgroup = $parent_subgroup;
            $this->data = is_array($criteriaGroup) ? $criteriaGroup : array($criteriaGroup);
    }

    public function current(){
            $row = $this->data[$this->counter];
            if ($row->id) {
                    return (object) array(
                            'id' => $row->id,
                            'field' => $row->id,
                            'operator' => $row->operator,
                            'value' => $row->value,
                            'condition'=> $this->condition,
                            'subgroup' => $GLOBALS['highest_subgroup'],
                            'parent_subgroup' => $this->parent_subgroup
                    );
            }
    }

    public function key(){
            return $this->counter;
    }

    public function next(){
            $this->counter++;
    }

    public function rewind(){
            $this->counter = 0;
    }

    public function valid(){
        return $this->counter < count($this->data);
    }

    public function hasChildren(){
        $row = $this->data[$this->counter];
        return isset($row->rules);
    }

    public function getChildren(){    
        $GLOBALS['highest_subgroup']++;
        $row = $this->data[$this->counter];
        return new self($row->rules, $row->condition, $this->subgroup);
    }
}

Invoked and cleaned up afterwards like so: (got a little lazy around the end, retrofitting into CodeIgniter running PHP 5.3)

$records = new RecursiveIteratorIterator(
    new CriteriaIterator($a['criteria_group'], $a['criteria_group']->condition),
    RecursiveIteratorIterator::SELF_FIRST);

$criteria = array();
$parent_encountered = false;

// cleanup
foreach($records as $row) {
    if($row != null) {
        $row->parent_subgroup = $row->parent_subgroup == - 1 ? null : $row->parent_subgroup;
        if($row->parent_subgroup === null) {
            $parent_encountered = true;
        }
        array_push($criteria, $row);
    }
}

if(!$parent_encountered) {
    $row = array(
        'subgroup' => 0,
        'parent_subgroup' => null,
        'condition' => $a['criteria_group']->condition
    );
    array_unshift($criteria, json_decode(json_encode($row)));
}

The problems arose with this on the subgroup member. My retrieval method uses a breadth first search to create the json object to pass into the script. Unfortunately, with the nesting levels things got out of hand when resaving.

Here is one example of settings that would result in a mix up. The days before value shows the expected subgroup.

It may have been possible to fix in the recursive iterator class, but Rocket Hazmat suggested leaving that class very simple. I implemented a fix during the cleanup:

        $records = new RecursiveIteratorIterator(
                new CriteriaIterator($a['criteria_group'], $a['criteria_group']->condition), 
                RecursiveIteratorIterator::SELF_FIRST);

        $criteria = array();
        $root_encountered = false;

        // cleanup
        foreach($records as $row) {
            if($row != null) {
                if($row->parent_subgroup == - 1) {
                    $row->parent_subgroup = null;
                    $row->subgroup = 0;
                } 
                if($row->parent_subgroup === null) {
                    $root_encountered = true;
                }
                array_push($criteria, $row);
            }
        }

        if(!$root_encountered) {
            $row = (object) array(
                    'subgroup' => 0,
                    'parent_subgroup' => null,
                    'condition' => $a['criteria_group']->condition 
            );
            array_unshift($criteria, $row);
        }

        //strategy: keep a record keyed by subgroups of where they are rooted. 
        //if an entry exists for a previous subgroup but the parent subgroup conflicts
        //use the subgroup of the LAST subgroup rooted there. 
        //else update array

        $adjacency = array(0 => null); //parent
        foreach($criteria as $row) {
            if (isset($adjacency[$row->subgroup]) && $adjacency[$row->subgroup] != $row->parent_subgroup) {
                $preserved = array_reverse($adjacency, true); //need LAST subgroup rooted there
                foreach($preserved as $key=>$val) {
                    if ($val == $row->parent_subgroup) {
                        $row->subgroup = $key;
                        break;
                    }
                }
            } else {
                $adjacency[$row->subgroup] = $row->parent_subgroup;
            }
        }


来源:https://stackoverflow.com/questions/32901351/php-building-an-adjacency-list-through-recursive-iteration

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!