解析"平坦"数组到表示目录结构的数组

时间:2016-07-09 04:24:03

标签: php arrays json parsing tree

我有一个"目录对象列表"看起来像这样:

$directoryObjects = [
    [
        'type' => 'folder',
        'name' => 'animals',
        'path' => '/animals',
        'path_array' => ['animals']
    ],

    [
        'type' => 'folder',
        'name' => 'cat',
        'path' => '/animals/cat',
        'path_array' => ['animals', 'cat']
    ],

    [
        'type' => 'folder',
        'name' => 'images',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ],

    [
        'type' => 'file',
        'name' => 'cat001.png',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ],

    [
        'type' => 'file',
        'name' => 'cat002.png',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ]
];

这是我的SQL数据库输出的,但它必须在我的API响应中以树形结构格式化。我已经决定在这个帖子的问题中概述了表示这种树结构的最佳方式:Convert a directory structure in the filesystem to JSON with Node.js。执行print_r(json_decode($jsonTreeStructure))输出:

Array
(
    [0] => stdClass Object
        (
            [type] => folder
            [name] => animals
            [path] => /animals
            [children] => Array
                (
                    [0] => stdClass Object
                        (
                            [type] => folder
                            [name] => cat
                            [path] => /animals/cat
                            [children] => Array
                                (
                                    [0] => stdClass Object
                                        (
                                            [type] => folder
                                            [name] => images
                                            [path] => /animals/cat/images
                                            [children] => Array
                                                (
                                                    [0] => stdClass Object
                                                        (
                                                            [type] => file
                                                            [name] => cat001.jpg
                                                            [path] => /animals/cat/images/cat001.jpg
                                                        )

                                                    [1] => stdClass Object
                                                        (
                                                            [type] => file
                                                            [name] => cat001.jpg
                                                            [path] => /animals/cat/images/cat002.jpg
                                                        )

                                                )

                                        )

                                )

                        )

                )

        )

)

我希望将$directoryObjects格式化为上面的输出。当我执行json_encode($output)时,它应该以上面链接的线程中显示的格式输出

[
  {
    type: "folder",
    name: "animals",
    path: "/animals",
    children: [
      {
        type: "folder",
        name: "cat",
        path: "/animals/cat",
        children: [
          {
            type: "folder",
            name: "images",
            path: "/animals/cat/images",
            children: [
              {
                type: "file",
                name: "cat001.jpg",
                path: "/animals/cat/images/cat001.jpg"
              }, {
                type: "file",
                name: "cat001.jpg",
                path: "/animals/cat/images/cat002.jpg"
              }
            ]
          }
        ]
      }
    ]
  }
];

我很难开始。我很尴尬地发布到目前为止我所做的事情,但就是这样:

$jsonDir = [];  
foreach($directoryObjects as $i => $dirObj)
    {
        $cur = &$jsonDir;

        foreach ($dirObj['path_array'] as $i => $dirName) 
        {
            $cur = &$jsonDir[$i];
        }
        $cur[] = $dirObj;

    }

我很难到达子节点并在$dirObj附加正确的位置。

2 个答案:

答案 0 :(得分:1)

我喜欢你的问题,我有时间编写代码...但是这假设您的目录数组按路径从上到下排序

