Логотип
vexaiv

📁 Файловый менеджер на PHP с шаблонизацией

🎯 Что мы создаем?


Минимальный файловый менеджер с шаблонами на чистом PHP без использования фреймворков, который позволяет:
  • просматривать папки и файлы через браузер
  • переходить по вложенным папкам
  • скачивать файлы
  • иметь красивые URL-адреса (например, /files/documents/reports)

🔄 Архитектура веб-приложения


Принцип работы: Браузер → index.php (единая точка входа) → Маршрутизация → Контроллер → Шаблон → HTML → Браузер
1. Все запросы приходят в index.php (единая точка входа)
2. index.php анализирует URL и передает управление контроллеру
3. Контроллер обрабатывает логику (читает файлы, проверяет права доступа)
4. Данные из контроллера передаются в шаблон для отображения
5. Готовый HTML отправляется пользователю

Такой подход имеет несколько неоспоримых преимуществ перед простой отдачей HTML страниц, самое главное из которых - полный контроль над тем, куда пользователь может зайти и что он получит.

Даже если у вас в корне сайта лежат какие-либо файлы, пользователь не сможет получить их набрав в браузере http://сайт.ru/admin.php или http://сайт.ru/моя_личная_фотка.jpg.

Любой URL, набранный пользователем, всегда приходит в index.php и там именно вы определяете, что делать с этим запросом.

📁 Структура папок


	
    ├── index.php              # Входная точка приложения
├── .htaccess              # Настройки для Apache (перенаправление всех запросов на index.php)
├── /config/               # Конфигурационные файлы
│   └── routes.php         # Список всех доступных маршрутов
├── /templates/            # Шаблоны страниц
│   ├── layout.php         # Каркас сайта (шапка, меню...)
│   ├── home.php           # Контент главной страницы
│   └── files.php          # Контент страницы с файлами
├── /controllers/          # Логика обработки запросов
│   └── FilesController.php # Вся логика работы с файлами
├── /css/                  # Стили оформления
│   ├── global.css         # Стили для всего сайта
│   └── files.css          # Стили только для страницы файлов
└── /storage/              # Папка с вашими файлами (создается автоматически контроллером)

🚦 Маршрутизация (ЧПУ)


Что такое ЧПУ?


ЧПУ (Человеко-Понятные УРЛы) - это URL-адреса, которые легко читаются людьми: вместо какого-нибудь index.php?page=files&folder=123&action=view у нас будет что-то типа /files/documents/plan.pdf.

Как это работает в нашем проекте?


Файл /config/routes.php


Это массив вида URL-запрос => что сним делать.
	
    <?php
return [
    // Пустая строка или 'home' ведут на главную
    '' => 'home',           // при запросе сайта без пути
    'home' => 'home',       // при запросе /home
    
    // Страницы файлов - используем контроллер и метод
    'files' => ['FilesController', 'index'],     // просто список файлов в корне
    'files!' => ['FilesController', 'index'],    // с путём к папке или файлу
    'download' => ['FilesController', 'download'], // скачивание файла
];
?>
  • Ключ массива - это URL-путь (то, что после домена)
  • Значение - что делать с этим запросом:
  • Если значение строка ('home') - просто показываем шаблон
  • Если значение массив - вызываем указанный метод контроллера

Файл index.php (обработка маршрутов)

	
    <?php
// Автозагрузка классов контроллеров из папки /controllers,
// чтобы не писать require_once для каждого контроллера
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/controllers/' . $class . '.php';
    if (file_exists($path)) {
        require_once $path;
    }
});

// Получаем путь URL из URI
// http://localhost/files/photo.jpg => /files/photo.jpg
$request = $_SERVER['REQUEST_URI'];

/* Убираем GET-параметры (всё после ?)
 * Например, этом адресе: http://localhost/files/photo.jpg?delete=yes
 * получим $path=/files/photo.jpg */
$path = parse_url($request, PHP_URL_PATH);

// убираем слеши в начале и конце
$path = trim($path, '/');

// Загружаем маршруты
$routes = require __DIR__ . '/config/routes.php';

// Ищем подходящий маршрут
$routeFound = false;
$params = [];

