Реализация обмена сообщениями между пользователями на PHP

Раздел: Разработка на PHP -> Управление пользователями в PHP

Организация обмена сообщениями между пользователями

Основное эффективное решение: хранение в реляционной базе данных с использованием PDO

Наиболее надёжный и масштабируемый способ – создание таблицы messages в MySQL (или другой СУБД) и работа через PDO. Это обеспечивает защиту от SQL-инъекций, поддержку транзакций и удобную работу с большими объёмами данных.

Как создать структуру базы данных для сообщений?

Сначала определим таблицу:


CREATE TABLE messages (
    id INT AUTO_INCREMENT PRIMARY KEY,
    sender_id INT NOT NULL,
    receiver_id INT NOT NULL,
    subject VARCHAR(255) DEFAULT NULL,
    message_text TEXT NOT NULL,
    is_read TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (sender_id) REFERENCES users(id),
    FOREIGN KEY (receiver_id) REFERENCES users(id)
);
  

User type php name (тип пользователя в php)

Поля: sender_id – отправитель, receiver_id – получатель, subject – тема, message_text – текст, is_read – флаг прочтения.

Типичные ошибки:

  • Отсутствие внешних ключей – возможны ссылки на несуществующих пользователей;
  • Неправильный тип данных для текста – TEXT позволяет хранить сообщения любой длины;
  • Забытый индекс на receiver_id – замедление выборки.

Как реализовать отправку сообщения с защитой от инъекций?


<?php
function sendMessage(PDO $pdo, int $sender, int $receiver, string $subject, string $message): bool {
    $sql = 'INSERT INTO messages (sender_id, receiver_id, subject, message_text) VALUES (:sender, :receiver, :subject, :message)';
    $stmt = $pdo->prepare($sql);
    return $stmt->execute([
        'sender' => $sender,
        'receiver' => $receiver,
        'subject' => htmlspecialchars($subject, ENT_QUOTES, 'UTF-8'),
        'message' => htmlspecialchars($message, ENT_QUOTES, 'UTF-8')
    ]);
}
?>
  

User group php (группа пользователей в php)

Используем prepared statements и функцию htmlspecialchars для защиты от XSS-атак. Всегда валидируйте ID пользователей – убедитесь, что они существуют.

Распространённая проблема: неэкранированный вывод сообщений

Если выводить текст сообщения напрямую без htmlspecialchars, злоумышленник может внедрить JavaScript-код. Также не следует доверять HTTP-заголовкам и сессиям – проверяйте права отправителя.

Как получить список всех сообщений для пользователя с пагинацией?