<?php
$directoriesArray = [
    [
        'type' => 'folder',
        'name' => 'animals',
        'path' => '/animals',
        'path_array' => ['animals']
    ],

    [
        'type' => 'folder',
        'name' => 'cat',
        'path' => '/animals/cat',
        'path_array' => ['animals', 'cat']
    ],

    [
        'type' => 'folder',
        'name' => 'images',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ],

    [
        'type' => 'file',
        'name' => 'AtlasX.png',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ],

    [
        'type' => 'file',
        'name' => 'AtlasX.png',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ]
];
    class fileObj
    {
        public $type;
        public $name;
        public $path;

        public function __construct( array $directoryArray )
        {
            $this->name = $directoryArray['name'];
            $this->type = $directoryArray['type'];
            $this->path = $directoryArray['path'];
        }
    }

    class directoryObj
    {
        public $type;
        public $name;
        public $path;
        public $children = array();

        public function __construct( array $directoryArray )
        {
            $this->name = $directoryArray['name'];
            $this->type = $directoryArray['type'];
            $this->path = $directoryArray['path'];
        }

        public function addChild( $child, $directory = null ){

            if(  !count($this->children) ){
                $this->createAndAddToChildren($child);
                return;
            }
            $sameChild = array_filter(
                    $this->children,
                    function( $savedChild ) use ( $child ){
                        switch($savedChild->type){
                            case 'folder':
                                return array_search($savedChild->name, $child['path_array']) !== false;
                                break;
                            case 'file':
                                return $savedChild->name == $child['name'] ;
                                break;
                        }
                    }
            );

            if(count($sameChild)){
                $myChild = array_shift($sameChild);
                if( $myChild->type == 'folder' ){
                    $myChild->addChild($child);
                }
            }
            else{
                $this->createAndAddToChildren($child);
            }

        }

        private function createAndAddToChildren($child){
            switch($child['type']){
                case 'folder':
                    echo 'addedDirectory <br/>';
                    $this->children[] = new directoryObj($child);
                    break;
                case 'file':
                    echo 'addedFile <br/>';
                    $this->children[] = new fileObj($child);
                    break;
            }
        }

    }

  $mainDirectory = new directoryObj(array_shift($directoriesArray));
  foreach( $directoriesArray as $directoryArray ){
        $mainDirectory->addChild($directoryArray);
    }

希望这个帮助: - )

祝你好运

答案 1 :(得分:1)

这是树步行。但是,我意识到大型文件列表会很慢。而且,无效路径怎么样?这里提供了试图解决这些问题的最终结果。

Demonstration of the code and data at eval.in

Source code at pastebin

记录代码以解释为什么以及如何做事。其中一些是“笨拙的”。即FindParent,因为它必须识别错误。

说明:

问题:

1)输出树中没有基于数据的数组键。这意味着所有树都必须搜索以找到任何特定的父级。我怀疑大树可能会变慢。我认为值得做额外的工作来避免这种情况。

2)我想知道在确定尝试识别添加到树中的无效节点时会有多大的额外复杂性。

文件夹:

  • 首先构建文件夹树 - 递归

好的,需要确保文件夹的顺序正确并且是列表中的第一个。

  • 对目录进行排序,使文件夹首先按路径长度排序。

  • 将文件夹添加到树中。

对于每个文件夹:

  • 找到父节点 - 如果找不到,则返回错误。

  • 将其添加到树中,并使用路径作为数组键将其添加到单独的数组($nodeRefs)中。这很有用,因为我们可以稍后快速找到文件的父级。

  • 无效的父母会被添加到$errors数组中,以便日后报告。

文件:

这些也是排序的,但它实际上并不重要,因为我们可以通过使用“路径”快速找到父母。作为在$nodeRefs中查找父项的关键。

$nodeRefs中找不到的任何文件都必须是错误。

BuildTree类:

/* -------------------------------------------------------------------------------------------------------
 * Build the tree folders first 
 *
 * a) I am concerned about efficiency on trees with large numbers of files. 
 *    To deal with this i keep a list of folder nodes so the file insert will be efficient.
 *
 * b) I am concerned about folder errors such as duplicates or invalid paths so I have attempted to deal with these. 
 * 
 * c) The root node is optional in the source data. It just creates one. 
 * 
 * To simplify the tree building logic I will sort the array into some useful order for me.
 *
 * Specifically:
 *
 *   1) Folders
 *      a) path length order
 *
 *   2) Files
 *      b) Path length order
 *   
 *   This will make the tree building easier but the cost is a sort plus the cost of the building the tree
 *
 *      
 */

class BuildTree
{
    /**
     *  The source data
     * 
     *  @var array 
     */
    private $directories = array();

    /**
    * References to the folders in the directory list. 
    * Makes adding files a lot more efficient 
    * 
    * @var array 
    */
    private $nodeRefs = array();    

    /**
    * Invalid nodes - there is am error message as to why it failed 
    * 
    * @var array  
    */
    private $errors = array();    

    /**
    * The tree - all the nodes are references rather than copies of the data 
    * 
    * @var array 
    */    
    private $tree = null;

    /**
    * The root node to make the tree complete
    * 
    * @var array
    */
    private $rootNode = array(
                        'type' => 'folder',
                        'name' => 'root',
                        'path' => '/',
                        'path_array' => array(),
                        'children' => array(),
                        );

    /**
    * The start index of the first file in the sorted input 
    * 
    * @var integer
    */
    private $firstFile = -1;


    /**
    * Sort the directory input in folders by length and then files
    * 
    * @param array $directories
    * 
    * @return void
    */
    public function __construct($directories) 
    {

        $this->sort($directories);
        $this->directories = $directories;
        $this->tree = &$this->rootNode; // make the tree a 'complete tree' - simplifies the addNode logic    
    }

    /**
    * Just executes the:
    *   1) the process folders (build the tree of directory nodes 
    *   2) adds the files to the correct nodes
    * 
    * @return boolean eorros or not
    */
    public function buildTree()
    {
        $this->buildFolderTree();
        $this->addFiles();            
        return count($this->errors) <= 0;
    }


   /**
    * All the folders are at the top of the directories list 
    * 
    * Read through the list and add all the folders into the tree
    * 
    * Foreach folder:
    *   1) Find the parent 
    *   2) if valid add to the tree
    *   3) record the reference to it so we can add files easily later
    *  
    * @return void
    */
    public function buildFolderTree()
    {
        // add rootnode to the list
        $this->nodeRefs[$this->tree['path']] =& $this->tree;

        foreach ($this->directories as $idx => &$node) {

            if ($node['type'] !== 'folder') { // record the index of the first file 
                $this->firstFile = $idx; // needed for processing the files efficiently
                break;
            }

            if (empty($node['path_array'])) { // you passed a root anyway - ignore it ;-/
                continue;
            }         

            $node['children'] = array();  // needed for adding nodes to the tree        
            $result = $this->findParent($node, $this->tree);

            if ($result['isError'] || !$result['isParent']) { // ignore as invalid...
                $this->errors[] = $result;
                continue; 
            }

            // add to the tree as a reference...

            $parent =& $result['treeNode'];
            $parent['children'][] =& $this->directories[$idx]; // reference to the original node 

            // fast lookup later... when we add files
            $this->nodeRefs[$node['path']] =& $node;
        }
        unset($node); // magic - 'cos we used a reference in a foreach loop.
    }


    /**
    * This does not need to do a treewalk to find the parent. 
    * 
    * All the parents are in the $nodeRefs array with a key of the `path`
    * 
    * This makes inserting the files quick 
    * 
    * @return void
    */     
    public function addFiles()
    {
        foreach (array_slice($this->directories, $this->firstFile) as $idx => $file)  {

            if (isset($this->nodeRefs[$file['path']])) { // add to the children

                $this->nodeRefs[$file['path']]['children'][] = $file;            
            }
            else { // make an error and add to the reject pile.
                $result =  array('isError'      => false,
                                 'isParent'     => false, 
                                 'treeNode'     => null,
                                 'node'         => $file,
                                 'message'      => 'invalid folder path'); 
                $this->errors[] = $result;                
            }
        }    
    }     

    /**
    * Get as close to the folder as you can
    * 
    * Return the node as a reference as we want to update it in some way
    * 
    * 1) folder:
    * 
    *    a) You get to the parent where the new folder will be
    *       i.e. treeNode depth is one less than the new node depth
    * 
    *    b) the node already exists so nothing to do
    *       i.e. treeNode depth = new node depth
    * 
    *    c) there is a node missing from the path - wrong name etc.    
    *       i.e. treeNode depth is 2 or more less than the new node depth 
    * 
    *     * 
    * @param array $node
    * @param array $treeNode
    * @param integer $level
    * 
    * @return treeNode  
    */
    public function findParent(&$node, &$treeNode, $level = 0)
    {
        $nodePathLength      = count($node['path_array']);  // keep track of these for now to ease debugging

        $treeNodeParentLevel = $nodePathLength - 1;         // the tree must match to here the tree node to be a parent
                                                            // i.e. less or more than this and the node is an error 

        $treeNodePathLength = count($treeNode['path_array']); // this must be one less for it to be a true parent



        // All the useful information you need about the parent and it's possible child
        $result =  array('isError'      => false,
                         'isParent'     => false, 
                         'treeNode'     => &$treeNode,
                         'node'         => &$node,
                         'message'      => ''); 

        // these are always correct by definition 
        if ($nodePathLength <= 1) { // the root is always the parent of the first level
            $result['isParent']    = true;
            $result['message']     = 'root or first child';
            return $result;
        }


        if ($level > $treeNodeParentLevel) { // this path already exists in the tree
            $result['isError']     = true;
            $result['isParent']    = false;
            $result['message']     = 'duplicate';
            return $result;       
        }

        // we are looking for this in the current treeNode Children
        $nodeDir      = $node['path_array'][$level];


        foreach ($treeNode['children'] as $idx => &$tnode) {

            $tnodeDir  = $tnode['path_array'][$level];

            if ($nodeDir === $tnodeDir) { // match at this level - check the next one

                return $this->findParent($node, $tnode, $level + 1);
            }
        }
        unset($tnode); // do this when using references in foreach loops


        // can only get to here if the current nodeDir was not found in the children
        if ($level == $treeNodeParentLevel) {

            $result['isParent']    = true;
            $result['message']     = 'matched path';
        }
        else { // someone has gotten really confused...

            $result['isError']  = true;
            $result['message'] = 'i am confused';
        }

        return $result;            
    }