// Сначала ищем точное совпадение
if (isset($routes[$path])) {
    $routeFound = $routes[$path];
} else {
    // Перебираем маршруты
    foreach ($routes as $key => $value) {
        // только динамические маршруты (с восклицательным знаком),
        // которые могут принимать параметры (например, путь к файлу)
        if (substr($key, -1) === '!') {
            $baseKey = substr($key, 0, -1); // убираем '!' в конце
            // Проверяем, содержит ли путь этот базовый маршрут с первого символа
            if (strpos($path, $baseKey) === 0) {
                // Извлекаем всё, что после строки базового маршрута
                $param = substr($path, strlen($baseKey));
                /* Параметр должен начинаться со слеша либо строка маршрута и URI
                 * могут быть одинаковы (то есть параметра нет). Если оба условия
                 * не проходят, то это не параметр. Например, если $path=filesfolder,
                 * а у нас есть только маршрут files, то этот случай мы отсекаем,
                 * чтобы не принять ошибочно folder за параметр */
                if ($path === $baseKey or $param[0] === '/') {
                    $param = ltrim($param, '/'); // убираем слеш слева
                    $params['slug'] = $param; // параметр всегда называется slug
                    $routeFound = $value;
                    break;
                }
            }
        }
    }
}

// Обрабатываем найденный маршрут
if ($routeFound) {
    if (is_string($routeFound)) {
        // Просто показываем шаблон
        renderTemplate($routeFound, $params);
    } elseif (is_array($routeFound) && count($routeFound) === 2) {
        // Подбираем контроллер и метод
        $controllerName = $routeFound[0];
        $methodName = $routeFound[1];
        
        if (class_exists($controllerName)) {
            $controller = new $controllerName();
            // Передаем параметры из URL в $_GET для удобства
            foreach ($params as $key => $value) {
                $_GET[$key] = $value; // $params['slug'] отправится в $_GET['slug']
            }
            // Вызываем метод контроллера
            $controller->$methodName();
        } else {
            header("HTTP/1.0 500 Internal Server Error");
            echo "Ошибка: контроллер не найден";
        }
    }
} else {
    // Маршрут не найден - 404 ошибка
    header("HTTP/1.0 404 Not Found");
    renderTemplate('404');
}

// Функция для отрисовки шаблонов
function renderTemplate($template, $data = []) {
    // extract(...) превращает указанный массив в переменные
    // например, ['title' => 'Главная'] запишется в $title = 'Главная'
    extract($data);
    
    // Буферизация вывода
    // Включаем "запись" всего, что будет дальше, предотвращая вывод в браузер
    ob_start();
    
    // Проверяем существует ли файл для шаблона
    if (file_exists(__DIR__ . "/templates/{$template}.php")) {
        // Подключаем шаблон страницы
        include __DIR__ . "/templates/{$template}.php";
        // Благодаря ob_start() вывод шаблона идет в буфер, а не сразу в браузер
    }
    // Сохраняем что было "написано" в буфер в переменную $content и очищаем буфер
    $content = ob_get_clean();
    // Теперь $content содержит шаблон страницы (либо он пуст, если файл шаблона не нашелся)
    
    // Подключаем основной шаблон сайта
    // Cодержание переменной $content впишется в поле $content главного шаблона
    include __DIR__ . '/templates/layout.php';
}

🎮 Контроллеры


Что такое контроллер?


Контроллер - это "мозг" страницы. Он получает запрос, выполняет логику и передает данные в шаблон.

Файл /controllers/FilesController.php

	
    <?php
class FilesController {
    
    private $basePath; // путь к папке с файлами
    
    public function __construct() {
        // Определяем путь к папке storage
        $this->basePath = __DIR__ . '/../storage';
        
        // Создаем папку, если её нет
        if (!file_exists($this->basePath)) {
            mkdir($this->basePath, 0755, true);
        }
    }
    