<?php
function getMessages(PDO $pdo, int $userId, int $page = 1, int $perPage = 20): array {
    $offset = ($page - 1) * $perPage;
    $sql = 'SELECT m.*, u.username as sender_name FROM messages m 
            JOIN users u ON m.sender_id = u.id 
            WHERE m.receiver_id = :userId 
            ORDER BY m.created_at DESC 
            LIMIT :limit OFFSET :offset';
    $stmt = $pdo->prepare($sql);
    $stmt->bindValue(':userId', $userId, PDO::PARAM_INT);
    $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
    $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
    $stmt->execute();
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
?>
  

Php user ip (ip-адрес пользователя в php)

Результат:

Array
(
    [0] => Array
        (
            [id] => 23
            [sender_id] => 5
            [receiver_id] => 42
            [subject] => Привет
            [message_text] => Текст сообщения
            [is_read] => 0
            [created_at] => 2025-03-12 10:30:00
            [sender_name] => Иван
        )
)
  

Remote user php (удаленный пользователь в php)

Пагинация обязательна для больших объёмов данных. Используйте LIMIT и OFFSET, а также считайте общее количество строк отдельным запросом.

Альтернативные подходы

1. Как сделать простую систему сообщений без базы данных, используя файлы?

Для совсем маленьких проектов можно хранить каждое сообщение в отдельном файле JSON. Например, в каталоге messages/user42/.


<?php
$message = [
    'from' => 5,
    'to' => 42,
    'text' => 'Привет',
    'time' => time()
];
file_put_contents('messages/42/msg_' . uniqid() . '.json', json_encode($message));
?>
  

User photo php (фото пользователя в php)

Недостатки:

  • Отсутствие конкурентного доступа – возможна потеря данных;
  • Сложность выборки (нет индексов);
  • Проблемы с безопасностью – файлы могут быть доступны напрямую.

2. Как использовать MySQLi процедурный стиль?

Для проектов, где PDO недоступен, подойдёт MySQLi, но с обязательным экранированием через mysqli_real_escape_string.


<?php
$sender = (int)$_SESSION['user_id'];
$receiver = (int)$_POST['receiver'];
$message = mysqli_real_escape_string($link, $_POST['message']);
mysqli_query($link, "INSERT INTO messages (sender_id, receiver_id, message_text) VALUES ($sender, $receiver, '$message')");
?>
  

Edits php id user (редактирование пользователя по id в php)

Опасность: ручное экранирование легко пропустить. Предпочтительнее PDO.

3. Как реализовать асинхронную отправку сообщений через AJAX?

На фронтенде JavaScript отправляет POST-запрос к PHP-скрипту, который сохраняет сообщение и возвращает JSON-ответ. Это улучшает пользовательский опыт – нет перезагрузки страницы.


// JavaScript (fetch)
const formData = new FormData();
formData.append('receiver', 42);
formData.append('message', 'Привет');
fetch('/send_message.php', { method: 'POST', body: formData })
  .then(response => response.json())
  .then(data => { if(data.success) alert('Отправлено'); });
  

Php script user (скрипт пользователя в php)


// PHP (send_message.php)
header('Content-Type: application/json');
// проверка сессии, валидация, сохранение
if (sendMessage($pdo, $sender, $receiver, $subject, $message)) {
    echo json_encode(['success' => true]);
} else {
    echo json_encode(['success' => false]);
}
  

Проблема: CSRF-атаки. Добавляйте токен в форму и проверяйте его.

4. Как организовать уведомления о новых сообщениях (flash messages)?

Самый простой способ – хранить непрочитанные сообщения в сессии и показывать на следующей странице. Но это не рабочее для реальных проектов – лучше использовать базу данных и проверять флаг is_read.

- User php mode (режим пользователя в php)
- Php create user (создание пользователя в php)
- String php user (строковое представление пользователя в php)

Расширенные примеры реализации

Полноценный класс для работы с сообщениями (PDO)

Пример

<?php
class MessageManager {
    private PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function send(int $sender, int $receiver, string $subject, string $text): int {
        $sql = 'INSERT INTO messages (sender_id, receiver_id, subject, message_text, created_at) 
                VALUES (:sender, :receiver, :subject, :text, NOW())';
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'sender' => $sender,
            'receiver' => $receiver,
            'subject' => htmlspecialchars($subject),
            'text' => htmlspecialchars($text)
        ]);
        return (int)$this->pdo->lastInsertId();
    }

    public function getInbox(int $userId, int $page = 1, int $perPage = 10): array {
        $offset = ($page - 1) * $perPage;
        $sql = 'SELECT m.id, m.subject, m.is_read, m.created_at, u.username AS sender_name
                FROM messages m
                JOIN users u ON m.sender_id = u.id
                WHERE m.receiver_id = :userId
                ORDER BY m.created_at DESC
                LIMIT :limit OFFSET :offset';
        $stmt = $this->pdo->prepare($sql);
        $stmt->bindValue(':userId', $userId, PDO::PARAM_INT);
        $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function markAsRead(int $messageId, int $userId): bool {
        $sql = 'UPDATE messages SET is_read = 1 WHERE id = :id AND receiver_id = :userId';
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['id' => $messageId, 'userId' => $userId]);
        return $stmt->rowCount() > 0;
    }

    public function deleteForUser(int $messageId, int $userId): bool {
        // Мягкое удаление: можно добавить поле deleted_receiver
        $sql = 'DELETE FROM messages WHERE id = :id AND (sender_id = :userId OR receiver_id = :userId)';
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['id' => $messageId, 'userId' => $userId]);
        return $stmt->rowCount() > 0;
    }

    public function countUnread(int $userId): int {
        $stmt = $this->pdo->prepare('SELECT COUNT(*) FROM messages WHERE receiver_id = :userId AND is_read = 0');
        $stmt->execute(['userId' => $userId]);
        return (int)$stmt->fetchColumn();
    }
}
?>

