🎯 Что мы создаем?
Минимальный файловый менеджер с шаблонами на чистом 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
- Безопасно показывает файлы из указанной папки
- Легко расширяется и модифицируется