15

I'd like my script to scandir recursively,

$files = scandir('/dir');
foreach($files as $file){
if(is_dir($file)){
    echo '<li><label class="tree-toggler nav-header"><i class="fa fa-folder-o"></i>'.$file.'</label>';
    $subfiles = scandir($rooturl.'/'.$file);
        foreach($subfiles as $subfile){
            // and so on and on and on
        }
        echo '<li>';
    } else {
        echo $file.'<br />';
    }
}

I'd like to loop this in a way that for each dir found by scandir, it runs another scandir on the folders that have been found within that dir,

So dir 'A' contains dir 1/2/3, it should now scandir(1), scandir(2), scandir(3) and so on for each dir found.

How can I manage to implement this easily without copy pasting the code over and over in every foreach?

EDIT: Since the answers are nearly the very same as I already tried, I'll update the question a bit.

With this script I need to create a treeview list. With the current posted scripts the following get's echo'd happens:

/images/dir1/file1.png
/images/dir1/file2.png
/images/dir1/file3.png
/images/anotherfile.php
/data/uploads/avatar.jpg
/data/config.php
index.php

What I actually need is:

<li><label>images</label>
    <ul>
        <li><label>dir1</label>
            <ul>
                <li>file1.png</li>
                <li>file2.png</li>
                <li>file3.png</li>
            </ul>
        </li>
        <li>anotherfile.php</li>
    </ul>
</li>
<li><label>data</label>
    <ul>
        <li><label>uploads</label>
            <ul>
                <li>avatar.jpg</li>
            </ul>
        </li>
        <li>config.php</li>
    </ul>
</li>
<li>index.php</li>

And so on, Thank you for the already posted answers!

GRX
  • 479
  • 1
  • 5
  • 22

7 Answers7

26

I know this is an old question but I wrote a version of this that was more functional. It doesn't use global state and uses pure functions to figure things out:

function scanAllDir($dir) {
  $result = [];
  foreach(scandir($dir) as $filename) {
    if ($filename[0] === '.') continue;
    $filePath = $dir . '/' . $filename;
    if (is_dir($filePath)) {
      foreach (scanAllDir($filePath) as $childFilename) {
        $result[] = $filename . '/' . $childFilename;
      }
    } else {
      $result[] = $filename;
    }
  }
  return $result;
}
Allain Lalonde
  • 91,574
  • 70
  • 187
  • 238
  • 2
    You may want to use the constant DIRECTORY_SEPARATOR instead of hardcoding '/' to be safe to use the code on Linux as well as Windows systems. – Select0r Oct 04 '18 at 09:36
  • For some reason it picks only one subdir and scan it recursively – JackTheKnife Oct 04 '19 at 15:39
  • Nicely done: works out of the box, as advertised. – Eric P May 14 '22 at 06:35
  • Is it not better to the results of the current scan as $thisDirectory = scandir($dir); ? To avoid having the function scandir($dir) executed in every iteration? Or will it not be executed with each iteration? – algo Nov 11 '22 at 10:51
  • scandir will only get called once per directory. Putting the result in a variable won't reduce the number of times it gets called. – Allain Lalonde Nov 11 '22 at 20:23
6

You can scan directory recursively in this way the target is your top most directory:

function scanDir($target) {

        if(is_dir($target)){

            $files = glob( $target . '*', GLOB_MARK ); //GLOB_MARK adds a slash to directories returned

            foreach( $files as $file )
            {
                scanDir( $file );
            }


        } 
    }

You can adapt this function for your need easily. For example if would use this to delete the directory and its content you could do:

function delete_files($target) {

        if(is_dir($target)){

            $files = glob( $target . '*', GLOB_MARK ); //GLOB_MARK adds a slash to directories returned

            foreach( $files as $file )
            {
                delete_files( $file );
            }

            rmdir( $target );

        } elseif(is_file($target)) {

            unlink( $target );
    }

You can not do this in the way you are doing. The following function gets recursively all the directories, sub directories so deep as you want and the content of them:

function assetsMap($source_dir, $directory_depth = 0, $hidden = FALSE)
    {
        if ($fp = @opendir($source_dir))
        {
            $filedata   = array();
            $new_depth  = $directory_depth - 1;
            $source_dir = rtrim($source_dir, '/').'/';

            while (FALSE !== ($file = readdir($fp)))
            {
                // Remove '.', '..', and hidden files [optional]
                if ( ! trim($file, '.') OR ($hidden == FALSE && $file[0] == '.'))
                {
                    continue;
                }

                if (($directory_depth < 1 OR $new_depth > 0) && @is_dir($source_dir.$file))
                {
                    $filedata[$file] = assetsMap($source_dir.$file.'/', $new_depth, $hidden);
                }
                else
                {
                    $filedata[] = $file;
                }
            }

            closedir($fp);
            return $filedata;
        }
        echo 'can not open dir';
        return FALSE;
    }

Pass your path to the function:

$path = 'elements/images/';
$filedata = assetsMap($path, $directory_depth = 5, $hidden = FALSE);

$filedata is than an array with all founded directories and sub directories with their content. This function lets you scan the directories structure ($directory_depth) so deep as you want as well get rid of all the boring hidden files (e.g. '.','..')

All you have to do now is to use the returned array, which is the complete tree structure, to arrange the data in your view as you like.

What you are trying to do is in fact a kind of file manager and as you know there are a lot of those in the wild, open source and free.

I hope this will help you and I wish you a merry Christmas.

Franco
  • 2,309
  • 1
  • 11
  • 18
  • Thank you for the response, However I can't get this working to the way how I need it, I've updated my initial question for a better explanation :) – GRX Dec 10 '15 at 09:31
  • I will definitely try this out, I didn't check out open source scripts for this yet since mostly you have to strip like 70% of their builds to fit to your own scripts/templates/pages. Thank you for the replies and a Merry Christmas to yourself aswell! :) – GRX Dec 10 '15 at 12:59
  • Your scan dir function doesn't return any value, and you can't call the function with this name it is a reserved name – Froggiz Apr 25 '20 at 12:03