    /**
    * Sort folders to the top in increasing path length
    *      then files in increasing path length 
    * 
    * @return void
    */
    public function sort(&$directories)
    {
        usort($directories,  function ($node1, $node2) {
                                if ($node1['type'] === $node2['type']) { // same type - check path length
                                    $n1pl = count($node1['path_array']);
                                    $n2pl = count($node2['path_array']);

                                    if ($n1pl > $n2pl) { // higher counts sort after
                                        return +1;
                                    }
                                    if ($n1pl < $n2pl) { // lower counts sort earlier
                                        return -1;
                                    }
                                    return $node1['path'] < $node2['path']   ? -1 : +1; // lower name - sort earlier
                                }

                                return $node1['type'] === 'folder' ? -1 : +1; // folders sort before files
                            });                             
    }


    public function getErrors()
    {
        return $this->errors;
    }    


    public function getTree()
    {
        return $this->tree['children'];
    }    


    public function getJsonTree()
    {
        return json_encode($this->getTree());
    }    

    // get any of the properties directly if you wish
    public function __get($name)
    {
        return $this->$name;    
    }

    // Cheap and cheerful disply of the errors
    public function printResult($result)
    {
        $node = $result['node'];
        echo PHP_EOL, '---------', PHP_EOL;
        echo 'type       =>', $node['type'], PHP_EOL;
        echo 'name       =>', $node['name'], PHP_EOL; 
        echo 'path       =>', $node['path'], PHP_EOL;
        echo 'message   =>', $result['message'], PHP_EOL;

        if (!empty($node['path_array'])) {
            echo 'path_array =>', implode(', ', $node['path_array']), PHP_EOL;
        }

        if (isset($node['children']) && count($node['children'])> 0) {
            echo 'children count => ', count($node['children']), PHP_EOL;
        };            
    }
}

运行代码:

$bt = new BuildTree($directoryObjects);

$allOk = $bt->buildTree(); 

$json = $bt->getJsonTree();

要查看树和错误:

print_r($bt->tree);


foreach ($bt->errors as $result) {
    $bt->printResult($result);
}

示例错误

Errors
---------
type       =>folder
name       =>rubbish
path       =>/rubbish/cat
message   =>i am confused
path_array =>rubbish, cat

---------
type       =>folder
name       =>images - some
path       =>/animals/cat/images
message   =>duplicate
path_array =>animals, cat, images

---------
type       =>file
name       =>rubbishfile.png
path       =>/crockery/foo/bar
message   =>invalid folder path
path_array =>crockery, foo, bar

---------
type       =>file
name       =>rubbishAgain.png
path       =>/foo/bar/baz
message   =>invalid folder path
path_array =>foo, bar, baz

示例树:

Array
(
    [type] => folder
    [name] => root
    [path] => /
    [path_array] => Array
        (
        )
    [children] => Array
        (
            [0] => Array
                (
                    [type] => folder
                    [name] => animals
                    [path] => /animals
                    [path_array] => Array
                        (
                            [0] => animals
                        )
                    [children] => Array
                        (
                            [0] => Array
                                (
                                    [type] => folder
                                    [name] => cat
                                    [path] => /animals/cat
                                    [path_array] => Array
                                        (
                                            [0] => animals
                                            [1] => cat
                                        )
                                    [children] => Array
                                        (
                                            [0] => Array
                                                (
                                                    [type] => folder
                                                    [name] => images - pretty
                                                    [path] => /animals/cat/images
                                                    [path_array] => Array
                                                        (
                                                            [0] => animals
                                                            [1] => cat
                                                            [2] => images
                                                        )
                                                    [children] => Array
                                                        (
                                                            [0] => Array
                                                                (
                                                                    [type] => file
                                                                    [name] => AtlasX.png
                                                                    [path] => /animals/cat/images
                                                                    [path_array] => Array
                                                                        (
                                                                            [0] => animals
                                                                            [1] => cat
                                                                            [2] => images
                                                                        )
                                                                )
                                                            [1] => Array
                                                                (
                                                                    [type] => file
                                                                    [name] => AtlasX.png
                                                                    [path] => /animals/cat/images
                                                                    [path_array] => Array
                                                                        (
                                                                            [0] => animals
                                                                            [1] => cat
                                                                            [2] => images
                                                                        )
                                                                )
                                                        )
                                                )
                                        )
                                )
                        )
                )
            [1] => Array
                (
                    [type] => folder
                    [name] => crockery
                    [path] => /crockery
                    [path_array] => Array
                        (
                            [0] => crockery
                        )
                    [children] => Array
                        (
                            [0] => Array
                                (
                                    [type] => folder
                                    [name] => cups
                                    [path] => /crockery/cups
                                    [path_array] => Array
                                        (
                                            [0] => crockery
                                            [1] => cups
                                        )
                                    [children] => Array
                                        (
                                            [0] => Array
                                                (
                                                    [type] => file
                                                    [name] => cup.png
                                                    [path] => /crockery/cups
                                                    [path_array] => Array
                                                        (
                                                            [0] => crockery
                                                            [1] => cups
                                                        )
                                                )
                                        )
                                )
                            [1] => Array
                                (
                                    [type] => file
                                    [name] => crockeryThumb.png
                                    [path] => /crockery
                                    [path_array] => Array
                                        (
                                            [0] => crockery
                                        )
                                )
                        )
                )
            [2] => Array
                (
                    [type] => file
                    [name] => unicorn.jpg
                    [path] => /
                    [path_array] => Array
                        (
                        )
                )
        )
) 

测试数据 - 包括错误

$directoryObjects = [
    [
        'type' => 'file',
        'name' => 'unicorn.jpg',
        'path' => '/',
        'path_array' => []
    ],
    [
        'type' => 'folder',
        'name' => 'animals',
        'path' => '/animals',
        'path_array' => ['animals']
    ],

    [
        'type' => 'folder',
        'name' => 'cat',
        'path' => '/animals/cat',
        'path_array' => ['animals', 'cat']
    ],

    [
        'type' => 'folder',
        'name' => 'images - pretty',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ],

    [
        'type' => 'folder',
        'name' => 'images - some',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ],

    [
        'type' => 'file',
        'name' => 'AtlasX.png',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ],

    [
        'type' => 'file',
        'name' => 'AtlasX.png',
        'path' => '/animals/cat/images',
        'path_array' => ['animals', 'cat', 'images']
    ],
    [
        'type' => 'folder',
        'name' => 'crockery',
        'path' => '/crockery',
        'path_array' => ['crockery']
    ],
    [
        'type' => 'folder',
        'name' => 'cups',
        'path' => '/crockery/cups',
        'path_array' => ['crockery', 'cups']
    ],
    [
        'type' => 'file',
        'name' => 'cup.png',
        'path' => '/crockery/cups',
        'path_array' => ['crockery', 'cups']
    ],
    [
        'type' => 'file',
        'name' => 'crockeryThumb.png',
        'path' => '/crockery',
        'path_array' => ['crockery']
    ],
    [
        'type' => 'folder',
        'name' => 'rubbish',
        'path' => '/rubbish/cat',
        'path_array' => ['rubbish', 'cat']
    ],
    [
        'type' => 'folder',  // will be ignored as I generate one
        'name' => 'root',
        'path' => '/',
        'path_array' => []
    ],
    [
        'type' => 'file',
        'name' => 'rubbishfile.png',
        'path' => '/crockery/foo/bar',
        'path_array' => ['crockery', 'foo', 'bar']
    ],
    [
        'type' => 'file',
        'name' => 'rubbishAgain.png',
        'path' => '/foo/bar/baz',
        'path_array' => ['foo', 'bar', 'baz']
    ],
];