Below is something I believe will help you out. I've create a directory and file listing interface using PHP's FilesystemIterator, Bootstrap. Written for PHP 8.
Here is the below code in use:

This example uses two files, Reader.php
and index.php
...
Reader.php
<?php
class Reader {
public function __construct(
public string $root
) {}
/**
* Remove the root directory from the path value.
*
* @param string $path
* @return string
*/
public function removeRootFromPath(string $path) {
$path = preg_replace('/^' . preg_quote($this->root, '/') . '/', '', $path);
$path = $this->cleanPath($path);
return DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
}
/**
* Add the root directory to the path value.
*
* @param string $path
* @return string
*/
public function addRootToPath(string $path) {
$path = $this->removeRootFromPath($path);
$path = ltrim($path, DIRECTORY_SEPARATOR);
$root = rtrim($this->root, DIRECTORY_SEPARATOR);
return $root . DIRECTORY_SEPARATOR . $path;
}
/**
* Replace dot notation in paths i.e ../ and ./
*
* @param string $dir
* @return string
*/
public function cleanPath(string $dir) {
$sep = preg_quote(DIRECTORY_SEPARATOR, '/');
return preg_replace('/\.\.' . $sep . '|\.' . $sep . '/', '', $dir);
}
/**
* @param string $dir
* @return FilesystemIterator|null
*/
public function readDirectory(string $dir) {
$dir = $this->addRootToPath($dir);
try {
return new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS);
} catch (UnexpectedValueException $exception) {
return null;
}
}
}
index.php
<?php
require_once 'Reader.php';
$root_dir = 'src'; // Relative to current file. Change to your path!
$reader = new Reader(__DIR__ . DIRECTORY_SEPARATOR . $root_dir);
$target = $reader->removeRootFromPath(!empty($_GET['path']) ? $_GET['path'] : '/');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Directory viewer</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container my-5">
<h1>Current: <?= $target; ?></h1>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th class="w-25">
Type
</th>
<th class="w-75">
File
</th>
</tr>
</thead>
<tbody>
<?php if ($target !== $reader->removeRootFromPath('/')): ?>
<tr>
<td>
Parent
</td>
<td>
<a href="?path=<?= urlencode($reader->removeRootFromPath(dirname($target))); ?>">../</a>
</td>
</tr>
<?php endif ?>
<?php if ($results = $reader->readDirectory($target)): ?>
<?php foreach($results as $result): ?>
<?php
// Make the full path user friendly by removing the root directory.
$user_friendly = $reader->removeRootFromPath($result->getFileInfo());
$type = $result->getType();
?>
<tr>
<td>
<?= ucfirst($type); ?>
</td>
<td>
<?php if ($type === 'dir'): ?>
<a href="?path=<?= urlencode($user_friendly); ?>"><?= $user_friendly; ?></a>
<?php else: ?>
<!-- Maybe add a download link to the file -->
<?= $user_friendly; ?>
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
<?php else: ?>
<tr>
<td colspan="2">
Directory does not exist.
</td>
</tr>
<?php endif ?>
</tbody>
</table>
</div>
</body>
</html>
The Reader class includes a number of helper methods removeRootFromPath()
, addRootToPath()
and cleanPath()
the combination of these are there to help prevent users from accessing paths outside of your source directory.
For example, if your file system looked something like....
/var/www/Reader.php
/var/www/index.php
/var/www/src/
And you were listing the /var/www/src/
directory, with out the path cleaning methods the the user could modify the input to ../../
and end up at your filesystems root which is something you don't want!
This is something I've put together quickly for the sake of this question and should be used as a guide.