Advertisement
rfv123

Q38278304 - Create a tree - validate nodes

Jul 13th, 2016
317
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 14.37 KB | None | 0 0
  1. <?php // http://stackoverflow.com/questions/38278304/parse-an-flat-array-to-a-one-that-represents-a-directory-structure
  2.  
  3. // include __DIR__. '/__bootstrap__.php';
  4.  
  5. /*
  6.  *  I have added duplicate node to the tree and a completely invalid path
  7.  *  
  8.  *
  9.  */
  10. $directoryObjects = [
  11.     [
  12.         'type' => 'file',
  13.         'name' => 'unicorn.jpg',
  14.         'path' => '/',
  15.         'path_array' => []
  16.     ],
  17.     [
  18.         'type' => 'folder',
  19.         'name' => 'animals',
  20.         'path' => '/animals',
  21.         'path_array' => ['animals']
  22.     ],
  23.  
  24.     [
  25.         'type' => 'folder',
  26.         'name' => 'cat',
  27.         'path' => '/animals/cat',
  28.         'path_array' => ['animals', 'cat']
  29.     ],
  30.  
  31.     [
  32.         'type' => 'folder',
  33.         'name' => 'images - pretty',
  34.         'path' => '/animals/cat/images',
  35.         'path_array' => ['animals', 'cat', 'images']
  36.     ],
  37.  
  38.     [
  39.         'type' => 'folder',
  40.         'name' => 'images - some',
  41.         'path' => '/animals/cat/images',
  42.         'path_array' => ['animals', 'cat', 'images']
  43.     ],
  44.  
  45.     [
  46.         'type' => 'file',
  47.         'name' => 'AtlasX.png',
  48.         'path' => '/animals/cat/images',
  49.         'path_array' => ['animals', 'cat', 'images']
  50.     ],
  51.  
  52.     [
  53.         'type' => 'file',
  54.         'name' => 'AtlasX.png',
  55.         'path' => '/animals/cat/images',
  56.         'path_array' => ['animals', 'cat', 'images']
  57.     ],
  58.     [
  59.         'type' => 'folder',
  60.         'name' => 'crockery',
  61.         'path' => '/crockery',
  62.         'path_array' => ['crockery']
  63.     ],
  64.     [
  65.         'type' => 'folder',
  66.         'name' => 'cups',
  67.         'path' => '/crockery/cups',
  68.         'path_array' => ['crockery', 'cups']
  69.     ],
  70.     [
  71.         'type' => 'file',
  72.         'name' => 'cup.png',
  73.         'path' => '/crockery/cups',
  74.         'path_array' => ['crockery', 'cups']
  75.     ],
  76.     [
  77.         'type' => 'file',
  78.         'name' => 'crockeryThumb.png',
  79.         'path' => '/crockery',
  80.         'path_array' => ['crockery']
  81.     ],
  82.     [
  83.         'type' => 'folder',
  84.         'name' => 'rubbish',
  85.         'path' => '/rubbish/cat',
  86.         'path_array' => ['rubbish', 'cat']
  87.     ],
  88.     [
  89.         'type' => 'folder',  // will be ignored as I generate one
  90.         'name' => 'root',
  91.         'path' => '/',
  92.         'path_array' => []
  93.     ],
  94.     [
  95.         'type' => 'file',
  96.         'name' => 'rubbishfile.png',
  97.         'path' => '/crockery/foo/bar',
  98.         'path_array' => ['crockery', 'foo', 'bar']
  99.     ],
  100.     [
  101.         'type' => 'file',
  102.         'name' => 'rubbishAgain.png',
  103.         'path' => '/foo/bar/baz',
  104.         'path_array' => ['foo', 'bar', 'baz']
  105.     ],
  106. ];
  107.  
  108.  
  109.  
  110. echo '<pre> ', 'Before...', PHP_EOL;
  111. echo 'Directories', PHP_EOL;
  112.  
  113.  
  114. /* -----------------------------------------------------------------------------------------------------
  115.  *  Run the code...
  116.  */
  117. $bt = new BuildTree($directoryObjects);
  118.  
  119. echo '<pre>';
  120. echo 'Sorted Directories', PHP_EOL;
  121. print_r($bt->directories);
  122. echo '</pre>';
  123.  
  124.  
  125. $allOk = $bt->buildTree();
  126.  
  127. echo '<pre>';
  128. echo 'Json', PHP_EOL;
  129. print_r($bt->getJsonTree());
  130. echo '</pre>';
  131.  
  132.  
  133. echo '<pre>';
  134. echo 'Errors', PHP_EOL;
  135. foreach ($bt->errors as $result) {
  136.     $bt->printResult($result);
  137. }
  138. echo '</pre>';
  139.  
  140. echo 'Tree', PHP_EOL;
  141. echo '<pre>';
  142. print_r($bt->tree);
  143. echo '</pre>';
  144.  
  145.  
  146.  
  147. exit;
  148.  
  149. /* -------------------------------------------------------------------------------------------------------
  150.  * Build the tree folders first
  151.  *
  152.  * a) I am concerned about efficiency on trees with large numbers of files.
  153.  *    To deal with this i keep a list of folder nodes so the file insert will be efficient.
  154.  *
  155.  * b) I am concerned about folder errors such as duplicates or invalid paths so I have attempted to deal with these.
  156.  *
  157.  * c) The root node is optional in the source data. It just creates one.
  158.  *
  159.  * To simplify the tree building logic I will sort the array into some useful order for me.
  160.  *
  161.  * Specifically:
  162.  *
  163.  *   1) Folders
  164.  *      a) path length order
  165.  *
  166.  *   2) Files
  167.  *      b) Path length order
  168.  *  
  169.  *   This will make the tree building easier but the cost is a sort plus the cost of the building the tree
  170.  *
  171.  *      
  172.  */
  173.  
  174. class BuildTree
  175. {
  176.     /**
  177.      *  The source data
  178.      *
  179.      *  @var array
  180.      */
  181.     private $directories = array();
  182.  
  183.     /**
  184.     * References to the folders in the directory list.
  185.     * Makes adding files a lot more efficient
  186.     *
  187.     * @var array
  188.     */
  189.     private $nodeRefs = array();    
  190.  
  191.     /**
  192.     * Invalid nodes - there is am error message as to why it failed
  193.     *
  194.     * @var array  
  195.     */
  196.     private $errors = array();    
  197.        
  198.     /**
  199.     * The tree - all the nodes are references rather than copies of the data
  200.     *
  201.     * @var array
  202.     */    
  203.     private $tree = null;
  204.    
  205.     /**
  206.     * The root node to make the tree complete
  207.     *
  208.     * @var array
  209.     */
  210.     private $rootNode = array(
  211.                         'type' => 'folder',
  212.                         'name' => 'root',
  213.                         'path' => '/',
  214.                         'path_array' => array(),
  215.                         'children' => array(),
  216.                         );
  217.    
  218.     /**
  219.     * The start index of the first file in the sorted input
  220.     *
  221.     * @var integer
  222.     */
  223.     private $firstFile = -1;
  224.    
  225.  
  226.     /**
  227.     * Sort the directory input in folders by length and then files
  228.     *
  229.     * @param array $directories
  230.     *
  231.     * @return void
  232.     */
  233.     public function __construct($directories)
  234.     {
  235.        
  236.         $this->sort($directories);
  237.         $this->directories = $directories;
  238.         $this->tree = &$this->rootNode; // make the tree a 'complete tree' - simplifies the addNode logic    
  239.     }
  240.    
  241.     /**
  242.     * Just executes the:
  243.     *   1) the process folders (build the tree of directory nodes
  244.     *   2) adds the files to the correct nodes
  245.     *
  246.     * @return boolean eorros or not
  247.     */
  248.     public function buildTree()
  249.     {
  250.         $this->buildFolderTree();
  251.         $this->addFiles();            
  252.         return count($this->errors) <= 0;
  253.     }
  254.  
  255.    
  256.    /**
  257.     * All the folders are at the top of the directories list
  258.     *
  259.     * Read through the list and add all the folders into the tree
  260.     *
  261.     * Foreach folder:
  262.     *   1) Find the parent
  263.     *   2) if valid add to the tree
  264.     *   3) record the reference to it so we can add files easily later
  265.     *  
  266.     * @return void
  267.     */
  268.     public function buildFolderTree()
  269.     {
  270.         // add rootnode to the list
  271.         $this->nodeRefs[$this->tree['path']] =& $this->tree;
  272.        
  273.         foreach ($this->directories as $idx => &$node) {
  274.            
  275.             if ($node['type'] !== 'folder') { // record the index of the first file
  276.                 $this->firstFile = $idx; // needed for processing the files efficiently
  277.                 break;
  278.             }
  279.    
  280.             if (empty($node['path_array'])) { // you passed a root anyway - ignore it ;-/
  281.                 continue;
  282.             }        
  283.  
  284.             $node['children'] = array();  // needed for adding nodes to the tree        
  285.             $result = $this->findParent($node, $this->tree);
  286.  
  287.             if ($result['isError'] || !$result['isParent']) { // ignore as invalid...
  288.                 $this->errors[] = $result;
  289.                 continue;
  290.             }
  291.  
  292.             // add to the tree as a reference...
  293.                              
  294.             $parent =& $result['treeNode'];
  295.             $parent['children'][] =& $this->directories[$idx]; // reference to the original node
  296.                                  
  297.             // fast lookup later... when we add files
  298.             $this->nodeRefs[$node['path']] =& $node;
  299.         }
  300.         unset($node); // magic - 'cos we used a reference in a foreach loop.
  301.     }
  302.    
  303.    
  304.     /**
  305.     * This does not need to do a treewalk to find the parent.
  306.     *
  307.     * All the parents are in the $nodeRefs array with a key of the `path`
  308.     *
  309.     * This makes inserting the files quick
  310.     *
  311.     * @return void
  312.     */    
  313.     public function addFiles()
  314.     {
  315.         foreach (array_slice($this->directories, $this->firstFile) as $idx => $file)  {
  316.            
  317.             if (isset($this->nodeRefs[$file['path']])) { // add to the children
  318.  
  319.                 $this->nodeRefs[$file['path']]['children'][] = $file;            
  320.             }
  321.             else { // make an error and add to the reject pile.
  322.                 $result =  array('isError'      => false,
  323.                                  'isParent'     => false,
  324.                                  'treeNode'     => null,
  325.                                  'node'         => $file,
  326.                                  'message'      => 'invalid folder path');
  327.                 $this->errors[] = $result;                
  328.             }
  329.         }    
  330.     }    
  331.        
  332.     /**
  333.     * Get as close to the folder as you can
  334.     *
  335.     * Return the node as a reference as we want to update it in some way
  336.     *
  337.     * 1) folder:
  338.     *
  339.     *    a) You get to the parent where the new folder will be
  340.     *       i.e. treeNode depth is one less than the new node depth
  341.     *
  342.     *    b) the node already exists so nothing to do
  343.     *       i.e. treeNode depth = new node depth
  344.     *
  345.     *    c) there is a node missing from the path - wrong name etc.    
  346.     *       i.e. treeNode depth is 2 or more less than the new node depth
  347.     *
  348.     *     *
  349.     * @param array $node
  350.     * @param array $treeNode
  351.     * @param integer $level
  352.     *
  353.     * @return treeNode  
  354.     */
  355.     public function findParent(&$node, &$treeNode, $level = 0)
  356.     {
  357.         $nodePathLength      = count($node['path_array']);  // keep track of these for now to ease debugging
  358.        
  359.         $treeNodeParentLevel = $nodePathLength - 1;         // the tree must match to here the tree node to be a parent
  360.                                                             // i.e. less or more than this and the node is an error
  361.        
  362.         $treeNodePathLength = count($treeNode['path_array']); // this must be one less for it to be a true parent
  363.  
  364.  
  365.        
  366.         // All the useful information you need about the parent and it's possible child
  367.         $result =  array('isError'      => false,
  368.                          'isParent'     => false,
  369.                          'treeNode'     => &$treeNode,
  370.                          'node'         => &$node,
  371.                          'message'      => '');
  372.  
  373.         // these are always correct by definition
  374.         if ($nodePathLength <= 1) { // the root is always the parent of the first level
  375.             $result['isParent']    = true;
  376.             $result['message']     = 'root or first child';
  377.             return $result;
  378.         }
  379.  
  380.  
  381.         if ($level > $treeNodeParentLevel) { // this path already exists in the tree
  382.             $result['isError']     = true;
  383.             $result['isParent']    = false;
  384.             $result['message']     = 'duplicate';
  385.             return $result;      
  386.         }
  387.  
  388.         // we are looking for this in the current treeNode Children
  389.         $nodeDir      = $node['path_array'][$level];
  390.  
  391.  
  392.         foreach ($treeNode['children'] as $idx => &$tnode) {
  393.  
  394.             $tnodeDir  = $tnode['path_array'][$level];
  395.            
  396.             if ($nodeDir === $tnodeDir) { // match at this level - check the next one
  397.  
  398.                 return $this->findParent($node, $tnode, $level + 1);
  399.             }
  400.         }
  401.         unset($tnode); // do this when using references in foreach loops
  402.        
  403.  
  404.         // can only get to here if the current nodeDir was not found in the children
  405.         if ($level == $treeNodeParentLevel) {
  406.                  
  407.             $result['isParent']    = true;
  408.             $result['message']     = 'matched path';
  409.         }
  410.         else { // someone has gotten really confused...
  411.        
  412.             $result['isError']  = true;
  413.             $result['message'] = 'i am confused';
  414.         }
  415.        
  416.         return $result;            
  417.     }
  418.    
  419.    
  420.  
  421.     /**
  422.     * Sort folders to the top in increasing path length
  423.     *      then files in increasing path length
  424.     *
  425.     * @return void
  426.     */
  427.     public function sort(&$directories)
  428.     {
  429.         usort($directories,  function ($node1, $node2) {
  430.                                 if ($node1['type'] === $node2['type']) { // same type - check path length
  431.                                     $n1pl = count($node1['path_array']);
  432.                                     $n2pl = count($node2['path_array']);
  433.                                    
  434.                                     if ($n1pl > $n2pl) { // higher counts sort after
  435.                                         return +1;
  436.                                     }
  437.                                     if ($n1pl < $n2pl) { // lower counts sort earlier
  438.                                         return -1;
  439.                                     }
  440.                                     return $node1['path'] < $node2['path']   ? -1 : +1; // lower name - sort earlier
  441.                                 }
  442.                                
  443.                                 return $node1['type'] === 'folder' ? -1 : +1; // folders sort before files
  444.                             });                            
  445.     }
  446.  
  447.    
  448.     public function getErrors()
  449.     {
  450.         return $this->errors;
  451.     }    
  452.  
  453.    
  454.     public function getTree()
  455.     {
  456.         return $this->tree['children'];
  457.     }    
  458.  
  459.    
  460.     public function getJsonTree()
  461.     {
  462.         return json_encode($this->getTree());
  463.     }    
  464.    
  465.     // get any of the properties directly if you wish
  466.     public function __get($name)
  467.     {
  468.         return $this->$name;    
  469.     }
  470.    
  471.     // Cheap and cheerful disply of the errors
  472.     public function printResult($result)
  473.     {
  474.         $node = $result['node'];
  475.         echo PHP_EOL, '---------', PHP_EOL;
  476.         echo 'type       =>', $node['type'], PHP_EOL;
  477.         echo 'name       =>', $node['name'], PHP_EOL;
  478.         echo 'path       =>', $node['path'], PHP_EOL;
  479.         echo 'message   =>', $result['message'], PHP_EOL;
  480.        
  481.         if (!empty($node['path_array'])) {
  482.             echo 'path_array =>', implode(', ', $node['path_array']), PHP_EOL;
  483.         }
  484.        
  485.         if (isset($node['children']) && count($node['children'])> 0) {
  486.             echo 'children count => ', count($node['children']), PHP_EOL;
  487.         };            
  488.     }
  489. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement