Управление документами пользователей в PHP: от загрузки до выдачи
Организация работы с документами пользователей в PHP
Основное решение: класс для управления загрузкой и хранением документов
Наиболее эффективный подход предполагает создание выделенного класса UserDocument, который инкапсулирует логику загрузки, валидации, сохранения и удаления файлов. Класс взаимодействует с базой данных через PDO для хранения метаданных (имя, тип, размер, путь, дата загрузки, идентификатор пользователя). Файлы размещаются в файловой системе вне корня веб-сервера (например, /var/data/documents/), что повышает безопасность. Для доступа к документам используется скрипт-посредник, который проверяет права пользователя и отдает файл через readfile().
class UserDocument {
private PDO $pdo;
private string $storagePath;
private int $userId;
public function __construct(PDO $pdo, string $storagePath, int $userId) {
$this->pdo = $pdo;
$this->storagePath = rtrim($storagePath, '/');
$this->userId = $userId;
}
public function upload(array $file, array $allowedMimes = ['application/pdf', 'image/jpeg'], int $maxSize = 5242880): ?int {
// 1. Валидация
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Ошибка загрузки файла: код ' . $file['error']);
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowedMimes, true)) {
throw new RuntimeException('Недопустимый MIME-тип: ' . $mime);
}
if ($file['size'] > $maxSize) {
throw new RuntimeException('Размер файла превышает лимит ' . ($maxSize / 1024 / 1024) . ' МБ');
}
// 2. Генерация уникального имени и перемещение
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = bin2hex(random_bytes(16)) . '.' . $ext;
$destination = $this->storagePath . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new RuntimeException('Не удалось переместить файл');
}
// 3. Сохранение метаданных в БД
$sql = 'INSERT INTO user_documents (user_id, original_name, filename, mime_type, size, storage_path) VALUES (?, ?, ?, ?, ?, ?)';
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$this->userId, $file['name'], $filename, $mime, $file['size'], $destination]);
return (int) $this->pdo->lastInsertId();
}
}
User type php name (тип пользователя в php)
В этом примере класс принимает PDO-соединение, путь к хранилищу и идентификатор пользователя. Метод upload() выполняет проверки и перемещает файл. Возвращается идентификатор вставленной записи. Аналогично реализуются методы delete(), getById() и serve() для выдачи файла.
Типичные проблемы и их исправление
- Ошибка 500 при загрузке – проверка upload_max_filesize и post_max_size в php.ini, а также max_file_uploads.
- Файл не сохраняется – неверно задан путь $storagePath или отсутствуют права на запись для веб-сервера.
- Подмена MIME-типа – проверка через finfo (по содержимому) вместо $_FILES['file']['type'] (который приходит от клиента).
- SQL-инъекция – обязательное использование подготовленных запросов PDO.
Вариант 1: Как загрузить документ с валидацией только по расширению?
Иногда требуется просто проверить расширение файла, без анализа содержимого. Такой подход удобен для быстрых прототипов, но не гарантирует безопасность – злоумышленник может переименовать вредоносный скрипт в .pdf. Пример:
$allowedExtensions = ['pdf', 'jpg', 'png'];
$ext = strtolower(pathinfo($_FILES['doc']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExtensions, true)) {
die('Расширение не разрешено');
}
User group php (группа пользователей в php)
Этот вариант подходит для внутренних систем с доверенными пользователями, где риски минимальны. Однако в публичных приложениях лучше применять проверку MIME через finfo.
Вариант 2: Как хранить документы непосредственно в базе данных (BLOB)?
Хранение файлов целиком в столбце BLOB (например, LONGBLOB) удобно для небольших документов (фотографии, подписи) и упрощает резервное копирование – данные находятся в одной БД. Пример вставки:
$sql = 'INSERT INTO user_documents (user_id, original_name, content, mime_type) VALUES (?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$userId,
$_FILES['doc']['name'],
file_get_contents($_FILES['doc']['tmp_name']),
mime_content_type($_FILES['doc']['tmp_name'])
]);
Php user ip (ip-адрес пользователя в php)
Недостатки: БД быстро растёт в размере, замедляется резервное копирование, сложно отдавать файлы через веб-сервер напрямую (требуется PHP-скрипт). Используется только для файлов до нескольких мегабайт.
Вариант 3: Как реализовать множественную загрузку через одно поле формы?
Для поля с атрибутом multiple в HTML массив $_FILES имеет структуру с вложенными массивами. Пример обработки:
if (isset($_FILES['documents'])) {
$files = $_FILES['documents'];
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
$singleFile = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i],
];
// then process $singleFile with the class method
}
}
Remote user php (удаленный пользователь в php)
Такой цикл позволяет обработать каждый загруженный документ индивидуально, применяя ту же валидацию и сохранение.
Вариант 4: Как защитить загруженные документы от прямого доступа по URL?
Файлы следует сохранять вне DocumentRoot (например, /var/data/documents/). Для выдачи документа использовать PHP-скрипт, который проверяет права пользователя и отдаёт файл:
$document = UserDocument::getById($id, $pdo);
if ($document && $document['user_id'] === $currentUserId) {
header('Content-Type: ' . $document['mime_type']);
header('Content-Disposition: inline; filename="' . $document['original_name'] . '"');
readfile($document['storage_path']);
exit;
} else {
http_response_code(403);
echo 'Доступ запрещён';
}
Этот подход гарантирует, что только авторизованный владелец документа сможет его просмотреть или скачать.
Ошибки при работе с разными вариантами
- BLOB-поле слишком маленькое – для файлов больше 1 МБ выбирать MEDIUMBLOB или LONGBLOB.
- Проблемы с кодировкой имени файла – использовать basename() и mb_convert_encoding() при необходимости.
- Утечка пути к файлу – никогда не передавать storage_path клиенту, только идентификатор записи.
Расширенные примеры работы с документами пользователя
Пример 1: Полный класс UserDocument с методами upload, delete, serve
Реализация включает все типичные операции. Обратите внимание на обработку ошибок и логирование.
class UserDocument {
private PDO $pdo;
private string $storagePath;
private int $userId;
public function __construct(PDO $pdo, string $storagePath, int $userId) {
$this->pdo = $pdo;
$this->storagePath = rtrim($storagePath, '/');
$this->userId = $userId;
}
public function upload(array $file, array $allowedMimes = ['application/pdf'], int $maxSize = 10485760): ?int {
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload error code: ' . $file['error']);
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowedMimes, true)) {
throw new UnexpectedValueException('MIME type ' . $mime . ' not allowed');
}
if ($file['size'] > $maxSize) {
throw new RangeException('File size ' . $file['size'] . ' exceeds limit ' . $maxSize);
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = $this->storagePath . '/' . $newName;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
throw new RuntimeException('Failed to move uploaded file');
}
$sql = 'INSERT INTO user_documents (user_id, original_name, filename, mime_type, size, storage_path, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())';
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$this->userId, $file['name'], $newName, $mime, $file['size'], $dest]);
return (int) $this->pdo->lastInsertId();
}
public function delete(int $documentId): bool {
$stmt = $this->pdo->prepare('SELECT storage_path FROM user_documents WHERE id = ? AND user_id = ?');
$stmt->execute([$documentId, $this->userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return false;
}
if (file_exists($row['storage_path'])) {
unlink($row['storage_path']);
}
$stmt = $this->pdo->prepare('DELETE FROM user_documents WHERE id = ?');
return $stmt->execute([$documentId]);
}
public function serve(int $documentId): void {
$stmt = $this->pdo->prepare('SELECT * FROM user_documents WHERE id = ? AND user_id = ?');
$stmt->execute([$documentId, $this->userId]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$doc) {
http_response_code(404);
echo 'Document not found';
return;
}
header('Content-Type: ' . $doc['mime_type']);
header('Content-Disposition: filename="' . basename($doc['original_name']) . '"');
header('Content-Length: ' . $doc['size']);
readfile($doc['storage_path']);
}
// Additional methods: getList(), getById() etc.
}
Результат работы метода upload при успешной загрузке (например, через отладку):
int(42) // идентификатор записи о документе в таблице user_documents
Пример 2: Работа с изображениями – автоматическое создание миниатюры
Используя библиотеку GD или Intervention Image, можно после загрузки создавать уменьшенную копию документа (например, для аватарок).
// после успешной загрузки файла
$image = new \Intervention\Image\ImageManager(/* driver */);
$img = $image->make($destination);
$thumbnailPath = dirname($destination) . '/thumb_' . $newName;
$img->resize(150, 150)->save($thumbnailPath);
// сохраняем путь к оригиналу и миниатюре в БД
$sql = 'INSERT INTO user_documents (user_id, original_name, filename, mime_type, size, storage_path, thumbnail_path) VALUES (?, ?, ?, ?, ?, ?, ?)';
После этого миниатюра может быть показана в списке документов без загрузки полного файла.
Пример 3: Загрузка документов из base64 (API, мобильные клиенты)
Иногда документы приходят в формате base64. Обработка такого варианта:
$base64 = 'data:application/pdf;base64,JVBERi0xLjQK...';
$data = explode(',', $base64)[1];
$decoded = base64_decode($data, true);
if ($decoded === false) {
throw new InvalidArgumentException('Invalid base64 data');
}
$tmpPath = sys_get_temp_dir() . '/' . uniqid('upload_', true) . '.tmp';
file_put_contents($tmpPath, $decoded);
$file = [
'name' => 'document_from_api.pdf',
'type' => 'application/pdf',
'tmp_name' => $tmpPath,
'error' => UPLOAD_ERR_OK,
'size' => strlen($decoded),
];
$docId = $uploader->upload($file);
unlink($tmpPath); // удаляем временный файл
Такой код полезен при интеграции с внешними сервисами или для загрузки через REST API.
Пример 4: Вывод списка документов пользователя с ссылками на скачивание
$stmt = $pdo->prepare('SELECT id, original_name, mime_type, size, created_at FROM user_documents WHERE user_id = ?');
$stmt->execute([$userId]);
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<ul>
<? foreach ($documents as $doc): ?>
<li>
<a href="/download.php?id=<?= $doc['id'] ?>"><?= htmlspecialchars($doc['original_name']) ?></a>
(<?= number_format($doc['size'] / 1024, 2) ?> КБ)
</li>
<? endforeach; ?>
</ul>
В файле download.php вызывается метод serve() соответствующего класса.