Как создать форум на базе PHP/MySQL с нуля

В этом руководстве мы собираемся создать форум на базе PHP/MySQL с нуля. Этот туториал идеально подходит для того, чтобы освоить основы PHP и использования баз данных.

Шаг 1: Создание таблиц базы данных

При создании приложения всегда рекомендуется начинать с создания хорошей модели данных.

Давайте вкратце посмотрим, что мы собираемся создать в нашем приложении. Мы собираемся создать форум, на котором пользователи смогут создавать темы в различных категориях. Другие пользователи могут публиковать ответы на темы. Как вы можете видеть, я выделил пару существительных, которые представляют названия наших таблиц:

  • Категории;
  • Темы;
  • Сообщений;
  • Пользователи.

Выглядит довольно опрятно, да? Каждый квадрат - это таблица базы данных. В нем перечислены все столбцы, а строки между ними представляют взаимосвязи. Я объясню их далее, так что ничего страшного, если это не так сейчас это имеет для тебя большой смысл. Давайте бегло взглянем на схему базы данных:

Database Design

Я рассмотрю каждую таблицу, объяснив SQL, который я создал, используя приведенную выше схему. Некоторые редакторы, такие как MySQL Workbench (тот, который я использовал), могут генерировать. sql-файлы тоже, но я бы рекомендовал изучать SQL, потому что гораздо интереснее делать это самостоятельно.

Таблица пользователей

CREATE TABLE users (
user_id     INT(8) NOT NULL AUTO_INCREMENT,
user_name	VARCHAR(30) NOT NULL,
user_pass  	VARCHAR(255) NOT NULL,
user_email	VARCHAR(255) NOT NULL,
user_date	DATETIME NOT NULL,
user_level	INT(8) NOT NULL,
UNIQUE INDEX user_name_unique (user_name),
PRIMARY KEY (user_id)
) TYPE=INNODB;

CREATE TABLE используется для указания на то, что мы хотим создать новую таблицу. За инструкцией следует название таблицы, а все столбцы указаны в квадратных скобках. Названия всех полей говорят сами за себя, поэтому ниже мы обсудим только типы данных.

user_id

Идентификатор пользователя тип этого поля - INT, что означает, что это поле содержит целое число. Поле не может быть пустым (НЕ NULL) и увеличивается при вставке каждой записи. В нижней части таблицы вы можете видеть, что поле user_id также объявлено в качестве первичного ключа. Первичный ключ используется для уникальной идентификации каждой строки в таблице. Никакие две разные строки в таблице не могут иметь одинаковое значение (или комбинацию значений) во всех столбцах. Это может быть немного непонятно, поэтому вот небольшой пример.

Давайте предположим, что есть пользователь по имени Джон Доу. Если другой пользователь регистрируется с тем же именем, возникает проблема, потому что трудно определить, кто из пользователей кто. Вы не можете сказать наверняка, и база данных тоже не может сказать. Используя первичный ключ, эта проблема решается, поскольку обе темы уникальны. Все остальные таблицы тоже имеют первичные ключи, и они работают таким же образом.

user_name

Это текстовое поле, называемое полем VARCHAR в MySQL. Число в скобках - это максимальная длина. Пользователь может выбрать имя пользователя длиной до 30 символов. Это поле не может быть ПУСТЫМ. В нижней части таблицы вы можете видеть, что это поле объявлено УНИКАЛЬНЫМ, что означает, что одно и то же имя пользователя не может быть зарегистрировано дважды. Часть УНИКАЛЬНОГО ИНДЕКСА сообщает базе данных, что мы хотим добавить уникальный ключ. Далее, мы определили имя уникального ключа, в данном случае user_name_unique. В скобках указано поле, к которому применяется уникальный ключ, - user_name.

user_pass

Это поле равно полю user_name, за исключением максимальной длины. Поскольку пароль пользователя, независимо от длины, хэшируется с помощью sha1(), длина пароля всегда будет составлять 40 символов.

user_email

Это поле равно полю user_pass.

user_date

Это поле, в котором мы будем хранить дату регистрации пользователя. Его тип данных - DATETIME, и поле не может быть ПУСТЫМ.

user_level

Это поле содержит уровень пользователя, например "0" для обычного пользователя и "1" для администратора. Подробнее об этом позже.

Таблицы категорий:

CREATE TABLE categories (
cat_id     	 	INT(8) NOT NULL AUTO_INCREMENT,
cat_name	 	VARCHAR(255) NOT NULL,
cat_description 	VARCHAR(255) NOT NULL,
UNIQUE INDEX cat_name_unique (cat_name),
PRIMARY KEY (cat_id)
) TYPE=INNODB;

Эти типы данных в основном работают так же, как и те, что указаны в таблице users. Эта таблица также имеет первичный ключ, и название категории должно быть уникальным.

Таблица тем:

CREATE TABLE topics (
topic_id    	INT(8) NOT NULL AUTO_INCREMENT,
topic_subject  		VARCHAR(255) NOT NULL,
topic_date		DATETIME NOT NULL,
topic_cat		INT(8) NOT NULL,
topic_by		INT(8) NOT NULL,
PRIMARY KEY (topic_id)
) TYPE=INNODB;

Эта таблица почти такая же, как и другие таблицы, за исключением поля topic_by. Это поле относится к пользователю, создавшему тему. topic_cat относится к категории, к которой принадлежит тема. Мы не можем принудительно установить эти отношения, просто объявив поле. Мы должны сообщить базе данных, что это поле должно содержать букву e существующий user_id из таблицы users или действительный cat_id из таблицы categories. Мы добавим некоторые взаимосвязи чуть позже.

Таблица сообщений:

CREATE TABLE posts (
post_id     	INT(8) NOT NULL AUTO_INCREMENT,
post_content		TEXT NOT NULL,
post_date 		DATETIME NOT NULL,
post_topic		INT(8) NOT NULL,
post_by		INT(8) NOT NULL,
PRIMARY KEY (post_id)
) TYPE=INNODB;