    /**
     * Отображает содержимое папки
     */
    public function index() {
        // Получаем путь из URL (приходит из маршрутизатора index.php)
        $currentPath = isset($_GET['slug']) ? $_GET['slug'] : '';
        $currentPath = urldecode($currentPath); // декодируем %D0%BF%D0%B0%D0%BF%D0%BA%D0%B0 → папка
        
        // Защита от взлома: убираем попытки выйти за пределы storage
        $currentPath = str_replace('..', '', $currentPath);
        $currentPath = ltrim($currentPath, '/');
        
        // Полный путь к запрошенной папке
        $fullPath = $this->basePath . ($currentPath ? '/' . $currentPath : '');
        
        // Проверяем, что мы не вышли за пределы storage
        if (strpos(realpath($fullPath), realpath($this->basePath)) !== 0) {
            die("Доступ запрещен");
        }
        
        // Если это файл (не папка) - скачиваем его
        if (file_exists($fullPath) && !is_dir($fullPath)) {
            $_GET['path'] = $currentPath;
            $this->download();
            return;
        }
        
        // Получаем список файлов в папке
        $files = scandir($fullPath);
        
        // Путь к родительской папке (для кнопки "Наверх")
        $parentDir = dirname($currentPath);
        if ($parentDir == '.') $parentDir = '';
        
        // Разделяем папки и файлы
        $folders = [];
        $filesList = [];
        
        foreach ($files as $file) {
            /* Как и "ls -a" в Linux, scandir($fullPath) возвращает не только
             * реальные файлы и папки, но также "." и ".." (текущая папка и родительская)
             * Пропускаем их */
            if ($file == '.' || $file == '..') continue;
            
            $filePath = $fullPath . '/' . $file;
            if (is_dir($filePath)) {
                $folders[] = $file; // это папка
            } else {
                $filesList[] = $file; // это файл
            }
        }
        
        // Сортируем в алфавитном порядке
        sort($folders, SORT_STRING | SORT_FLAG_CASE);
        sort($filesList, SORT_STRING | SORT_FLAG_CASE);
        
        // Передаем данные в метод создания шаблона в index.php
        renderTemplate('files', [
            'title' => 'Файлы',
            'contentStyle' => '/css/files.css',
            'currentPath' => $currentPath,
            'folders' => $folders,
            'filesList' => $filesList,
            'parentDir' => $parentDir
        ]);
    }
    
    /**
     * Скачивание файла
     */
    public function download() {
        // Получаем путь к файлу
        $path = $_GET['path'] ?? '';
        $path = urldecode($path);
        $path = str_replace('..', '', $path);
        $path = ltrim($path, '/');
        
        $fullPath = $this->basePath . '/' . $path;
        $realBase = realpath($this->basePath);
        $realFull = realpath($fullPath);
        
        // Проверяем, что файл существует и доступен
        if (!$realFull || strpos($realFull, $realBase) !== 0 || !is_file($realFull)) {
            header("HTTP/1.0 404 Not Found");
            echo "Файл не найден";
            exit;
        }
        
        // Отправляем заголовки для скачивания
        header('Content-Description: File Transfer');
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . basename($path) . '"');
        header('Content-Length: ' . filesize($realFull));
        
        // Отправляем содержимое файла
        readfile($realFull);
        exit;
    }
}

🎨 Шаблоны


Почему шаблоны отдельно?


Шаблоны отвечают только за отображение данных. Это позволяет легко менять дизайн, не трогая логику приложения.

Файл /templates/layout.php (общий каркас сайта)

	
    <!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $title ?? 'Мой сайт' ?></title>
    <!-- Общие стили для всего сайта -->
    <link rel="stylesheet" href="/css/global.css">
    <!-- Специфичные стили для текущей страницы, если заданы переменной $contentStyle -->
    <?php if (isset($contentStyle)): ?>
        <link rel="stylesheet" href="<?= $contentStyle ?>">
    <?php endif; ?>
</head>
<body>
    <!-- Шапка сайта (одинаковая на всех страницах) -->
    <header class="header">
        <div>📁 Файловый менеджер</div>
    </header>
    
    <div class="container">
        <!-- Навигационное меню -->
        <nav class="menu">
            <a href="/">Главная</a>
            <a href="/files">Файлы</a>
        </nav>
        
        <!-- Контент текущей страницы (подставляется из $content в renderTemplate) -->
        <!-- Если $content не задан или пустой, выведет текст 'Страница не найдена' -->
        <main class="content">
            <?= !empty($content) ? $content : 'Страница не найдена' ?>
        </main>
    </div>
</body>
</html>

Файл /templates/home.php (главная страница)

	
    <div class="home">
    <h1>Добро пожаловать!</h1>
    <p>Это простой файловый менеджер, который показывает файлы из папки <strong>/storage</strong>.</p>
    <p>👉 <a href="/files">Перейти к файлам</a></p>
</div>

