PHP Урок 27. Комментарии к постам
Предыдущие уроки:
Программируем на PHP - Введение
PHP - Запросы от браузера к серверу
PHP - Как работает сервер
PHP - Урок 4. PHP - интерпретатор
PHP - Урок 5. Переменные сервера и глобальные переменные
PHP - Урок 6. Конструкции print и echo. Кавычки одинарные и двойные и конкатенация строк
PHP - Урок 7. Переменные, константы и условия
PHP - Урок 8. Точка входа в приложение. Настройка mod_rewrite и файл .htaccess
PHP - Урок 9. Массивы и switch. Кодим основной каркас
PHP - Урок 10. COOKIE
PHP - Урок 11. Функции. Добавляем ядро системы core.php
PHP - Урок 12. Обзор модели MVC. Добавляем шаблоны страниц в наше приложение
PHP - Урок 13. Введение в базы данных и SQL. СУБД MySQL. Подключаемся к БД из нашего приложения
PHP - Урок 14. Регистрация пользователей на сайте
PHP Урок 15. Авторизация пользователей
PHP. Урок 16. Проверка авторизации. Функция check().
PHP Урок 17. Добавляем CSS фреймворк Bootstrap и jQuery
PHP Урок 18. Загрузка файлов на сервер
PHP Урок 19. Добавляем меню навигации
PHP Урок 20. Создаем AJAX (JavaScript) API
PHP Урок 21. Циклы
PHP Урок 22. Данные пользователя.
PHP Урок 23. Подписчики и подписки
PHP Урок 24. Отправка и сохранение пользовательских постов
PHP Урок 25. Лента новостей.
PHP Урок 26. Прикрепление контента к постам
Теория
Принцип организации комментариев в различных пользовательских приложениях довольно разнообразный. Например в Голосе они построены иерархическим принципом. Причем тут отсутствует понятие постов вообще. В данной реализации блокчейна комментарии хранят в том числе и то что в нашем проекте называется постами. В голосе комментарии имеют родителя. Таким родителем может быть как другой комментарий (тогда комментарий является относящимся к нему и по сути ни чем не отличается от обычных комментариев), но также может принадлежать категории, тогда это уже похоже на пост. Кто работал с CMS MODX может обнаружить сходства комментариев Голоса с элементами MODX - ресурсами. В этом и нет ничего удивительного. Подобная иерархическая вложенность очень удобна и используется во многих проектах.В нашей же реализации основанной на таких соцсетях как ВК и FB реализация более простая. Комментарии не имеют вложенности, просто берется пост его ID и в таблицу БД комментариев добавляется комментарий со своим ID, ID поста и идентификатором автора комментария.
На самом деле реализовать иерархический принцип также довольно просто - просто посты и комментарии (а можно и еще чего-нибудь) нужно сохранять не в разных, а в одной и той же таблице.
Структура у них будет одинаковая, а дополнительную информацию уже хранить в других таблицах, чтобы не нарушать правильную форму (не было избыточных полей в таблицах).
Когда таблиц с дополнительной информацией много добавляют также таблицы - связи (по структуре похожие на нашу таблицу news - которая связывает пользователей с авторами на которые он подписан, а затем JOIN-ит таблицу posts для отображение нужных постов).
И вместе с собственным идентификатором у них имеется поле id родителя, которое также соответствует id записи из этой таблице. Тут кстати пригодится значение NULL в поле id родителя, которое будет означать, что это корневой элемент - без родителей. В частности данный прием очень нравится разработчикам CMS WordPress.
Для ускорения и упрощения создания дерева иерархии используют поле level, которое хранит целое неотрицательное число - уровень вложенности (ну а какой уровень принимать за первый (корневой элемент) - 0 или 1 решает разработчик.
Ну а мы рассмотрим нашу реализацию без иерархии.
Практика
Таблица БД
CREATE TABLE IF NOT EXISTS `mc_comment` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`post_id` bigint(20) unsigned NOT NULL,
`user_id` int(10) unsigned NOT NULL,
`comment_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`text` text NOT NULL,
`status` tinyint(3) unsigned NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
KEY `post_id` (`post_id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=10 ;
Тут вместо корневой записи (комментария) используется id поста (post_id) из таблице постов. В терминологии SQL это называют внешним ключом, потому что он служит ссылкой на запись из внешней таблицы.
У самих комментариев тоже есть посты, но поскольку нет поля для ссылки на родительский комментарий, то и вложенность не поддерживается. ID служит для нахождения комментария, изменения и удаления его автором.
Остальные поля по принципу как у постов, за исключением того, что нет поля для привязки контента. Ну лень мне было реализовывать это :)
Функции core.php
// Комментарии
function getPostComments($pdo, $post_id){
$post_id = $pdo->quote($post_id);
$sql = "SELECT mc_comment.id, mc_profile.user_id, comment_time, text, first_name, last_name FROM mc_comment INNER JOIN mc_profile ON mc_comment.user_id = mc_profile.user_id WHERE post_id=$post_id AND mc_comment.status != 0 ORDER BY comment_time DESC";
$stmt = $pdo->query($sql);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function addPostCommentData($pdo, $user_id, $post_id, $comment_text){
$post_id = $pdo->quote($post_id);
$user_id = $pdo->quote($user_id);
$comment_text = $pdo->quote($comment_text);
$sql_insert = "INSERT INTO mc_comment (post_id, user_id, text) VALUES ($post_id, $user_id, $comment_text)";
//print $sql_insert;
if($pdo->exec($sql_insert)){
return true;
}else{
return false;
}
}
function delPostCommentData($pdo, $user_id, $comment_id){
$user_id = $pdo->quote($user_id);
$comment_id = $pdo->quote($comment_id);
$sql_update = "UPDATE mc_comment SET status=0 WHERE id=$comment_id AND user_id=$user_id";
//print $sql_update;
if($pdo->exec($sql_update)){
return true;
}else{
return false;
}
}
Здесь все аналогично тому, что мы и изучали ранее про посты. Разница в том, что если посты мы искали по ID автора, то здесь комментарии - по id собственно поста.
Так как все посты имеют уникальный ID то нет необходимости указывать ID автора этого поста.
Ну а ID автора комментария конечно мы указываем при добавлении комментария, ведь по нему мы приклеиваем (INNER JOIN) данные об отправителе комментария из таблице profiles.
Опять же удаление комментария - это не команда SQL DELETE. Мы пишем так UPDATE mc_comment SET status=0 WHERE id=$comment_id AND user_id=$user_id
Так как при выборке записей у нас условие, чтобы был статус больше 0, то установим это поле в 0 мы их скроем, но при этом они останутся в БД.
Так делают не для сбора компромата и не для выполнения "Закона Яровой" (хотя второе именно это подразумневает). На самом деле все разработчики баз данных так делали еще до появления подобных законов по всему миру.
Но для нас это важно, чтобы не нарушить работу БД. У них - даже в InnoDB при больших объемах данных возникает необходимость разделить большие файлы на более компактные и, например, раскидать по кластерам. Если таблицы "дырявые" то возникает масса проблем, как минимум высокие затраты времени на оптимизацию данных файлов.
В других движках при частом использовании DELETE в больших таблицах (сотни тысяч и миллионы записей) проблемы могут возникать, как говориться на ровном месте.
Поэтому в крупных проектах DELETE не используют. Ну или придумывают какую-то свою фишку, вроде собственного движка БД.
Вызов в контроллере
Отдельной страницы для одного комментария нет (Хотя можно сделать как в твиттере - загрузка его по AJAX (по id) и отображение в модальном окне).
Поэтому и отдельной ветки в switch (типо comment) тоже нет. В уроке про посты мы уже видели функцию, извлекающую посты в переменную (массив).
case 'post':
---
$post_comments = getPostComments($pdo, $post_id);
---
Выше мы видели ее определение (Функции в core.php).
Таким образом, загрузив в массив $post_comments комментарии мы их с помощью foreach отображаем в шаблоне post.tpl.php снизу самого поста:
<? //var_dump($post_comments); ?>
<?foreach($post_comments as $post_comment){?>
<div>
<div class="row">
<div class="col-md-offset-1 col-md-1">
<a href="/blog/<?=$post_comment['user_id']?>">
<?if(file_exists('content/'.$post_comment['user_id'].'/s_ava.jpg')){?>
<*img class="img-responsive img-circle" src="/content/<?=$post_comment['user_id']?>/s_ava.jpg" alt="" />
<?}else{?>
<*img class="img-responsive img-circle" src="/content/default/s_ava.jpg" alt="" />
<?}?>
</a>
</div>
<div class="col-md-8">
<a href="/blog/<?=$post_comment['user_id']?>">
<?=$post_comment['first_name']?> <?=$post_comment['last_name']?>
</a>
<?=$post_comment['text']?><br />
<small><?=$post_comment['comment_time']?><?if($this_id == $post_comment['user_id']){?> | <a class="del-comment" data-comment-id="<?=$post_comment['id']?>" href="javascript:void();">Удалить</a><?}?></small>
</div>
</div>
<?}?>
foreach($post_comments as $post_comment)
перебирает этот массив каждую итерацию в $post_comment сохраняется текущий комментарий из таблицы mc_comments.
А дальше мы просто извлекаем из него нужные данные форматируя их разметкой HTML:
<a href="/blog/<?=$post_comment['user_id']?>">
<?=$post_comment['first_name']?> <?=$post_comment['last_name']?>
</a>
AJAX API
<?if($this_id == $post_comment['user_id']){?> | <a class="del-comment" data-comment-id="<?=$post_comment['id']?>" href="javascript:void();">Удалить</a><?}?>
Мы сравниваем id выданный пользователю при авторизации $this_id и id автора текущего комментария $post_comment['user_id'], если они равны - это означает, что текущий пользователь и есть автор этого комментария. Поэтому мы разрешаем ему удалить свой комментарий и выводим соответствующую ссылку.
Ссылка для удаления комментария у нас не совсем обычная <a class="del-comment" data-comment-id="<?=$post_comment['id']?>" href="javascript:void();">Удалить</a>
Мы добавляем класс del-comment (мы не можем добавить атрибут id, так как он должен быть уникальным - не повторяться в одном HTML-документе).
href="javascript:void();"
такая интересная ссылка позволяет нам заблокировать событие браузера - переход по ссылке, так чтобы он не заметил клика по ней. При этом назначенное нами событие (описанное далее) прекрасно отработает.
data-comment-id="<?=$post_comment['id']?>"
данный атрибут позволяет нам передать id текущего комментария в обработчик события клика по ссылке удалить данного комментария. Атрибуты, начинающиеся с data-, по сути являются некими переменными, где после data- как бы имя переменной, а через = соответственно ее значение. Мы им можем пользоваться когда работаем с DOM документа из JavaScript.
Посмотрим на функцию-обработчик клика по ссылке удалить:
$(".del-comment").on("click", function(){
var comment_id = $(this).data("comment-id");
//alert('content-item: ' + comment_id);
$.post(
'/api/delPostComment',
{
"user_id": this_id,
"user_hash": this_hash,
"comment_id": comment_id,
},
function(data){
// тут добавляем наш пост сверху выборки
//alert(data);
location.reload(true);
}
);
});
В обработчике клика мы получаем id нашего комментария с помощью var comment_id = $(this).data("comment-id");
и затем передаем его нашему api (coreapi.php):
function delPostComment($pdo){
$user_id = clearInt($_POST['user_id']);
$user_hash = clearStr($_POST['user_hash']);
$comment_id = clearInt($_POST['comment_id']);
//print_r($_POST);
if(check($pdo, $user_id, $user_hash)){
//print_r($_POST);
if(delPostCommentData($pdo, $user_id, $comment_id)){
print '{"response":1}';
}else{
print '{"response":0, "error":"Error added post"}';
}
}else{
print '{"response":0, "error":"No avtorized user"}';
}
}
Определение функции delPostCommentData у нас также описана выше (core.php). Остальное не отличается от рассмотренных в предыдущих уроках функций coreapi.php.
При успешном удалении комментария возвращается JSON структура с response: 1 (true). Получив это значение в функции обработчике клика по ссылке удалить мы можем добавить анимацию например fadeOut для скрытия комментария или просто удалить из структуры документа removeChild.
Поскольку удаление у нас всего-лишь изменение статуса - можно сделать как в ВК ссылку восстановить которая бы возвращала предыдущий статус добавив соответствующую функцию API или же изменив delPostComment на что-то вроде setPostCommentStatus.
Ваш пост поддержали следующие Инвесторы Сообщества "Добрый кит":
alex2016, polyideic, niiu, galina1, vadbars, vika-teplo, dobronrav
Поэтому я тоже проголосовал за него!
Узнать подробности о сообществе можно тут:
Разрешите представиться - Кит Добрый
Правила
Инструкция по внесению Инвестиционного взноса
Вы тоже можете стать Инвестором и поддержать проект!!!
Если Вы хотите отказаться от поддержки Доброго Кита, то ответьте на этот комментарий командой "!нехочу"
Руслан, спасибо за долгожданное продолжение уроков!
У меня накопилось много вопросов, так как я с 1-го урока все описанное делаю на локальном сервере, но никак еще до финальной визуализации не доходит - не все функции еще описаны, не все темплейты созданы и т.д. Попробую систематизировать:
Прошу прощения если все свалил в кучу - вопросы к разным урокам. Просто ожидал что проясню все это в последующих уроках.
И спасибо за возможность изучить новое для меня!
@ur7ez хорошо тогда я весь код на хаб скину и те ссылку дам. Ну и там в рокет-чатедусмаю расчскажу:)
Отлично, я пока отслеживаю репозиторий thesite на GitHub-e
Окей