Это то же самое, что и в остальных таблицах; здесь также есть поле, которое ссылается на user_id: поле post_by. Поле post_topic относится к теме, к которой относится сообщение.

Теперь, когда мы выполнили эти запросы, у нас есть довольно приличная модель данных, но связи по-прежнему отсутствуют. Давайте начнем с определения отношений. Мы собираемся использовать нечто, называемое внешним ключом. Внешний ключ - это ссылочное ограничение между двумя таблицами. Внешний ключ идентифицирует столбец или набор столбцов в одной (ссылочной) таблице, которая ссылается на столбец или набор столбцов в другой (r ссылочная) таблица. Некоторые условия:

  • Столбец в ссылочной таблице, на который ссылается внешний ключ, должен быть первичным ключом.
  • Значения, на которые ссылаются, должны существовать в таблице, на которую ссылаются.

При добавлении внешних ключей информация связывается воедино, что очень важно для нормализации базы данных. Теперь вы знаете, что такое внешний ключ и почему мы его используем. Пришло время добавить их в таблицы, которые мы уже создали, с помощью инструкции ALTER, которую можно использовать для изменения существующей таблицы.

Сначала мы свяжем темы с категориями:

ALTER TABLE topics ADD FOREIGN KEY(topic_cat) REFERENCES categories(cat_id)

В последней части запроса уже сказано, что происходит. Когда категория удаляется из базы данных, все темы тоже будут удалены. Если cat_id категории изменится, каждая тема также будет обновлена. Вот для чего предназначена КАСКАДНАЯ часть ON UPDATE. Конечно, вы можете отменить это, чтобы защитить свои данные, так что вы не сможете удалить сохраняйте категорию до тех пор, пока с ней все еще связаны темы. Если бы вы хотели это сделать, вы могли бы заменить каскадную часть ON DELETE на ON DELETE RESTRICT. Существуют также значения SET NULL и NO ACTION, которые говорят сами за себя.

Теперь каждая тема связана с определенной категорией. Давайте свяжем темы с пользователем, который их создает.

ALTER TABLE topics ADD FOREIGN KEY(topic_by) REFERENCES users(user_id) ON DELETE RESTRICT ON UPDATE CASCADE;

Этот внешний ключ такой же, как и предыдущий, но есть одно отличие: пользователь не может быть удален до тех пор, пока все еще существуют разделы с идентификатором пользователя. Мы не используем здесь CASCADE, потому что в наших темах может содержаться ценная информация. Мы бы не хотели, чтобы эта информация была удалена, если кто-то решит удалить свою учетную запись. Чтобы по-прежнему предоставлять пользователям возможность удалить свою учетную запись, вы могли бы создать функцию, которая анонимизирует все их темы, а затем удаляет их учетную запись. К сожалению, это выходит за рамки данного руководства.

Привязывайте сообщения к темам:

ALTER TABLE posts ADD FOREIGN KEY(post_topic) REFERENCES topics(topic_id) ON DELETE CASCADE ON UPDATE CASCADE;

И, наконец, свяжите каждое сообщение с пользователем, который его сделал:

ALTER TABLE posts ADD FOREIGN KEY(post_by) REFERENCES users(user_id) ON DELETE RESTRICT ON UPDATE CASCADE;

Это часть базы данных! Это была довольно большая работа, но в результате получилась отличная модель данных, которая определенно стоит затраченных усилий.

Шаг 2: Введение в систему верхнего и нижнего колонтитулов.

Каждая страница нашего форума нуждается в нескольких базовых вещах, таких как тип документа и некоторая разметка. Вот почему мы включим в него header. php файл в верхней части каждой страницы и footer. php в самом низу. Тот header. php файл содержит тип документа, ссылку на таблицу стилей и некоторую важную информацию о форуме, такую как тег заголовка и метатеги.

Тот header. php Файл:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
"https://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="nl" lang="nl">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<meta name="description" content="A short description." />
	<meta name="keywords" content="put, keywords, here" />
	<title>PHP-MySQL forum</title>
	<link rel="stylesheet" href="style.css" type="text/css">
</head>

<body>
<h1>My forum</h1>
	<div id="wrapper">
	<div id="menu">
		<a class="item" href="/forum/index.php">Home</a> -
		<a class="item" href="/forum/create_topic.php">Create a topic</a> -
		<a class="item" href="/forum/create_cat.php">Create a category</a>

		<div id="userbar">
		<div id="userbar">Hello Example. Not you? Log out.</div>
	</div>

		<div id="content">

Div-оболочка будет использоваться для упрощения оформления всей страницы. Раздел меню, очевидно, содержит меню со ссылками на страницы, которые нам еще предстоит создать, но это помогает чтобы немного посмотреть, куда мы движемся.

Пользовательская панель div будет использоваться для небольшой верхней панели, которая содержит некоторую информацию, такую как имя пользователя и ссылка на страницу выхода из системы. Очевидно, что страница содержимого содержит фактическое содержимое страницы.

Внимательный читатель, возможно, уже заметил, что мы упускаем некоторые моменты. Здесь нет </body> или </html>. Они находятся в footer. php файл, как вы можете видеть ниже.

</div><!-- content -->
</div><!-- wrapper -->
<div id="footer">Created for tutsplus</div>
</body>
</html>

Когда мы включаем компоненты верхнего и нижнего колонтитулов на каждой странице, остальная часть страницы встраивается между верхним и нижним колонтитулами. Этот метод имеет ряд преимуществ. Прежде всего, все будет оформлено правильно. Краткий пример:

<?php
$error = false;
 
if($error = false)
{
        //the beautifully styled content, everything looks good 
        echo '<div id="content">some text</div>';
}
else
{
                  //bad looking, unstyled error :-( 
} 
?>

Как вы можете видеть, страница без ошибок приведет к появлению красивой страницы с содержимым. Но если есть ошибка, все выглядит действительно уродливо; вот почему лучше убедиться, что не только реальный контент оформлен правильно, но и ошибки, которые мы можем получить.