Файл /templates/files.php (страница файлов)

	
    <div class="files">
    <!-- Хлебные крошки (навигация по папкам) -->
    <div class="path">
        <a href="/files">📁 Корень</a>
        <?php if ($currentPath):
            $parts = explode('/', $currentPath);
            $cumulative = '';
            foreach ($parts as $part):
                $cumulative .= ($cumulative ? '/' : '') . $part;
        ?>
            / <a href="/files/<?= urlencode($cumulative) ?>"><?= htmlspecialchars($part) ?></a>
        <?php endforeach; ?>
        <?php endif; ?>
    </div>
    
    <!-- Список файлов и папок -->
    <ul class="file-list">
        <!-- Кнопка "Наверх" (если не в корне) -->
        <?php if ($currentPath): ?>
        <li class="up">
            <a href="/files/<?= urlencode($parentDir) ?>">⬆️ Наверх</a>
        </li>
        <?php endif; ?>
        
        <!-- Отображаем папки -->
        <?php foreach ($folders as $folder): 
            $folderParam = $currentPath ? $currentPath . '/' . $folder : $folder;
        ?>
        <li class="folder-item">
            <a href="/files/<?= urlencode($folderParam) ?>">
                📁 <?= htmlspecialchars($folder) ?>
            </a>
        </li>
        <?php endforeach; ?>
        
        <!-- Отображаем файлы -->
        <?php foreach ($filesList as $file): 
            $fileParam = $currentPath ? $currentPath . '/' . $file : $file;
        ?>
        <li class="file-item">
            <a href="/download?path=<?= urlencode($fileParam) ?>">
                📄 <?= htmlspecialchars($file) ?>
            </a>
        </li>
        <?php endforeach; ?>
        
        <!-- Сообщение, если папка пуста -->
        <?php if (empty($folders) && empty($filesList)): ?>
        <li class="empty">📂 Папка пуста</li>
        <?php endif; ?>
    </ul>
</div>

🛡️ Безопасность


Какие уязвимости мы предотвращаем?


1. Выход за пределы storage
	
    // Убираем попытки перейти в родительскую папку через ..
$currentPath = str_replace('..', '', $currentPath);

// Проверяем, что реальный путь начинается с пути к storage
if (strpos(realpath($fullPath), realpath($this->basePath)) !== 0) {
    die("Доступ запрещен");
}

2. XSS-атаки (внедрение скриптов)
	
    // Экранируем спецсимволы HTML
<?= htmlspecialchars($folder) ?>

3. Пути с спецсимволами
	
    // Кодируем URL-небезопасные символы
urlencode($folderParam)

📥 Пошаговая установка


Шаг 1: Создайте структуру папок

	
    mkdir -p my-file-manager/{config,controllers,templates,css,storage}
cd my-file-manager

Шаг 2: Создайте все файлы

Скопируйте код из примеров выше в соответствующие файлы.

Шаг 3: Настройте веб-сервер


Для Apache (файл .htaccess в корне)

	
    RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

Для Nginx (в конфигурации сайта)

	
    server {
    listen 80;
    server_name localhost; // адрес сайта
    root /path/to/my-file-manager; // путь к корневой папке сайта
    index index.php;

    // Указываем Nginx, что любые запросы нужно отправлять в index.php
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    // Включаем обработку и выполнение php-кода, иначе Nginx будет
    // просто отдавать браузеру наши php файлы как обычный текст
    // ВНИМАНИЕ: Проверьте правильность пути и версию php-fpm в вашей системе
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    }
}

Шаг 4: Проверьте права доступа

	
    # Папка storage должна быть доступна для записи
chmod 755 storage
# Остальные файлы можно оставить 644
find . -type f -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;
Также проверьте владельца файлов сайта. Nginx работает от пользователя www-data и этот пользователь должен иметь права на эти файлы
	
    chown -R www-data:www-data /путь/к/корню/сайта/

Шаг 5: Добавьте тестовые файлы

	
    echo "Тестовый файл 1" > storage/test1.txt
echo "Тестовый файл 2" > storage/test2.txt
mkdir storage/documents
echo "Важный документ" > storage/documents/readme.txt

Шаг 6: Запустите и проверьте

Откройте браузер и перейдите по адресу:
  • http://localhost/ - главная страница
  • http://localhost/files - файловый менеджер
  • http://localhost/files/documents - папка с документами

📚 Итог


Мы сделали полноценное веб-приложение на чистом PHP, которое:
  • Использует единую точку входа
  • Поддерживает ЧПУ
  • Работает по архитектуре MVC
  • Безопасно показывает файлы из указанной папки
  • Легко расширяется и модифицируется