Создание и администрирование учётных записей в PHP с поддержкой баз данных
Основные подходы к хранению пользователей в PHP
Как реализовать базу пользователей с помощью PDO и MySQL?
Цель
Создать надёжное хранилище учётных записей с защитой от SQL-инъекций, поддержкой разных СУБД и удобным кодом. Этот подход считается наиболее эффективным для веб-проектов.
Подготовка таблицы
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Таблица содержит уникальные логин и email, хэш пароля и метку времени регистрации.
Регистрация пользователя
<?php
$pdo = new PDO('mysql:host=localhost;dbname=mydb;charset=utf8', 'user', 'password');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$username = $_POST['username'];
$email = $_POST['email'];
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$username, $email, $hash]);
?>
Подготовленный запрос защищает от инъекций, а password_hash создаёт безопасный хэш.
Типичная ошибка
Если не задать PDO::ATTR_ERRMODE, можно пропустить ошибки дублирования уникальных полей. Решение: обрабатывать исключение или проверять существование пользователя перед вставкой.
try {
$stmt->execute([$username, $email, $hash]);
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
// дубликат username или email
}
}
Аутентификация
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute([$_POST['username']]);
$user = $stmt->fetch();
if ($user && password_verify($_POST['password'], $user['password_hash'])) {
// успешный вход
session_start();
$_SESSION['user_id'] = $user['id'];
}
password_verify сравнивает введённый пароль с хэшем без раскрытия оригинала.
Проблема: слабый пароль
Хэширование само по себе не гарантирует стойкость, если пароль слишком прост. Решение: добавить проверку длины и сложности на стороне клиента и сервера.
if (strlen($password) < 8) {
// ошибка: пароль должен быть не менее 8 символов
}
Как сохранить пользователей в SQLite для небольшого проекта?
Цель
Использовать встроенную базу данных без отдельного сервера – идеально для прототипов, локальных тестов или приложений с одним пользователем.
$pdo = new PDO('sqlite:/path/to/users.db');
$pdo->exec('CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL
)');
Код практически идентичен MySQL, меняется только DSN. Важно: SQLite не поддерживает параллельную запись на уровне таблиц, поэтому для высоконагруженных проектов не подходит.
Ошибка: блокировка базы
При одновременных записях SQLite может выдать ошибку database is locked. Решение: установить busy timeout.
$pdo->exec('PRAGMA busy_timeout = 3000');
Как обойтись без SQL, используя файловое хранилище (JSON)?
Цель
Хранить пользователей в JSON-файле для максимальной простоты, без установки СУБД. Подходит для одноразовых скриптов или демонстрации.
$users = json_decode(file_get_contents('users.json'), true);
$users[] = [
'id' => uniqid(),
'username' => $username,
'email' => $email,
'password_hash' => password_hash($password, PASSWORD_DEFAULT)
];
file_put_contents('users.json', json_encode($users, JSON_PRETTY_PRINT));
Проблема: гонка данных
При одновременных запросах файл может быть повреждён. Для реального проекта такая схема неприемлема.
Решение:
Использовать блокировку файла (flock) или перейти на реляционную базу.
$file = fopen('users.json', 'c+');
flock($file, LOCK_EX);
// чтение, запись
flock($file, LOCK_UN);
fclose($file);
Как применить ORM (например, Doctrine) для работы с пользователями?
Цель
Автоматизировать маппинг, миграции и запросы, ускорить разработку крупных проектов.
// Сущность
/**
* @ORM\Entity
* @ORM\Table(name="users")
*/
class User {
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/** @ORM\Column(type="string", unique=true) */
private $username;
// ...
}
// Сохранение
$entityManager->persist($user);
$entityManager->flush();
Ошибка: настройка соединения
Если неверно указать драйвер или параметры, Doctrine не сможет подключиться. Решение: проверять конфигурацию через bin/console doctrine:database:create.
Расширенные примеры работы с базой пользователей
Пример 1. Обновление профиля с проверкой прав
if ($_SESSION['user_id'] != $user_id) {
die('Доступ запрещён');
}
$stmt = $pdo->prepare('UPDATE users SET email = ? WHERE id = ?');
$stmt->execute([$new_email, $user_id]);
// При успехе возвращается количество затронутых строк (1)
Пример 2. Пагинация списка пользователей
$page = $_GET['page'] ?? 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$countStmt = $pdo->query('SELECT COUNT(*) FROM users');
$total = $countStmt->fetchColumn();
$stmt = $pdo->prepare('SELECT id, username, email FROM users ORDER BY id DESC LIMIT ? OFFSET ?');
$stmt->bindValue(1, $limit, PDO::PARAM_INT);
$stmt->bindValue(2, $offset, PDO::PARAM_INT);
$stmt->execute();
$users = $stmt->fetchAll();
// $users - массив строк, $total - общее количество
Пример 3. Поиск пользователей по части имени с использованием LIKE
$searchTerm = '%' . $_GET['q'] . '%';
$stmt = $pdo->prepare('SELECT * FROM users WHERE username LIKE ?');
$stmt->execute([$searchTerm]);
$results = $stmt->fetchAll();
// Например, при q="jo" найдутся "john", "jo_doe"
Пример 4. Транзакция: массовая вставка или удаление с откатом
try {
$pdo->beginTransaction();
$pdo->exec('DELETE FROM users WHERE id = 1');
$pdo->exec('DELETE FROM users WHERE id = 2');
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
echo 'Ошибка: ' . $e->getMessage();
}
// Если первый DELETE выполнился, а второй нет, изменения откатываются
Пример 5. Хеширование пароля с явным указанием алгоритма и стоимостью
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$stored = '$2y$12$...';
if (password_verify($password, $stored)) {
echo 'Пароль верен';
}
// password_hash генерирует случайную соль, проверить можно только через password_verify
Пример 6. Запись ошибок в лог при неудачной аутентификации
if (!$user || !password_verify($password, $user['password_hash'])) {
error_log('Неудачная попытка входа для пользователя: ' . $_POST['username']);
// дополнительно: задержка перед ответом
sleep(2);
echo 'Неверный логин или пароль';
}
// Лог поможет выявить попытки подбора паролей
Пример 7. Использование подготовленного запроса с именованными параметрами
$stmt = $pdo->prepare('INSERT INTO users (username, email, password_hash) VALUES (:username, :email, :pass)');
$stmt->execute([
':username' => $username,
':email' => $email,
':pass' => $hash
]);
// Именованные параметры делают код более читаемым