6

A variant of the previous function, using the ternary operator to make the function shorter and the splat (or spread) operator to unpack the array for array_push.

function getFiles(string $directory): array
{
    $files = array_diff(scandir($directory), ['.', '..']);
    $allFiles = [];

    foreach ($files as $file) {
        $fullPath = $directory. DIRECTORY_SEPARATOR .$file;
        is_dir($fullPath) ? array_push($allFiles, ...getFiles($fullPath)) : array_push($allFiles, $file);
    }

    return $allFiles;
}

OLD

function getFiles(string $directory, array $allFiles = []): array
{
    $files = array_diff(scandir($directory), ['.', '..']);

    foreach ($files as $file) {
        $fullPath = $directory. DIRECTORY_SEPARATOR .$file;

        if( is_dir($fullPath) )
            $allFiles += getFiles($fullPath, $allFiles);
        else
            $allFiles[] = $file;
    }

    return $allFiles;
}

I know this is old but i wanted to show a slightly different version of the other answers. Uses array_diff to discard the "." and ".." folders. Also the + operator to combine 2 arrays ( i rarely see this used so it might be useful for someone )

matteoruda
  • 131
  • 1
  • 4
  • 1
    Using `array_diff` is really nice as it makes it more succinct compared to `array_filter` – Alexander Kucheryuk Sep 13 '21 at 09:08
  • I did not even know it was possible to combine arrays using += The closest thing I knew was array_merge() but it was useless because it replaces the values with the same keys. Does this solution keep all the values? – algo Nov 11 '22 at 10:53
2

Though the question is old. But my answer could help people who visit this question.

This recursively scans the directory and child directories and stores the output in a global variable.

global $file_info; // All the file paths will be pushed here
$file_info = array();

/**
 * 
 * @function recursive_scan
 * @description Recursively scans a folder and its child folders
 * @param $path :: Path of the folder/file
 * 
 * */
function recursive_scan($path){
    global $file_info;
    $path = rtrim($path, '/');
    if(!is_dir($path)) $file_info[] = $path;
        else {
            $files = scandir($path);
            foreach($files as $file) if($file != '.' && $file != '..') recursive_scan($path . '/' . $file);
        }
}

recursive_scan('/var/www/html/wp-4.7.2/wp-content/plugins/site-backup');
print_r($file_info);
Aftabul Islam
  • 454
  • 4
  • 10
2

Modern way:

use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;

$path = '/var/www/path/to/your/files/';

foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path, RecursiveDirectoryIterator::SKIP_DOTS ) ) as $file ) {
    
    // TODO: Uncomment to see additional information. Do your manipulations with the files here.
    // var_dump( $file->getPathname() ); // Or simple way: echo $file;
    // var_dump( $file->isFile() );
    // var_dump( $file->isDir() );
    // var_dump( $file->getFileInfo() );
    // var_dump( $file->getExtension() );
    // var_dump( $file->getDepth() );

}
1

Create a scan function and call it recursively...

e.g:

   <?php

    function scandir_rec($root)
    {
        echo $root . PHP_EOL;
        // When it's a file or not a valid dir name
        // Print it out and stop recusion 
        if (is_file($root) || !is_dir($root)) {
            return;
        }

        // starts the scan
        $dirs = scandir($root);
        foreach ($dirs as $dir) {
            if ($dir == '.' || $dir == '..') {
                continue; // skip . and ..
            }

            $path = $root . '/' . $dir;
            scandir_rec($path); // <--- CALL THE FUNCTION ITSELF TO DO THE SAME THING WITH SUB DIRS OR FILES.
        }
    }

    // run it when needed
    scandir_rec('./rootDir');

You can do a lot variation of this function. Printing a 'li' tag instead of PHP_EOL for instance, to create a tree view.

[EDIT]

 <?php

function scandir_rec($root)
{
    // if root is a file
    if (is_file($root)) {
        echo '<li>' . basename($root) . '</li>';
        return;
    }

    if (!is_dir($root)) {
        return;
    }

    $dirs = scandir($root);
    foreach ($dirs as $dir) {
        if ($dir == '.' || $dir == '..') {
            continue;
        }

        $path = $root . '/' . $dir;
        if (is_file($path)) {
            // if file, create list item tag, and done.
            echo '<li>' . $dir . '</li>';
        } else if (is_dir($path)) {
            // if dir, create list item with sub ul tag
            echo '<li>';
            echo '<label>' . $dir . '</label>';
            echo '<ul>';
            scandir_rec($path); // <--- then recursion
            echo '</ul>';
            echo '</li>';
        }
    }
}

// init call
$rootDir = 'rootDir';
echo '<ul>';
scandir_rec($rootDir);
echo '</ul>';
VVLeon
  • 399
  • 3
  • 8
  • 1
    Thank you for the response, However I can't get this working to the way how I need it, I've updated my initial question for a better explanation :) – GRX Dec 10 '15 at 09:31
0
function deepScan($dir = __DIR__){
  static $allFiles = [];
  $allFiles[$dir] = [];

   $directories = array_values(array_diff(scandir($dir), ['.', '..']));
   foreach($directories as $directory){
     if(is_dir("$dir\\$directory")){
       foreach(deepScan("$dir\\$directory") as $key => $value) $allFiles[$key] = $value;
     }
     else{
      $allFiles[$dir][] = "$directory";
     }
   }
   return $allFiles;
}