Пример использования:

Пример

<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$manager = new MessageManager($pdo);
$manager->send(5, 42, 'Вопрос', 'Как дела?');
echo 'Отправлено, ID: ' . $manager->lastInsertId();
?>
Отправлено, ID: 101

Миграция таблицы с помощью SQL (MySQL)

Пример

CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS messages (
    id INT AUTO_INCREMENT PRIMARY KEY,
    sender_id INT NOT NULL,
    receiver_id INT NOT NULL,
    subject VARCHAR(255) DEFAULT NULL,
    message_text TEXT NOT NULL,
    is_read TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_receiver (receiver_id, is_read),
    INDEX idx_sender (sender_id),
    FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (receiver_id) REFERENCES users(id) ON DELETE CASCADE
);

Вывод списка сообщений с пагинацией (HTML + PHP)

Пример

<?php
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 10;
$inbox = $manager->getInbox($_SESSION['user_id'], $page, $perPage);
?>
<table>
    <tr><th>Отправитель</th><th>Тема</th><th>Дата</th><th>Статус</th></tr>
    <?php foreach ($inbox as $msg): ?>
    <tr>
        <td><?= htmlspecialchars($msg['sender_name']) ?></td>
        <td><?= htmlspecialchars($msg['subject']) ?></td>
        <td><?= $msg['created_at'] ?></td>
        <td><?= $msg['is_read'] ? 'Прочитано' : 'Новое' ?></td>
    </tr>
    <?php endforeach; ?>
</table>
<?php
// Пагинация
$total = $manager->getInboxCount($_SESSION['user_id']); // доработайте метод
echo 'Страница ' . $page . ' из ' . ceil($total / $perPage);
?>

Отметка о прочтении при открытии сообщения (AJAX)

Пример

// PHP: mark.php
<?php
session_start();
require 'db.php';
$manager = new MessageManager($pdo);
$msgId = (int)$_POST['msg_id'];
$userId = $_SESSION['user_id'];
$result = $manager->markAsRead($msgId, $userId);
echo json_encode(['success' => $result]);
?>

// JavaScript
function markRead(msgId) {
    fetch('mark.php', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: 'msg_id=' + msgId
    });
}

Получение последних сообщений через WebSocket (продвинутый вариант)

Для реального времени используют WebSocket (например, Ratchet) или long polling. Пример с long polling:

Пример

// poll.php
<?php
session_start();
header('Content-Type: application/json');
$lastId = isset($_GET['last_id']) ? (int)$_GET['last_id'] : 0;
$userId = $_SESSION['user_id'];
$pdo = new PDO(...);
$stmt = $pdo->prepare('SELECT * FROM messages WHERE receiver_id = :uid AND id > :last_id ORDER BY id DESC LIMIT 10');
$stmt->execute(['uid' => $userId, 'last_id' => $lastId]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
?>

Обработка ошибок и валидация

Пример

<?php
try {
    if (!$manager->send($sender, $receiver, $subject, $text)) {
        throw new Exception('Не удалось отправить сообщение');
    }
} catch (PDOException $e) {
    error_log($e->getMessage());
    echo 'Внутренняя ошибка. Попробуйте позже.';
}
?>

Частая ошибка: не проверяется, существует ли пользователь с таким ID. Используйте SELECT COUNT(*) FROM users WHERE id = ? перед отправкой.

Сообщения пользователя в PHP - comments

En
User messages php (php)