PHP Урок 24. Отправка и сохранение пользовательских постов
PHP Урок 24. Отправка и сохранение пользовательских постов.
Предыдущие уроки:
Программируем на 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. Подписчики и подписки
Теория
В предыдущем уроке мы затронули тему подписок, на самом деле можно было бы эти уроки поменять местами. Но я почему то решил писать их именно в этой последовательности. На самом деле в следующем уроке при реализации ленты новостей, вы увидите что эти две таблицы в запросе к MySQL связаны (точнее связано еще больше, но реализацию ленты обеспечивают эти две). Таким образом наши подписки (авторы, которых мы читаем) и их (а также наши) новости вместе взятые, обеспечивают основу информационной (интерактивной) и главную составляющую "WEB2" соц. сети.Пока что наши подписки просто находятся в нашем списке, как и мы в списке у кого-то. Список основывается на таблице news.
Однако в этом списке кроме данных профиля пользователя нет ни какой информации. Мы же хотим, чтобы переходя на страницу пользователя, можно было бы читать его посты (новости).
Вот так примерно это может выглядеть. Приступаем к реализации.
Практика
Таблица БД
Структура таблицы с постами следующая:
CREATE TABLE IF NOT EXISTS `mc_post` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`post_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`content_id` bigint(20) unsigned DEFAULT NULL,
`text` text NOT NULL,
`likes_count` bigint(20) unsigned NOT NULL DEFAULT '0',
`status` tinyint(3) unsigned NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `likes_count` (`likes_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=55 ;
На самом деле здесь ID для самого поста можно было не создавать, а в качестве первичного ключа использовать пару столбцов - user_id и post_time.
Дело в том что user_id содержит id пользователя, отправившего пост, а post_time - время отправки данного поста.
Тем самым обеспечивается уникальность (ну если н писать по несколько постов в секунду =)) и получается первичный ключ по двум столбцам.
Однако здесь я решил сделать как на хабре. Добавил для каждой новости свой id, так проще.
Как видно тип bigint поэтому разрядов числа хватит на все новости.
Следует также обратить внимание на столбец likes_count. Он хранит число лайков. На самом деле он вовсе не обязателен, так как на самом деле лайки хранятся в другой таблице - mc_likes.
Данное поле я добавил для ускорения работы БД, так как ходят слухи что функция COUNT в InnoDB, с которой я работаю, не кэширует результат своей работы. Вот я и реализовал подобное кэширование самостоятельно.
В поле content_id хранится ссылка на запись в таблице mc_contents - это прикрепленный контент.
На самом деле в твиттере придумали более простой и удобный способ. Поскольку сообщения там короткие, в них добавляется специальная ссылка, которая, может быть скрыта, эта ссылка содержит id на сервере изображений и затем с помощью JS заменяется на тег IMG.
Функции core.php
// Функции работы с постами
// Все посте пользователя
function getUserPosts($pdo, $user_id, $from, $len){
$user_id = $pdo->quote($user_id);
$sql = "SELECT mc_post.id, mc_post.user_id, mc_post.post_time, mc_post.content_id, LEFT(mc_post.text, 180) as text, mc_content.content_time FROM mc_post LEFT OUTER JOIN mc_content ON mc_content.id = mc_post.content_id WHERE mc_post.user_id=$user_id AND mc_post.status != 0 ORDER BY post_time DESC LIMIT $from, $len";
$stmt = $pdo->query($sql);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Количество постов
function getUserPostsCount($pdo, $user_id){
$user_id = $pdo->quote($user_id);
$sql = "SELECT COUNT(id) FROM mc_post WHERE user_id = $user_id AND status != 0";
$stmt = $pdo->query($sql);
$row = $stmt->fetch(PDO::FETCH_NUM);
return $row[0];
}
// Выбранный пост
function getUserPost($pdo, $post_id){
$post_id = $pdo->quote($post_id);
$sql = "SELECT mc_post.user_id, first_name, last_name, post_time, content_id, text, LEFT(text, 180) as min_text FROM mc_post INNER JOIN mc_profile ON mc_post.user_id = mc_profile.user_id WHERE id=$post_id AND status != 0";
//print $sql;
$stmt = $pdo->query($sql);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// Прикрепить контент
function addPostData($pdo, $user_id, $post_text, $content_id){
$user_id = $pdo->quote($user_id);
$post_text = $pdo->quote($post_text);
$content_id_id = $pdo->quote($content_id);
$sql_insert = "INSERT INTO mc_post (user_id, text, content_id) VALUES ($user_id, $post_text, $content_id)";
if($pdo->exec($sql_insert)){
$last_id = $pdo->lastInsertId();
return $last_id;
}else{
return false;
}
}
// Обновление поста
function setPostData($pdo, $user_id, $post_id, $update_post_text){
$user_id = $pdo->quote($user_id);
$update_post_text = $pdo->quote($update_post_text);
$sql_update = "UPDATE mc_post SET text=$update_post_text WHERE id=$post_id";
//print $sql_update;
if($pdo->exec($sql_update)){
return true;
}else{
return false;
}
}
// Удаление поста
function delPostData($pdo, $user_id, $post_id){
$post_id = $pdo->quote($post_id);
$sql_update = "UPDATE mc_post SET status=0 WHERE id=$post_id";
//print $sql_update;
if($pdo->exec($sql_update)){
return true;
}else{
return false;
}
}
В функции getUserPosts($pdo, $user_id, $from, $len)
мы помимо объекта соединения с БД передаем id пользователя, чи новости хотим прочитать, а также два числа - с какой новости начинать просмотр и сколько всего новостей выводить.
Посмотрим на сам запрос: SELECT mc_post.id, mc_post.user_id, mc_post.post_time, mc_post.content_id, LEFT(mc_post.text, 180) as text, mc_content.content_time FROM mc_post LEFT OUTER JOIN mc_content ON mc_content.id = mc_post.content_id WHERE mc_post.user_id=$user_id AND mc_post.status != 0 ORDER BY post_time DESC LIMIT $from, $len
Как видно значения из параметров $from
и $len
мы подставили в сам запрос в LIMIT. Также LEFT OUTER JOIN mc_content ON mc_content.id = mc_post.content_id
, как я уже говорил позволяет склеивать таблицы. Тут мы берем таблицу mc_post с постами, но при этом знаем, что контент (например фотка со всей информацией о ней), хранится в таблице mc_contents. Чтобы понять какой контент к какому посту относится (или в терминах БД - какая таблица к какой), нам нужен общий признак - таким является значение content_id (идентификатор контента).
Таким образом мы в памяти сервера создаем из двух (или более) таблиц одну (операция JOIN).
Вызов в контроллере
case 'post':
$post_id = clearInt(route(2));
$post = getUserPost($pdo, $post_id);
//var_dump($post);
$author_id = $post['user_id'];
$avafile = 'content/'.$author_id.'/s_ava.jpg';
//var_dump(file_exists($avafile));
if(!file_exists($avafile)) $avafile = '/content/default/s_ava.jpg';
else $avafile = '/content/'.$author_id.'/s_ava.jpg';
if(!$post) header('location: /404');
$ins_mcts = getInsUserMcts($pdo, $this_id);
$in_mcts = getPostMcts($pdo, $post_id);
$post_comments = getPostComments($pdo, $post_id);
$post_likes_count = getPostLikesCount($pdo, $post_id);
$content_id = $post['content_id'];
$post_likes = getPostLikesData($pdo, $post_id);
$is_like = isPostLike($pdo, $post_id, $this_id);
//print $is_like;
if($content_id){
$content = getContentData($pdo, $content_id);
}
else{
$content = false;
}
//var_dump($post);
$title = $post['first_name'].' '.$post['last_name'];
$tpl = 'post';
break;
Как видно в одном посте достаточно много информации.
Для того чтобы увидеть чей-нибудь пост мы указываем в запросе строку post и через слэш его id: thesite.loc/post/10
Идентификатор получаем с помощью функции route: $post_id = clearInt(route(2));
(Само значение post (route(1)) у нас уже отработало в case).
А получаем информацию о нем так: $post = getUserPost($pdo, $post_id);
Остальные функции сохраняют в переменные значения, которые подставляются в шаблоне post.tpl.php
Добавление и другие действия (обновление и удаление) по принципу описанному в предыдущих уроках.
В файле assets/js/post.js:
$("#add_post").on("click", function(){
var post_text = $("#new_post").val();
//alert('user_id: ' + this_id + ' user_hash: ' + this_hash + ' msg: ' + msg);
$.post(
'/api/addPost',
{
"user_id": this_id,
"user_hash": this_hash,
"post_text": post_text,
"content_id": content_id,
},
function(data){
// тут добавляем наш пост сверху выборки
//alert(data.response);
//last_post_id = data.response;
location.reload(true);
}
);
});
И соответствующие функции в coreapi.php:
// Функции работы с постами
function addPost($pdo){
$user_id = clearInt($_POST['user_id']);
$user_hash = clearStr($_POST['user_hash']);
// TODO: validate min 2 chars
$post_text = clearStr($_POST['post_text']);
if(empty($post_text)){
header('Content-Type: application/json');
print '{"response":0, "error":"Length of 0 chars"}';
die();
}
$content_id = clearInt($_POST['content_id']);
//print $content_id;
header('Content-Type: application/json');
if(check($pdo, $user_id, $user_hash)){
//print_r($_POST);
if($last_id = addPostData($pdo, $user_id, $post_text, $content_id)){
print '{"response":'.$last_id.'}';
}else{
print '{"response":0, "error":"Error added post"}';
}
}else{
print '{"response":0, "error":"No avtorized user"}';
}
}
// Изменение поста
function setPost($pdo){
$user_id = clearInt($_POST['user_id']);
$post_id = clearInt($_POST['post_id']);
$user_hash = clearStr($_POST['user_hash']);
// TODO: validate min 2 chars
$update_post_text = clearStr($_POST['update_post_text']);
//header('Content-Type: application/json');
if(check($pdo, $user_id, $user_hash)){
//print_r($_POST);
if(setPostData($pdo, $user_id, $post_id, $update_post_text)){
print '{"response":1}';
}else{
print '{"response":0, "error":"Error added post"}';
}
}else{
print '{"response":0, "error":"No avtorized user"}';
}
}
// Удаление поста
function delPost($pdo){
$user_id = clearInt($_POST['user_id']);
$post_id = clearInt($_POST['post_id']);
$user_hash = clearStr($_POST['user_hash']);
//header('Content-Type: application/json');
if(check($pdo, $user_id, $user_hash)){
//print_r($_POST);
if(delPostData($pdo, $user_id, $post_id)){
print '{"response":1}';
}else{
print '{"response":0, "error":"Error added post"}';
}
}else{
print '{"response":0, "error":"No avtorized user"}';
}
}
В файле assets/js/post.js так же интересная функция:
$(document).on('ready', function(){
var ptext = $("#post").html();
// link
var re = /(https?:\/\/(?!www.youtube.com)[a-zA-Zа-яА-Я0-9-.]{2,64}\.[a-zA-Zа-яА-Я]{2,6}\/?[a-zA-Zа-яА-Я0-9-.&?=%_/]*)/g;
var ptext = ptext.replace(re, '<a onmouseover="onlink(window.event, this.href);" onmouseout="offlink();" href="$1">$1</a>');
// hashtags
var re_h = /#([a-zA-Zа-яА-Я0-9]{2,64})/g;
var ptext = ptext.replace(re_h, '<a href="/hashtag/$1">#$1</a>');
// youtube
var re_v = /https?:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9-]+)/g;
var ptext = ptext.replace(re_v, '<br /><div class="text-center"><iframe width="480" height="270" src="https://www.youtube.com/embed/$1?rel=0" frameborder="0" allowfullscreen></iframe></div>');
$("#post").html(ptext);
});
Делает она то, что заменяет ссылки на видео из ютуба на само собственно видео (фрейм). Делается это на уровне JavaScript (то есть сам пост загружается в браузер без видео, но уже после загрузки скрипт делает подмену и вы видите видео, а не ссылку.