Еще одним преимуществом является возможность быстрого внесения изменений. Вы можете убедиться в этом сами, отредактировав текст в footer. php когда вы закончите это руководство, вы заметите, что нижний колонтитул сразу меняется на каждой странице. Наконец, мы добавили таблицу стилей, которая предоставляет некоторую базовую разметку — ничего особенного.

body {
   background-color: #4E4E4E;
           text-align: center;                                /* make sure IE centers the page too */
 }

 #wrapper {
           width: 900px;
           margin: 0 auto;                                    /* center the page */
 }

#content {
          background-color: #fff;
          border: 1px solid #000;
          float: left;
          font-family: Arial;
          padding: 20px 30px;
          text-align: left;
          width: 100%;                                                       /* fill up the entire div */
}

#menu {
          float: left;
          border: 1px solid #000;
          border-bottom: none;                          /* avoid a double border */
          clear: both;                                                         /* clear:both makes sure the content div doesn't float next to this one but stays under it */
          width:100%;
          height:20px;
          padding: 0 30px;
          background-color: #FFF;
          text-align: left;
          font-size: 85%;
}

 #menu a:hover {
              background-color: #009FC1;
}

#userbar {
          background-color: #fff;
          float: right;
          width: 250px;
 }

#footer {
          clear: both;
}

 /* begin table styles */

 table {
         border-collapse: collapse;
          width: 100%;
}

table a {
         color: #000;
}

 table a:hover {
          color:#373737;
          text-decoration: none;
 }

th {
          background-color: #B40E1F;
          color: #F0F0F0;
}

td {
          padding: 5px;
}

 /* Begin font styles */

 h1, #footer {
          font-family: Arial;
          color: #F1F3F1;
}

 h3 {margin: 0; padding: 0;}

 /* Menu styles */

 .item {
          background-color: #00728B;
          border: 1px solid #032472;
          color: #FFF;
          font-family: Arial;
          padding: 3px;
          text-decoration: none;
}

.leftpart {
          width: 70%;
}

 .rightpart {
          width: 30%;
}

 .small {
        font-size: 75%;
      color: #373737;


 #footer {
        font-size: 65%;
        padding: 3px 0 0 0;
}

 .topic-post {
        height: 100px;
        overflow: auto;
}

 .post-content {
        padding: 30px;
 }

 textarea {
        width: 500px;
        height: 200px;
}

Шаг 3: Готовимся к действию.

Прежде чем мы сможем прочитать что-либо из нашей базы данных, нам нужно подключение. Вот что connect. php используется для. Мы включим его в каждый файл, который собираемся создать.

<?php
// connect.php 
$server = 'localhost';
$username = 'usernamehere';
$password = 'passwordhere';
$database = 'databasenamehere';
$conn = mysqli_connect($server, $username, $password, $database);
// Check connection 
if (!$conn) {
   exit('Error: could not establish database connection');
}
// Select database 
if (!mysqli_select_db($conn, $database)) {
   exit('Error: could not select the database');
}
?>

Просто замените значения переменных по умолчанию в верхней части страницы своими собственными, сохраните файл, и все готово!

Шаг 4 : Отображение обзора форума.

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

<?php
//create_cat.php 
include 'connect.php';
include 'header.php';
echo '<tr>';
              echo '<td class="leftpart">';
                              echo '<h3><a href="category.php?id=">Category name</a></h3> Category description goes here';
              echo '</td>';
              echo '<td class="rightpart">';                                                           
                                             echo '<a href="topic.php?id=">Topic subject</a> at 10-10';
              echo '</td>';
echo '</tr>';
include 'footer.php';

Вот оно: приятный, понятный обзор. Мы собираемся обновлять эту страницу на протяжении всего урока, чтобы она больше походила на конечный результат, шаг за шагом!

Шаг 5: Регистрация пользователя.

Давайте начнем с создания простой HTML-формы, чтобы новый пользователь мог зарегистрироваться.

Sign Up

Для обработки формы необходима страница на PHP. Мы собираемся использовать переменную $_SERVER. Сервер $_SERVER переменная - это массив со значениями, которые автоматически устанавливаются при каждом запросе. Одним из значений массива $_SERVER является REQUEST_METHOD. Когда страница запрашивается с помощью GET, эта переменная будет содержать значение GET. Когда страница запрашивается через POST, она будет содержать значение POST. Мы можем использовать это значение, чтобы проверить, была ли отправлена форма. Тот signup. php файл выглядит как следующий фрагмент.

<?php
//signin.php
include 'connect.php';
include 'header.php';
echo '<h3>Sign in</h3>';

//first, check if the user is already signed in. If that is the case, there is no need to display this page 
if(isset($_SESSION['signed_in']) && $_SESSION['signed_in'] == true)
{
    echo 'You are already signed in, you can <a href="signout.php">sign out</a> if you want.';
}
else
{
    if($_SERVER['REQUEST_METHOD'] != 'POST')
	{
		/*the form hasn't been posted yet, display it 
note that the action="" will cause the form to post to the same page it is on */

		echo '<form method="post" action=""> 
Username: <input type="text" name="user_name" /> 
Password: <input type="password" name="user_pass"> 
<input type="submit" value="Sign in" /> 
</form>';
	}
	else
	{
		/* so, the form has been posted, we'll process the data in three steps: 
1. Check the data 
2. Let the user refill the wrong fields (if necessary) 
3. Varify if the data is correct and return the correct response 
*/
		$errors = array(); /* declare the array for later use */

		if(!isset($_POST['user_name']))
		{
			$errors[] = 'The username field must not be empty.';
		}

		if(!isset($_POST['user_pass']))
		{
			$errors[] = 'The password field must not be empty.';
		}

		if(!empty($errors)) /*check for an empty array, if there are errors, they're in this array (note the ! operator)*/
		{
			echo 'Uh-oh.. a couple of fields are not filled in correctly..';
			echo '<ul>';
			foreach($errors as $key => $value) /* walk through the array so all the errors get displayed */
			{
				echo '<li>' . $value . '</li>'; /* this generates a nice error list */
			}
			echo '</ul>';
		}
		else
		{
			//the form has been posted without errors, so save it 
			//notice the use of mysql_real_escape_string, keep everything safe! 
			//also notice the sha1 function which hashes the password 
			$sql = "SELECT user_id, user_name, user_level FROM users WHERE user_name = ? AND user_pass = ?";

            $stmt = mysqli_prepare($conn, $sql);

            mysqli_stmt_bind_param($stmt, 'ss', $_POST['user_name'], sha1($_POST['user_pass']));

            $result = mysqli_stmt_execute($stmt);
			if(!$result)
			{
				//something went wrong, display the error 
				echo 'Something went wrong while signing in. Please try again later.';
				//echo mysql_error(); //debugging purposes, uncomment when needed 
			}
			else
			{
				//the query was successfully executed, there are 2 possibilities 
				//1. the query returned data, the user can be signed in 
				//2. the query returned an empty result set, the credentials were wrong 
                $result = mysqli_stmt_get_result($stmt);

				if (mysqli_num_rows($result) == 0) {
				{
					echo 'You have supplied a wrong user/password combination. Please try again.';
				}
				else
				{
					//set the $_SESSION['signed_in'] variable to TRUE 
					$_SESSION['signed_in'] = true;

					//we also put the user_id and user_name values in the $_SESSION, so we can use it at various pages 
					while ($row = mysqli_fetch_assoc($result)) {
					{
						$_SESSION['user_id'] 	= $row['user_id'];
						$_SESSION['user_name'] 	= $row['user_name'];
						$_SESSION['user_level'] = $row['user_level'];
					}

					echo 'Welcome, ' . $_SESSION['user_name'] . '. <a href="index.php">Proceed to the forum overview</a>.';
				}
			}
		}
	}
}

include 'footer.php';
?>

Много объяснений тион - это я n комментарии, которые я предоставил вместе с кодом, так что обязательно ознакомьтесь с ними. Обработка данных происходит в три этапа:

  • проверка данных;
  • если данные недействительны, снова покажите форму;
  • если данные действительны, сохраните запись в базе данных.

Часть PHP вполне понятна сама по себе. Однако SQL-запрос, вероятно, нуждается в дополнительном объяснении.

// Prepare the SQL statement with placeholders 
$sql = "INSERT INTO users(user_name, user_pass, user_email, user_date, user_level) 
VALUES (?, ?, ?, NOW(), 0)";

// Create a prepared statement 
$stmt = mysqli_prepare($conn, $sql);

// Bind parameters to the prepared statement 
mysqli_stmt_bind_param($stmt, 'sss', $_POST['user_name'], sha1($_POST['user_pass']), $_POST['user_email']);

// Execute the prepared statement 
$result = mysqli_stmt_execute($stmt);

Мы использовали подготовленные инструкции с заполнителями (?) для безопасной привязки параметров к инструкция e SQL. Это помогает предотвратить атаки с использованием SQL-инъекций. Мы использовали функцию mysqli_prepare() для создания подготовленного оператора и функцию mysqli_stmt_bind_param() для привязки параметров к оператору. Далее мы использовали функцию mysqli_stmt_execute() для выполнения подготовленного оператора.

Также вы можете видеть, что функция sha1() используется для шифрования пароля пользователя. Это тоже очень важная вещь, которую следует помнить. Никогда не вводите простой пароль как есть. Вы должны зашифровать его. Представьте себе хакера, которому каким-то образом удается получить доступ к вашей базе данных. Если они смогут увидеть все пароли в виде обычного текста, они смогут войти в любую учетную запись (администратора), которую захотят. Если столбцы паролей содержат строки sha1, они должны сначала взломать их, что практически невозможно.

Кроме того, также возможно использовать md5, но я всегда предпочитаю использовать sha1, потому что тесты доказали, что это немного быстрее.

Если процесс регистрации прошел успешно, вы должны увидеть что-то вроде этого:

Sign Up

Попробуй освежить себя Экран phpMyAdmin. Новая запись должна быть видна в таблице пользователей.

Шаг 6: Добавление аутентификации и пользовательских уровней

Важным аспектом форума является разница между обычными пользователями и администраторами/модераторами. Поскольку это небольшой форум и создание таких функций, как добавление новых модераторов, заняло бы слишком много времени, мы сосредоточимся на процессе входа в систему и добавим некоторые функции администратора, такие как создание новых категорий и закрытие темы.

Теперь, когда вы выполнили предыдущий шаг, мы собираемся сделать вашу недавно созданную учетную запись учетной записью администратора. В phpMyAdmin щелкните по таблице пользователей, а затем просмотрите. Ваша учетная запись, вероятно, появится сразу же. Нажмите на значок редактирования и измените значение поля user_level с 0 на 1. На данный момент это все. Вы не сразу заметите никакой разницы в нашем приложении, но когда мы добавим функции администратора, обычная учетная запись и ваша учетная запись будут обладать разными возможностями.

  • Процесс входа в систему работает следующим образом: Посетитель вводит пользовательские данные и отправляет это форма.
  • Если имя пользователя и пароль указаны правильно, мы можем начать сеанс.
  • Если имя пользователя и пароль указаны неверно, мы снова показываем форму с сообщением.

Sign Up

Тот signin. php файл показан в следующем фрагменте. Я не собираюсь объяснять, что я делаю, но ознакомьтесь с комментариями в файле.

<?php
//signin.php 
include 'connect.php';
include 'header.php';
echo '<h3>Sign in</h3>';

//first, check if the user is already signed in. If that is the case, there is no need to display this page 
if(isset($_SESSION['signed_in']) && $_SESSION['signed_in'] == true)
{
    echo 'You are already signed in, you can <a href="signout.php">sign out</a> if you want.';
}
else
{
    if($_SERVER['REQUEST_METHOD'] != 'POST')
	{
		/*the form hasn't been posted yet, display it 
note that the action="" will cause the form to post to the same page it is on */

		echo '<form method="post" action=""> 
Username: <input type="text" name="user_name" /> 
Password: <input type="password" name="user_pass"> 
<input type="submit" value="Sign in" /> 
</form>';
	}
	else
	{
		/* so, the form has been posted, we'll process the data in three steps: 
1. Check the data 
2. Let the user refill the wrong fields (if necessary) 
3. Varify if the data is correct and return the correct response 
*/
		$errors = array(); /* declare the array for later use */

		if(!isset($_POST['user_name']))
		{
			$errors[] = 'The username field must not be empty.';
		}

		if(!isset($_POST['user_pass']))
		{
			$errors[] = 'The password field must not be empty.';
		}

		if(!empty($errors)) /*check for an empty array, if there are errors, they're in this array (note the ! operator)*/
		{
			echo 'Uh-oh.. a couple of fields are not filled in correctly..';
			echo '<ul>';
			foreach($errors as $key => $value) /* walk through the array so all the errors get displayed */
			{
				echo '<li>' . $value . '</li>'; /* this generates a nice error list */
			}
			echo '</ul>';
		}
		else
		{
			//the form has been posted without errors, so save it 
			//notice the use of mysql_real_escape_string, keep everything safe! 
			//also notice the sha1 function which hashes the password 
			$sql = "SELECT user_id, user_name, user_level FROM users WHERE user_name = ? AND user_pass = ?";

            $stmt = mysqli_prepare($conn, $sql);

            mysqli_stmt_bind_param($stmt, 'ss', $_POST['user_name'], sha1($_POST['user_pass']));

            $result = mysqli_stmt_execute($stmt);

			if(!$result)
			{
				//something went wrong, display the error 
				echo 'Something went wrong while signing in. Please try again later.';
				//echo mysql_error(); //debugging purposes, uncomment when needed 
			}
			else
			{
				//the query was successfully executed, there are 2 possibilities 
				//1. the query returned data, the user can be signed in 
				//2. the query returned an empty result set, the credentials were wrong 
                $result = mysqli_stmt_get_result($stmt);

				if (mysqli_num_rows($result) == 0) {
				{
					echo 'You have supplied a wrong user/password combination. Please try again.';
				}
				else
				{
					//set the $_SESSION['signed_in'] variable to TRUE 
					$_SESSION['signed_in'] = true;

					//we also put the user_id and user_name values in the $_SESSION, so we can use it at various pages 
					while ($row = mysqli_fetch_assoc($result)) {
					{
						$_SESSION['user_id'] 	= $row['user_id'];
						$_SESSION['user_name'] 	= $row['user_name'];
						$_SESSION['user_level'] = $row['user_level'];
					}

					echo 'Welcome, ' . $_SESSION['user_name'] . '. <a href="index.php">Proceed to the forum overview</a>.';
				}
			}
		}
	}
}

include 'footer.php';
?>

Так это гораздо легче понять.

$sql = "SELECT user_id, user_name, user_level FROM users WHERE user_name = ? AND user_pass = ?";

Очевидно, что нам нужна проверка, чтобы определить, принадлежат ли предоставленные учетные данные существующему пользователю. Множество скриптов извлекают пароль из базы данных и сравнивают его с помощью PHP. Если мы сделаем это напрямую через SQL, пароль будет сохранен в базе данных один раз во время регистрации и никогда больше не покинет ее. Это безопаснее, потому что все реальные действия происходят на уровне базы данных, а не в нашем приложении.

Если пользователь успешно вошел в систему, мы выполняем несколько действий:

<?php
//set the $_SESSION['signed_in'] variable to TRUE 
$_SESSION['signed_in'] = true;    				
//we also put the user_id and user_name values in the $_SESSION, so we can use it at various pages 
while ($row = mysqli_fetch_assoc($result)) 
{
 	$_SESSION['user_id'] = $row['user_id'];
 	$_SESSION['user_name'] = $row['user_name'];	
}
?>

Во-первых, мы установили переменной signed_in session значение TRUE, чтобы мы могли использовать ее на других страницах, чтобы убедиться, что пользователь вошел в систему. Мы также помещаем имя пользователя и идентификатор пользователя в переменную $_SESSION для использования на другой странице. Наконец, мы выводим ссылку на обзор форума, чтобы пользователь мог сразу приступить к работе.

Конечно, для входа в систему требуется еще одна функция: выход из системы! Процесс выхода из системы на самом деле намного проще, чем процесс входа в систему. Поскольку вся информация о пользователе хранится в переменных $_SESSION, все, что нам нужно сделать это отключить их и отобразить сообщение.

Теперь, когда мы установили переменные $_SESSION, мы можем определить, вошел ли кто-то в систему. Давайте внесем последнее простое изменение в header. php: Заменять:

<div id="userbar">Hello Example. Not you? Log out.</div>

With:

<?php
<div id="userbar">
    if($_SESSION['signed_in'])
 	{
 	 	echo 'Hello' . $_SESSION['user_name'] . '. Not you? <a href="signout.php">Sign out</a>';
 	}
 	else
 	{
 		echo '<a href="signin.php">Sign in</a> or <a href="sign up">create an account</a>.';
 	}
</div>

Если пользователь вошел в систему, он увидит свое имя, отображаемое на главной странице, со ссылкой на страницу выхода. Часть аутентификации выполнена! К настоящему времени наш форум должен выглядеть примерно так:

Category View

Шаг 7: Создание категории

Мы хотим создать категории, поэтому давайте начнем с создания формы.

<form method="post" action="">
    Category name: <input type="text" name="cat_name" />
 	Category description: <textarea name="cat_description" /></textarea>
	<input type="submit" value="Add category" />
 </form>

Этот шаг во многом похож на шаг 4 (Регистрация пользователя), поэтому я не собираюсь здесь вдаваться в подробные объяснения. Если вы выполнили все шаги, вы должны быть в состоянии понять это довольно быстро.

<?php
// create_cat.php 
include 'connect.php';

if ($_SERVER['REQUEST_METHOD'] != 'POST') {
    // The form hasn't been posted yet, display it 
    echo "<form method='post' action=''> 
Category name: <input type='text' name='cat_name' /> 
Category description: <textarea name='cat_description'></textarea> 
<input type='submit' value='Add category' /> 
</form>";
} else {
    // The form has been posted, so save it 
    $cat_name = $_POST['cat_name'];
    $cat_description = $_POST['cat_description'];

    // Prepare the SQL statement with placeholders 
    $sql = "INSERT INTO categories (cat_name, cat_description) 
VALUES (?, ?)";

    // Create a prepared statement 
    $stmt = mysqli_prepare($conn, $sql);

    // Bind parameters to the prepared statement 
    mysqli_stmt_bind_param($stmt, 'ss', $cat_name, $cat_description);

    // Execute the prepared statement 
    $result = mysqli_stmt_execute($stmt);

    if (!$result) {
        // Something went wrong, display the error 
        echo 'Error: ' . mysqli_error($conn);
    } else {
        echo 'New category successfully added.';
    }

    // Close the prepared statement 
    mysqli_stmt_close($stmt);
}
?>

Как вы можете видеть, мы запустили скрипт с проверкой $_SERVER, предварительно проверив, есть ли у пользователя права администратора, которые необходимы для создания категории. Форма отображается, если она еще не была отправлена. Если это так, то значения сохраняются. Снова подготавливается и затем выполняется SQL-запрос.

Create a Category

Шаг 8: Добавление категорий в index. php

Мы создали несколько категорий, так что теперь мы можем отображать их на главной странице. Давайте добавим следующий запрос в область содержимого index. php.

SELECT
    categories.cat_id,
	categories.cat_name,
 	categories.cat_description,
FROM
 	categories;

Этот запрос выбирает все категории, а также их названия и описания из таблицы категорий. Нам нужно всего лишь немного PHP для отображения результатов. Если мы добавим эту часть точно так же, как мы делали на предыдущих шагах, код будет выглядеть следующим образом.

<?php
// create_cat.php 
include 'connect.php';
include 'header.php';

$sql = "SELECT 
cat_id, 
cat_name, 
cat_description 
FROM 
categories";

$result = mysqli_query($conn, $sql);

if (!$result) {
    echo 'The categories could not be displayed, please try again later.';
} else {
    if (mysqli_num_rows($result) == 0) {
        echo 'No categories defined yet.';
    } else {
        // Prepare the table 
        echo '<table border="1"> 
<tr> 
<th>Category</th> 
<th>Last topic</th> 
</tr>';

        while ($row = mysqli_fetch_assoc($result)) {
            echo '<tr>';
            echo '<td class="leftpart">';
            echo '<h3><a href="category.php?id">' . $row['cat_name'] . '</a></h3>' . $row['cat_description'];
            echo '</td>';
            echo '<td class="rightpart">';
            echo '<a href="topic.php?id=">Topic subject</a> at 10-10';
            echo '</td>';
            echo '</tr>';
        }
    }
}

include 'footer.php';
?>

Обратите внимание, как мы используем cat_id для создания ссылок на category. php. Все ссылки на эту страницу будут выглядеть следующим образом: category. php?cat_id=x, где x может быть любым числовым значением. Возможно, это для вас в новинку. Мы можем проверить URL-адрес с помощью PHP на наличие значений $_GET. Например, у нас есть эта ссылка:

category.php?cat_id=23

На следующих нескольких шагах мы будем использовать это значение для извлечения тем при просмотре одной категории, но темы нельзя просмотреть, если мы их еще не создали. Итак, давайте создадим несколько тем!

Шаг 9: Создание темы

На этом этапе мы комбинируем методы, которым научились на предыдущих этапах. Мы проверяем, вошел ли пользователь в систему, а затем используем входной запрос для создания темы и добавления некоторых базовых HTML-форм.

Структура этого create_topic. php файл вряд ли можно объяснить в виде списка или чего-то в этом роде, поэтому я переписал его в псевдокоде.

<?php
if(user is not signed in)
{
    //the user is not signed in 
}
else
{
	//the user is signed in 
	if(form has not been posted)
	{	
		//show form 
	}
	else
	{
		//process form 
	}
}
?>

Вот код для этой части нашего форума. Ознакомьтесь с пояснениями под кодом, чтобы увидеть, что он делает.

<?php
//create_cat.php 
include 'connect.php';
include 'header.php';

echo '<h2>Create a topic</h2>';
if($_SESSION['signed_in'] == false)
{
    //the user is not signed in 
    echo 'Sorry, you have to be <a href="/forum/signin.php">signed in</a> to create a topic.';
}
else
{
	//the user is signed in 
	if($_SERVER['REQUEST_METHOD'] != 'POST')
	{	
		//the form hasn't been posted yet, display it 
		//retrieve the categories from the database for use in the dropdown 
		$sql = "SELECT 
cat_id, 
cat_name, 
cat_description 
FROM 
categories";

        $result = mysqli_query($link, $sql);

		if(!$result)
		{
			//the query failed, uh-oh :-( 
			echo 'Error while selecting from database. Please try again later.';
		}
		else
		{
			if(mysqli_num_rows($result) == 0)
			{
				//there are no categories, so a topic can't be posted 
				if($_SESSION['user_level'] == 1)
				{
					echo 'You have not created categories yet.';
				}
				else
				{
					echo 'Before you can post a topic, you must wait for an admin to create some categories.';
				}
			}
			else
			{
				echo '<form method="post" action=""> 
Subject: <input type="text" name="topic_subject" /> 
Category:'; 

				echo '<select name="topic_cat">';
					while($row = mysqli_fetch_assoc($result))
					
						echo '<option value="' . $row['cat_id'] . '">' . $row['cat_name'] . '</option>';
					}
				echo '</select>';	

				echo 'Message: <textarea name="post_content" /></textarea> 
<input type="submit" value="Create topic" /> 
</form>';
			}
		}
	}
	else
	{
        mysqli_begin_transaction($link);

		//start the transaction 
		$query = "BEGIN WORK;";
        $result = mysqli_query($link, $query);

		if(!$result)
		{
			//Damn! the query failed, quit 
			echo 'An error occured while creating your topic. Please try again later.';
		}
		else
		{
            $topic_subject = mysqli_real_escape_string($link, $_POST['topic_subject']);
            $topic_cat = mysqli_real_escape_string($link, $_POST['topic_cat']);
            $topic_by = $_SESSION['user_id'];

			//the form has been posted, so save it 
			//insert the topic into the topics table first, then we'll save the post into the posts table 
			$sql = "INSERT INTO 
topics(topic_subject, 
topic_date, 
topic_cat, 
topic_by) 
VALUES('$topic_subject', 
NOW(), 
'$topic_cat', 
'$topic_by' 
)";

            $result = mysqli_query($link, $sql);

			if(!$result)
			{
				//something went wrong, display the error 
				echo 'An error occurred while inserting your data. Please try again later.' . mysqli_error($link);
                $sql = "ROLLBACK;";
                $result = mysqli_query($link, $sql);
			}
			else
			{
				//the first query worked, now start the second, posts query 
				//retrieve the id of the freshly created topic for usage in the posts query 
				$topicid = mysqli_insert_id($link);

                $post_content = mysqli_real_escape_string($link, $_POST['post_content']);
                $post_topic = $topicid;
                $post_by = $_SESSION['user_id'];

				$sql = "INSERT INTO 
posts(post_content, 
post_date, 
post_topic, 
post_by) 
VALUES 
('$post_content', 
NOW(), 
'$post_topic', 
'$post_by'
)";

                $result = mysqli_query($link, $sql);

				if(!$result)
				{
					//something went wrong, display the error 
					echo 'An error occured while inserting your post. Please try again later.' . mysql_error();
					$sql = "ROLLBACK;";
                    $result = mysqli_query($link, $sql);
				}
				else
				{
					$sql = "COMMIT;";
                    $result = mysqli_query($link, $sql);

					//after a lot of work, the query succeeded! 
					echo 'You have successfully created <a href="topic.php?id='. $topicid . '">your new topic</a>.';
				}
			}
		}
	}
}

include 'footer.php';
?>

Я расскажу об этой странице в двух частях: о показе формы и ее обработке.

Отображение формы

Мы начинаем с простой HTML-формы. На самом деле здесь есть что-то особенное, потому что мы используем выпадающий список. Этот выпадающий список заполняется данными из базы данных с использованием этого запроса:

SELECT
    cat_id,
 	cat_name,
 	cat_description
FROM
 	categories

Это единственная потенциально запутанная часть здесь; это довольно большой фрагмент кода, как вы можете видеть, глядя на create_topic. php файл в нижней части этого шага.

Обработка формы

Процесс сохранения топи c состоит из двух частей: сохранения темы в таблице topics и сохранения первого сообщения в таблице posts. Это требует чего-то довольно продвинутого, что немного выходит за рамки данного руководства. Это называется транзакцией, что в основном означает, что мы начинаем с выполнения команды start, а затем откатываемся назад, когда возникают ошибки базы данных, и фиксируем, когда все прошло хорошо. Узнайте больше о транзакциях.

<?php
//start the transaction 
$query  = "BEGIN WORK;";
$result = mysqli_query($query);
//stop the transaction 
$sql = "ROLLBACK;";
$result = mysqli_query($sql);
//commit the transaction 
$sql = "COMMIT;";
$result = mysqli_query($sql);
?>

Первым запросом, используемым для сохранения данных, является запрос на создание темы, который выглядит следующим образом:

$sql = "INSERT INTO 
topics(topic_subject, 
topic_date, 
topic_cat, 
topic_by) 
VALUES('$topic_subject', 
NOW(), 
'$topic_cat', 
'$topic_by' 
)";

Сначала определяются поля, затем значения, которые будут вставлены. Мы уже видели первый вариант раньше; это просто строка, которая становится безопасной с помощью функции mysqli_real_escape_string(). Второе значение, NOW(), является функцией SQL для текущего времени. Однако третье значение - это значение, которого мы раньше не видели. Это относится к (действительному) идентификатору категории. Последнее значение относится к (существующему) user_id, который в данном случае является значением $_SESSION['user_id']. Эта переменная была объявлена во время процесса входа в систему.

Если запрос выполнен без ошибок, мы переходим ко второму запросу. Помните, что мы все еще проводим здесь транзакцию. Если бы мы получили ошибки, мы бы использовали команду ОТКАТА.

$sql = "INSERT INTO 
posts(post_content, 
post_date, 
post_topic, 
post_by) 
VALUES 
('$post_content', 
NOW(), 
'$post_topic', 
'$post_by' 
)";

Первое, что мы делаем в этом коде, - это используем функцию mysql_insert_id() для извлечения последний сгенерированный идентификатор из поля topic_id в таблице topics. Как вы, возможно, помните из первых шагов этого руководства, идентификатор генерируется в базе данных с использованием автоматического увеличения.

Затем запись вставляется в таблицу posts. Этот запрос очень похож на запрос topics. Единственная разница заключается в том, что этот пост относится к теме, а тема относится к категории. С самого начала мы решили создать хорошую модель данных, и вот результат: приятная иерархическая структура.

Create a Topic

Шаг 10: Просмотр категории

Мы собираемся создать обзорную страницу для одной категории. Мы только что создали категорию, и было бы удобно иметь возможность просматривать все темы в ней. Сначала создайте страницу под названием category. php.

Вот краткий список того, что нам нужно:

Поля для отображения информации о категории:

  • имя кошки,
  • описание кошки

Поля для отображения информации по теме:

  • topic_id
  • topic_subject
  • topic_date
  • topic_cat

Давайте создадим SQL-запросы, которые извлекают именно эти данные из daтабаза.

SELECT
    cat_id,
    cat_name,
    cat_description
FROM
    categories
WHERE
    cat_id = " . mysqli_real_escape_string($_GET['id'])

Приведенный выше запрос выбирает все категории из базы данных.

SELECT    
    topic_id,
    topic_subject,
    topic_date,
    topic_cat
FROM
    topics
WHERE
    topic_cat = " . mysqli_real_escape_string($_GET['id'])

Приведенный выше запрос выполняется в цикле while, в котором мы печатаем информацию о категории. Сделав это таким образом, мы увидим все категории и последнюю тему для каждой из них. Полный код category. php будет следующим:

<?php
// create_cat.php 
include 'connect.php';
include 'header.php';

// first select the category based on $_GET['cat_id'] 
$sql = "SELECT 
cat_id, 
cat_name, 
cat_description 
FROM 
categories 
WHERE 
cat_id = " . mysqli_real_escape_string($conn, $_GET['id']);

$result = mysqli_query($conn, $sql);

if(!$result)
{
    echo 'The category could not be displayed, please try again later.' . mysqli_error($conn);
}
else
{
    if(mysqli_num_rows($result) == 0)
	{
		echo 'This category does not exist.';
	}
	else
	{
		// display category data 
		while($row = mysqli_fetch_assoc($result))
		{
			echo '<h2>Topics in ′' . $row['cat_name'] . '′ category</h2>';
		}

		// do a query for the topics 
		$sql = "SELECT 
topic_id, 
topic_subject, 
topic_date, 
topic_cat 
FROM 
topics 
WHERE 
topic_cat = " . mysqli_real_escape_string($conn, $_GET['id']);

		$result = mysqli_query($conn, $sql);

		if(!$result)
		{
			echo 'The topics could not be displayed, please try again later.';
		}
		else
		{
			if(mysqli_num_rows($result) == 0)
			{
				echo 'There are no topics in this category yet.';
			}
			else
			{
				// prepare the table 
				echo '<table border="1"> 
<tr> 
<th>Topic</th> 
<th>Created at</th> 
</tr>';	

				while($row = mysqli_fetch_assoc($result))
				{				
					echo '<tr>';
						echo '<td class="leftpart">';
							echo '<h3><a href="topic.php?id=' . $row['topic_id'] . '">' . $row['topic_subject'] . '</a><h3>';
						echo '</td>';
						echo '<td class="rightpart">';
							echo date('d-m-Y', strtotime($row['topic_date']));
						echo '</td>';
					echo '</tr>';
				}
			}
		}
	}
}

include 'footer.php';
?>

И вот окончательный результат работы нашей страницы категорий:

Topics View

Шаг 11: Просмотр темы

SQL-запросы в этот шаг является сложным. Часть PHP похожа на то, что вы видели раньше. Давайте взглянем на запросы. Первый из них извлекает основную информацию по теме:

SELECT
    topic_id,
    topic_subject
FROM
    topics
WHERE
    topics.topic_id = '" . mysqli_real_escape_string($_GET['id']) . "'";

Эта информация отображается в заголовке таблицы, которую мы будем использовать для отображения всех данных. Далее мы извлекаем все сообщения в этой теме из базы данных. Следующий запрос дает нам именно то, что нам нужно:

SELECT
    posts.post_topic,
    posts.post_content,
    posts.post_date,
    posts.post_by,
    users.user_id,
    users.user_name
FROM
    posts
LEFT JOIN
    users
ON
    posts.post_by = users.user_id
WHERE
    posts.post_topic = '" . mysqli_real_escape_string($_GET['id']) . "'";

На этот раз нам нужна информация от пользователей и таблицу posts, поэтому мы снова используем ЛЕВОЕ СОЕДИНЕНИЕ. Условие таково: идентификатор пользователя должен совпадать с полем post_by. Таким образом, мы можем показать имя пользователя, который ответил на каждое сообщение.

Окончательный вид темы выглядит следующим образом:

Detail View

Шаг 12: Добавление ответа

Давайте создадим последнюю недостающую часть этого форума - возможность добавить ответ. Мы начнем с создания формы:

<form method="post" action="reply.php?id=5">
    <textarea name="reply-content"></textarea>
    <input type="submit" value="Submit reply" />
</form>

Reply Form

Полный reply. php код выглядит следующим образом.

<?php
//create_cat.php 
include 'connect.php';
include 'header.php';

if($_SERVER['REQUEST_METHOD'] != 'POST')
{
    //someone is calling the file directly, which we don't want 
    echo 'This file cannot be called directly.';
}
else
{
	//check for sign in status 
	if(!$_SESSION['signed_in'])
	{
		echo 'You must be signed in to post a reply.';
	}
	else
	{
		//a real user posted a real reply 
		$sql = "INSERT INTO 
posts(post_content, 
post_date, 
post_topic, 
post_by) 
VALUES (?, NOW(), ?, ?)";

		$stmt = mysqli_prepare($conn, $sql);
		mysqli_stmt_bind_param($stmt, "sii", $_POST['reply-content'], $_GET['id'], $_SESSION['user_id']);
		mysqli_stmt_execute($stmt);

		if(mysqli_stmt_errno($stmt))
		{
			echo 'Your reply has not been saved, please try again later.';
		}
		else
		{
			echo 'Your reply has been saved, check out <a href="topic.php?id=' . htmlentities($_GET['id']) . '">the topic</a>.';
		}
	}
}

include 'footer.php';
?>

Комментарии в коде в значительной степени объясняют, что происходит. Мы проверяем наличие реального пользователя, а затем вставляем запись в базу данных.

Reply Made

Заключение

Теперь после прочтения данной статьи вы должны гораздо лучше понимать, что требуется для создания форума. Еще раз спасибо за чтение.