Как написать сайт форум

Время на прочтение
27 мин

Количество просмотров 37K

Не так давно я рассказывал о геме Oxymoron, позволяющем очень просто и быстро строить современные Single Page Application на AngularJS и Ruby on Rails. Статья была встречена весьма позитивно, поэтому пришло время написать более-менее сложное приложение, чтобы показать все возможности гема.

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

Репозиторий с полным исходным кодом
Развернутое приложение

Задача

Написать форум обладающий следующей функциональностью:

  • Пользователи должны иметь возможность зарегистрироваться и авторизоваться в системе
  • Пользователи имеют роли. На данный момент в системе предусмотрены 2 роли: администратор и модератор
  • Администратор может создавать группы, наполнять эти группы темами
  • Пользователи могут создавать топики в существующих темах и писать в эти топики свои посты
  • Модератор может удалять сообщения и топики пользователей
  • Модератор и администратор могут блокировать нерадивых пользователей
  • Каждый пользователь имеет свой рейтинг, определяемый другими участниками
  • Пользователи должны иметь возможность загрузить аватарку
  • На форуме должен быть предусмотрен поиск

Выбранные технологии

В качестве базы данных я буду использовать PostgreSQL, так как мне необходима возможность хранения массивов и хэшей. Для поиска будет использоваться поисковой движок Sphinx. Процессинг изображений на сервере по старой доброй традиции будет идти через ImageMagick.
В данной статье я не буду использовать вспомогательные кеширующие инструменты и постараюсь обойтись только возможностями Rails и Postgresql.

Используемые гемы

gem 'active_model_serializers', '0.9.4'
gem 'pg'
gem 'slim'
gem 'slim-rails'
gem 'devise'
gem 'gon'
gem 'carrierwave'
gem 'mysql2',          '~> 0.3.18', :platform => :ruby
gem 'thinking-sphinx', '~> 3.1.4'
gem 'mini_magick'
gem "oxymoron"
gem 'kaminari'
gem 'oj'
gem 'file_validators'

Описание базы данных

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

Создадим соответствующие модели и миграции к ним:

rails g model Group
rails g model Theme
rails g model Topic
rails g model Post

Генерацию модели пользователей выполним с помощью гема Devise:

rails g devise:install
rails g devise User

Содержимое миграций:

create_groups.rb

class CreateGroups < ActiveRecord::Migration
  def change
    create_table :groups do |t|
      t.string :title

      t.timestamps null: false
    end
  end
end

create_themes.rb

class CreateThemes < ActiveRecord::Migration
  def change
    create_table :themes do |t|
      t.string :title
      
      t.integer :group_id
      t.index :group_id

      t.integer :posts_count, default: 0
      t.integer :topics_count, default: 0

      t.json :last_post

      t.timestamps null: false
    end
  end
end

create_topics.rb

class CreateTopics < ActiveRecord::Migration
  def change
    create_table :topics do |t|
      t.string :title
      t.text :content
      
      t.integer :user_id
      t.index :user_id

      t.integer :group_id
      t.index :group_id

      t.integer :theme_id
      t.index :theme_id

      t.json :last_post
      t.integer :posts_count, default: 0

      t.timestamps null: false
    end
  end
end

create_posts.rb

class CreatePosts < ActiveRecord::Migration
  def change
    create_table :posts do |t|
      t.string :title
      t.text :content

      t.integer :user_id
      t.index :user_id
      
      t.integer :topic_id
      t.index :topic_id

      t.integer :theme_id
      t.index :theme_id

      t.boolean :delta, default: true

      t.timestamps null: false
    end
  end
end

create_users.rb

class DeviseCreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

      t.string :name
      t.boolean :banned, default: false
      t.integer :avatar_id
      t.string :avatar_url, default: "/default_avatar.png"

      t.integer :rating, default: 0
      t.integer :votes, array: true, default: []
      t.integer :posts_count, default: 0
      t.integer :topics_count, default: 0

      t.string :role

      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

Для решения вопросов загрузки файлов на сервер я всегда создаю отдельную модель и на нее ставлю uploader. В данном случае это модель Avatar:

rails g model avatar

create_avatar.rb

class CreateAvatars < ActiveRecord::Migration
  def change
    create_table :avatars do |t|
      t.string :body
      t.timestamps null: false
    end
  end
end

Организация моделей

Укажем все необходимые связи и валидации для наших моделей:

models/group.rb

class Group < ActiveRecord::Base
  has_many :themes, ->{order(:id)}, dependent: :destroy
  has_many :topics, through: :themes, dependent: :destroy
  has_many :posts, through: :topics, dependent: :destroy
end

models/theme.rb

class Theme < ActiveRecord::Base
  has_many :topics, dependent: :destroy
  has_many :posts, dependent: :destroy
  belongs_to :group
end

models/topic.rb

class Topic < ActiveRecord::Base
  has_many :posts, dependent: :destroy
  belongs_to :theme, :counter_cache => true
  belongs_to :user, :counter_cache => true

  validates_presence_of :theme, :title, :content
end

models/post.rb

class Post < ActiveRecord::Base
  belongs_to :topic, :counter_cache => true
  belongs_to :theme, :counter_cache => true
  belongs_to :user, :counter_cache => true

  validates :content, presence: true, length: { in: 2..300 }
end

models/user.rb

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  belongs_to :avatar
  has_many :posts
  has_many :topics
  validates :name, :uniqueness => {:case_sensitive => false}, presence: true, length: { in: 2..10 }
end

models/avatar.rb

class Avatar < ActiveRecord::Base
  belongs_to :user
end

Для моделей Topic и Theme необходимо устанавливать в поле last_post последний созданный пост. Сделать это лучше всего в каллбэке after_create модели Post:

models/post.rb

class Post < ActiveRecord::Base
  belongs_to :topic, :counter_cache => true
  belongs_to :theme, :counter_cache => true
  belongs_to :user, :counter_cache => true

  validates :content, presence: true, length: { in: 2..300 }

  after_create :set_last_post

  private
    def set_last_post
      last_post = self.as_json(include: [:topic, :user])
      topic.update(last_post: last_post)
      theme.update(last_post: last_post)
    end
end

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

models/topic.rb

class Topic < ActiveRecord::Base
  has_many :posts, dependent: :destroy
  belongs_to :theme, :counter_cache => true
  belongs_to :user

  validates_presence_of :theme, :title, :content

  after_create :create_post
  
  private
    def create_post
      self.posts.create self.attributes.slice("title", "content", "user_id", "theme_id")
    end
end

Приступим к модели Avatar. Первым делом сгенерируем uploader, который будет использоваться для обработки загружаемых аватарок. Я использую carrierwave:

rails g uploader Avatar

Укажем нашему аплоадеру, что он должен сжимать все загружаемые картинки до версии thumb(150х150рх), и делать это он будет через MiniMagick (враппер для ImageMagick):

uploaders/avatar_uploader.rb

class AvatarUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick  
  storage :file

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  version :thumb do
    process :resize_to_fill => [150, 150]
  end

  def extension_white_list
    %w(jpg jpeg gif png)
  end
end

Теперь подключим AvatarUploader к модели Avatar и укажем, что размер загружаемого файла должен быть не более 2 МБайт:

models/avatar.rb

class Avatar < ActiveRecord::Base
  mount_uploader :body, AvatarUploader
  belongs_to :user

  validates :body, file_size: { less_than: 2.megabytes },
                   file_content_type: { allow: ['image/jpg', 'image/jpeg', 'image/png', 'image/gif'] }
end

Загрузка файлов на сервер

В Oxymoron есть директива fileupload. Для того, чтобы отправить файл на сервер, необходимо ее определить на теге input[type=«file»]

input type="file" fileupload="'/uploads/avatar'" ng-model="result_from_server" percent-completed="percent"

В ответ ожидается массив. При необходимости можно указать аттрибут multiple. Если multiple не указан, то в переменную result_from_server будет положен первый элемент массива, в ином случае – весь массив.
Сгенерируем UploadsController, отвечающий за загрузку файлов на сервер:

rails g controller uploads

Создадим метод avatar, который будет управлять логикой загрузки аватарки:

class UploadsController < ApplicationController
  before_action :authenticate_user!

  def avatar
    avatar = Avatar.new(body: params[:attachments].first)

    if avatar.save
      avatar_url = avatar.body.thumb.url
      current_user.update(avatar_id: avatar.id, avatar_url: avatar_url)
      render json: Oj.dump([avatar_url])
    else
      render json: {msg: avatar.errors.full_messages.join(", ")}
    end
  end
end

Поиск по постам

Для полнотекстового поиска я использую Sphinx и гем thinking_sphinx. Первым делом необходимо создать файл конфига для thinking_sphinx, который будет транслирован в sphinx.conf. Итак, нам нужен стиминговый поиск с возможностью поиска по звёздочке(автокомплит) и минимальным запросом в 3 символа. Опишем это в thinking_sphinx.yml:

config/thinking_sphinx.yml

development: &generic
  mem_limit: 256M
  enable_star: 1
  expand_keywords: 1
  index_exact_words: 1
  min_infix_len: 3
  min_word_len: 3
  morphology: stem_enru
  charset_type: utf-8
  max_matches: 100000
  per_page: 100000
  utf8: true
  mysql41: 9421
  charset_table: "0..9, A..Z->a..z, _, a..z, 
    U+410..U+42F->U+430..U+44F, U+430..U+44F, U+401->U+0435, U+451->U+0435"

staging:
  <<: *generic
  mysql41: 9419

production:
  <<: *generic
  mysql41: 9450

test:
  <<: *generic
  mysql41: 9418
  quiet_deltas: true

Теперь создадим индекс для постов. Индексироваться должны заголовок и контент. Результат будем сортировать в обратном порядке от даты создания, поэтому ее необходимо указать в виде фильтра:

app/indices/post_index.rb

ThinkingSphinx::Index.define :post, {delta: true} do
  indexes title
  indexes content
  has created_at
end

Выполняем генерацию sphinx-конфига и запускаем демон searchd одной командой:

rake ts:rebuild

Если ребилд пройдет успешно, то вы увидите в консоли сообщение о том, что демон стартовал удачно.

Добавим в модель Post метод для поиска. Так как метод search занял thinking_sphinx, я использовал look_for:

def self.look_for query
  return self if query.blank? or query.length < 3
  search_ids = self.search_for_ids(query, {per_page: 1000, order: 'created_at DESC'})
  self.where(id: search_ids)
end

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

rails g controller search index

Данный метод мы определим позже.

Капча reCAPTCHA

Дабы не отставать от моды, подключим в свое приложение новую reCAPTCHA. После регистрации, вам будет доступно 2 ключа: публичный и приватный. Оба этих ключа мы положим в secrets.yml. Туда же будем складировать все возможные api-key нашего приложения.

config/secrets.yml

apikeys: &generic
  recaptcha:
    public_key: your_recaptcha_public_key
    secret_key: your_recaptcha_secret_key

# generate your_secret_key_base by `rake secret`
development:
  <<: *generic
  secret_key_base: your_secret_key_base

test:
  <<: *generic
  secret_key_base: your_secret_key_base

production:
  <<: *generic
  secret_key_base: your_secret_key_base

Напишем protected метод в ApplicationContoller, который верифицирует капчу

protected
    def verify_captcha response
      result = RestClient.post(
                  "https://www.google.com/recaptcha/api/siteverify",
                  secret: Rails.application.secrets[:recaptcha]["secret_key"],
                  response: response)

      JSON.parse(result)["success"]
    end

Теперь этот метод доступен у всех контроллеров, унаследованных от ApplicationController.

Авторизация и регистрация

У нас чистое SPA-приложение. Страницу мы не перезагружаем даже при логине/разлогине. Создадим контроллеры для управления сессией и регистрацией на основе JSON API:

controllers/auth/sessions_controller.rb

class Auth::SessionsController < Devise::SessionsController
  skip_before_action :authenticate_user!
  after_filter :set_csrf_headers, only: [:create, :destroy]

  def create
    if verify_captcha(params[:user][:recaptcha])
      self.resource = warden.authenticate(auth_options)
      if self.resource
        sign_in(resource_name, self.resource)
        render json: {msg: "Вы успешно авторизовались в системе", current_user: current_user.public_fields}
      else
        render json: {msg: "Email не найден, либо пароль неверен"}, status: 401
      end
    else
      render json: {msg: "Проверка каптчи не пройдена"}, status: 422
    end
  end

  def destroy
    Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
    render json: {msg: "Вы успешно вышли"}
  end

  protected
  def set_csrf_headers
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?  
  end
end

controllers/auth/registrations_controller.rb

class Auth::RegistrationsController < Devise::RegistrationsController
  skip_before_action :authenticate_user!
  def create
    if verify_captcha(params[:user][:recaptcha])
      build_resource(sign_up_params)
      resource.save

      unless resource.persisted?
        render json: {
          msg: resource.errors.full_messages.first,
          errors: resource.errors,
        }, status: 403
      else
        sign_up(resource_name, resource)
        
        render json: {
          msg: "Вы успешно зарегистрировались!",
          current_user: current_user.public_fields
        }
      end
    else
      render json: {msg: "Проверка каптчи не пройдена"}, status: 422
    end
  end
  private
    def sign_up_params
      params.require(:user).permit(:name, :email, :password)
    end
end

Здесь комментарии излишни. Разве что стоит обратить внимание на set_csrf_headers. Так как страница у нас не обновляется, нам необходимо получать «свежие» CSRF-токены с сервера, чтобы не быть уязвимыми к CSRF-атакам. Для ActionController это делает автоматически Oxymoron. Для всех остальных контроллеров, работающих в обход ActionController необходимо устанавливать в cookies[‘XSRF-TOKEN’] актуальное значение CSRF-токена.

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

before_action :authenticate_user!, only: [:create, :update, :destroy, :new, :edit]

private
    def authenticate_user!
      unless current_user
        if request.xhr?
          render json: {msg: "Вы не авторизованы"}, status: 403            
        else
          redirect_to root_path
        end
      end
    end

Роутинг приложения

Сразу опишем файл routes.rb, чтобы больше к нему не возвращаться. Итак, у нас есть 5 ресурсов: users, groups, themes, topics и posts. Так же имеются роуты /uploads/avatar и /search. Помимо этого, нам необходимы методы на ресурсе users для определения онлайна пользователя, получения его рейтинга и прочей статистики.

Rails.application.routes.draw do
  root to: 'groups#index'

  devise_for :users, controllers: {
    sessions: 'auth/sessions',
    registrations: 'auth/registrations',
  }

  post "uploads/avatar" => "uploads#avatar"

  get "search" => "search#index"

  resources :groups
  resources :themes
  resources :topics
  resources :posts
  resources :users, only: [:index, :show] do
    collection do
      get "touch"  # touch для current_user, чтобы обновить время онлайна
      get "metrics" # разнообразная статистика
    end
    member do
      put "rate" # Изменение рейтинга
      put "ban" # Забанить
      put "unban" # Разбанить
    end
  end
end

Сериализация

Мне нравится философия сериализиции ActiveModelSerializer, но я очень стеснен в серверных мощностях, особенно перед Хабраэффектом. Поэтому пришлось придумать механизм максимально быстрой сериализации, которая только возможна в рамках текущего проекта. Основной критерий, предъявляемый мной перед сериализацией состоит в том, что она не должна занимать больше 5-10 мс.

Все что Вы прочитаете дальше, может показаться вам чуждым, странным и неправильным

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

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

javascripts/serializers/interceptor.js

app.factory('serializerInterceptor', ['$q', function ($q) {
  return {
    response: function (response) {
      // Если в ответе найден сериалайзер, то
      if (response.data.serializer) {
        // Находим его в нашей глобальной области видимости
        var serializer = window[response.data.serializer];
        
        // Если он найден, то
        if (serializer) {
          // применяем его
          var collection = serializer(response.data.collection);
         
          // если результат ожидается как массив, то кладем его в поле collection, иначе в resource
          if (response.data.single) {
            response.data.resource = collection[0]
          } else {
            response.data.collection = collection;
          }
        } else {
          console.error(response.data.serializer + " is not defined")
        }
      }
      // Возвращаем измененный ответ с сервера
      return response || $q.when(response);
    }
  };
}])
// Кладем serializerInterceptor в стек перехватчиков для http-запросов, выполненных посредством Angular 
.config(['$httpProvider', function ($httpProvider) {
  $httpProvider.interceptors.push('serializerInterceptor');
}])

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

collection = Post.joins(:user).pluck("posts.id", "posts.title", "posts.content", "users.id", "users.name")
render json: {
  collection: collection,
  serializer: "ExampleSerializer"
}

Результатом будет таблица с соответствующими колонками, или в JSON-представлении – это массив, состоящий из массивов. Напишем сериалайзер:

Пример сериалайзера

function ExampleSerializer (collection) {
  var result = [];

  collection.forEach(function(item) {
    id: item[0],
    title: item[1],
    content: item[2],
    user: {
      id: item[3],
      name: item[4]
    }
  })

  return result
}

Данный сериалайзер будет автоматически применён к collection и в response любого $http-запроса мы увидим уже сериализованный результат.
Теперь необходимо для каждой модели создать метод pluck_fields, который возвращает поля для селекта:

models/group.rb

class Group < ActiveRecord::Base
  has_many :themes, ->{order(:id)}, dependent: :destroy
  has_many :topics, through: :themes, dependent: :destroy
  has_many :posts, through: :topics, dependent: :destroy

  def self.pluck_fields
    ["groups.id", "groups.title", "themes.id", "themes.title",
    "themes.posts_count", "themes.topics_count", "themes.last_post"]
  end
end

models/post.rb

class Post < ActiveRecord::Base
  belongs_to :topic, :counter_cache => true
  belongs_to :theme, :counter_cache => true
  belongs_to :user, :counter_cache => true

  after_create :set_last_post

  validates :content, presence: true, length: { in: 2..300 }

  def self.pluck_fields
    ["posts.id", "posts.title", "posts.content", "users.id", "users.created_at", "users.name",
     "users.rating", "users.posts_count", "users.avatar_url", "topics.id", "topics.title"]
  end

  def self.look_for query
    return self if query.blank? or query.length < 3
    search_ids = self.search_for_ids(query, {per_page: 1000000, order: 'created_at DESC'})
    self.where(id: search_ids)
  end

  private
    def set_last_post
      last_post = self.as_json(include: [:topic, :user])
      topic.update(last_post: last_post)
      theme.update(last_post: last_post)
    end
end

models/theme.rb

class Theme < ActiveRecord::Base
  has_many :topics, dependent: :destroy
  has_many :posts, dependent: :destroy
  belongs_to :group

  def self.pluck_fields
    [:id, :title]
  end
end

models/topic.rb

class Topic < ActiveRecord::Base
  has_many :posts, dependent: :destroy
  belongs_to :theme, :counter_cache => true
  belongs_to :user

  validates_presence_of :theme, :title, :content

  after_create do
    Post.create(title: title, content: content, user_id: user_id, theme_id: theme_id, topic_id: id)
  end

  def self.pluck_fields
    ["topics.id", "topics.title", "topics.last_post", "topics.posts_count",
     "users.id", "users.name", "themes.id", "themes.title"]
  end
end

models/user.rb

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  belongs_to :avatar
  has_many :posts
  validates :name, :uniqueness => {:case_sensitive => false}, presence: true, length: { in: 2..10 }

  def self.pluck_fields
    [:id, :created_at, :updated_at, :name, :avatar_url, :posts_count, :rating, :banned]
  end

  def public_fields
    self.attributes.slice("id", "email", "rating", "name", "created_at", "updated_at", "posts_count", "banned")
  end
end

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

Контроллеры

В своей предыдущей статье «Архитектура построения Single Page Application на основе AngularJS и Ruby on Rails» я приводил пример «типичного Rails-контроллера». Типичный – значит нам нет необходимости описывать каждый раз одну и ту же логику. Достаточно написать наиболее общий контроллер, унаследоваться от него и переопределить, либо доопределить необходимые методы. Писать такой контроллер я не стал и просто вынес всю общую логику в concern.
Итоговый concern выглядит очень необычно:

controllers/concern/spa.rb

module Spa
  extend ActiveSupport::Concern
  # @model – модель (со всем чейнингом), к которой идет обращение
  # @resource – текущий ресурс
  # Все методы имеют свое дефолтное состояние и переопределяются при необходимости
  included do
    before_action :set_model
    before_action :set_resource, only: [:show, :edit, :update, :destroy]

    def index
      respond_to do |format|
        format.html
        format.json {
          collection = @model.where(filter_params) if params[:filter]
          
          render json: Oj.dump({
            total_count: collection.count,
            serializer: serializer,
            collection: collection.page(params[:page]).per(10).pluck(*pluck_fields),
            page: params[:page] || 1
          })
        }
      end
    end

    def show
      respond_to do |format|
        format.html
        format.json {
          @resource = @model.where(id: params[:id]).pluck(*pluck_fields)
          
          render json: Oj.dump({
            collection: @resource,
            serializer: serializer,
            single: true
          })
        }
      end
    end

    def new
      new_params = resource_params rescue {}
      @resource = @model.new(new_params)
      authorize @resource, :create?

      respond_to do |format|
        format.html
        format.json {
          render json: Oj.dump(@resource)
        }
      end
    end

    def edit
      authorize @resource, :update?
      respond_to do |format|
        format.html
        format.json {
          render json: Oj.dump(@resource)
        }
      end
    end

    def create
      @resource = @model.new resource_params
      authorize @resource

      if @resource.save
        @collection = @model.where(id: @resource.id).pluck(*pluck_fields)
        result = {
          collection: @collection,
          serializer: serializer,
          single: true,
        }.merge(redirect_options[:update] || {})
        
        render json: Oj.dump(result)
      else
        render json: {errors: @resource.errors, msg: @resource.errors.full_messages.join(', ')}, status: 422
      end
    end

    def update
      authorize @resource
      if @resource.update(resource_params)
        render json: {resource: @resource, msg: "#{@model.name} успешно обновлен"}.merge(redirect_options[:update] || {})
      else
        render json: {errors: @resource.errors, msg: @resource.errors.full_messages.join(', ')}, status: 422
      end
    end

    def destroy
      authorize @resource
      @resource.destroy
      render json: {msg: "#{@model.name} успешно удален"}
    end

    private
      def set_resource
        @resource = @model.find(params[:id])
      end

      def pluck_fields
        @model.pluck_fields
      end

      def redirect_options
        {}
      end
      
      def filter_params
        params.require(:filter).permit(filter_fields)
      end

      def serializer
        serializer = "#{@model.model_name}Serializer"
      end
  end
end

Итак, создадим на его основе PostsController:

class PostsController < ApplicationController
  include Spa

  private
    # Устанавливаем модель для консёрна
    def set_model
      @model = Post.joins(:user, :topic).order(:created_at)
    end
    
    # Указываем поля, по которым можно производить фильтрацию 
    def filter_fields
      [:theme_id, :topic_id]
    end

    # Определяем поля, которые допустимы при сабмите формы
    def resource_params
      # Топик нам нужен для того, чтобы устанавливать его заголовок, как дефолтный заголовок для постов
      topic = Topic.find(params[:post][:topic_id])
      title = params[:post][:title]

      params.require(:post).permit(:content, :title, :topic_id)
      .merge({
        theme_id: topic.theme_id,
        user_id: current_user.id,
        title: title.present? ? title : "Re: #{topic.title}"
      })
    end
end

Это и есть весь код контроллера, которым он отличается от Spa. Аналогично создадим остальные контроллеры:

controllers/groups_controller.rb

class GroupsController < ApplicationController
  include Spa

  private
    def set_model
      @model = Group.joins("LEFT JOIN themes ON themes.group_id = groups.id").order("groups.id")
    end

    def redirect_options
      {
        create: {
          redirect_to_url: root_path
        },
        update: {
          redirect_to_url: root_path
        }
      }
    end

    def resource_params
      params.require(:group).permit(:title)
    end
end

controllers/themes_controller.rb

class ThemesController < ApplicationController
  include Spa

  private
    def set_model
      @model = Theme.order(:created_at)
    end

    def redirect_options
      {
        create: {
          redirect_to_url: root_path
        },
        update: {
          redirect_to_url: root_path
        }
      }
    end

    def resource_params
      params.require(:theme).permit(:title, :group_id)
    end
end

controllers/topics_controller.rb

class TopicsController < ApplicationController
  include Spa

  private
    def set_model
      @model = Topic.joins(:theme, :user).order("topics.updated_at DESC")
    end

    def filter_fields
      [:theme_id]
    end

    def redirect_options
      {
        create: {
          redirect_to_url: topic_path(@resource)
        },
        update: {
          redirect_to_url: topic_path(@resource)
        }
      }
    end

    def resource_params
      params.require(:topic).permit(:title, :content, :theme_id)
      .merge({
        user_id: current_user.id
      })
    end
end

controllers/users_controller.rb

class UsersController < ApplicationController
  include Spa

  def touch
    current_user.touch if current_user
    render json: {}
  end

  def rate
    if current_user.votes.include?(params[:id].to_i)
      return render json: {msg: "Вы уже влияли на репутацию пользователя"}, status: 422
    end

    current_user.votes.push(params[:id].to_i)
    current_user.save

    set_resource
    if params[:positive]
      @resource.increment!(:rating)
    else
      @resource.decrement!(:rating)
    end

    render json: {rating: @resource.rating}
  end

  def metrics
    result = current_user.attributes.slice("posts_count", "rating") if current_user
    render json: result || {}
  end

  def ban
    authorize @resource
    @resource.update(banned: true)
    render json: {msg: "Пользователь был забанен"}
  end

  def unban
    authorize @resource, :ban?
    @resource.update(banned: false)
    render json: {msg: "Пользователь был разбанен"}
  end

  private
    def set_model
      @model = User
    end
end

Контроллер SearchController не использует concern Spa, поэтому его опишем полностью:

SearchController

class SearchController < ApplicationController
  def index
    respond_to do |format|
      format.html
      format.json {
        collection = Post.look_for(params[:q]).joins(:user, :topic).order("created_at DESC")
        render json: Oj.dump({
          total_count: collection.count,
          serializer: "PostSerializer",
          collection: collection.page(params[:page]).per(10).pluck(*Post.pluck_fields),
          page: params[:page] || 1
        })
      }
    end
  end
end

Разграничение прав доступа

Я сознательно не использовал Rolify для организации ролей пользователей, посколько в данном случае это не оправдано. На форуме нет комбинированных ролей. Все управление идет через поле role. Для разграничения прав доступа я использую Pundit. В описании гема есть вся информация по его использованию.
Напишем все policies для нашего приложения исходя из требований:

app/policies/group_policy.rb

class GroupPolicy
  def initialize(user, group)
    @user = user
    @group = group
  end

  def create?
    @user.role == "admin"
  end

  def update?
    @user.role == "admin"
  end

  def destroy?
    @user.role == "admin"
  end
end

app/policies/post_policy.rb

class PostPolicy
  def initialize(user, post)
    @user = user
    @post = post
  end

  def create?
    true
  end

  def update?
    ["admin", "moderator"].include?(@user.role) || @user.id == @post.user_id
  end

  def destroy?
    ["admin", "moderator"].include?(@user.role) || @user.id == @post.user_id
  end
end

app/policies/theme_policy.rb

class ThemePolicy
  def initialize(user, theme)
    @user = user
    @theme = theme
  end

  def create?
    @user.role == "admin"
  end

  def update?
    @user.role == "admin"
  end

  def destroy?
    @user.role == "admin"
  end
end

app/policies/topic_policy.rb

class TopicPolicy
  def initialize(user, topic)
    @user = user
    @topic = topic
  end

  def create?
    true
  end

  def update?
    @user.id == @topic.user_id || ["admin", "moderator"].include?(@user.role)
  end

  def destroy?
    @user.id == @topic.user_id || ["admin", "moderator"].include?(@user.role)
  end
end

app/policies/user_policy.rb

class UserPolicy
  def initialize(user, resource)
    @user = user
    @resource = resource
  end

  def ban?
    ["admin", "moderator"].include? @user.role
  end
end

По умолчанию Pundit кидает исключение Pundit::NotAuthorizedError, если проверка не пройдена, поэтому нам необходимо настроить его на работу посредством JSON API. Для этого в ApplicationController обработаем это исключение:

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private
    def user_not_authorized
      if request.xhr?
        render json: {msg: "Нет прав на данное действие"}, status: 403            
      else
        redirect_to root_path
      end
    end

Перед тем, как перейти к клиентской части, давайте закончим с ApplicationController, передав текущего пользователя в Gon, чтобы сразу после загрузки страницы у нас сразу была вся необходимая информация о нём:

controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Pundit
  protect_from_forgery with: :exception
  
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
  before_action :authenticate_user!, only: [:create, :update, :destroy, :new, :edit]
  
  # Выключаем лейаут для всех ajax-запросов
  layout proc {
    if request.xhr?
      false
    else
      set_gon
      "application"
    end
  }

  protected
    def verify_captcha response
      result = RestClient.post("https://www.google.com/recaptcha/api/siteverify",
                               secret: Rails.application.secrets[:recaptcha]["secret_key"],
                               response: response)
      JSON.parse(result)["success"]
    end

  private
    def set_gon
      gon.current_user = current_user.public_fields if current_user
    end

    def authenticate_user!
      unless current_user
        if request.xhr?
          render json: {msg: "Вы не авторизованы"}, status: 403            
        else
          redirect_to root_path
        end
      end
    end

    def user_not_authorized
      if request.xhr?
        render json: {msg: "Нет прав на данное действие"}, status: 403            
      else
        redirect_to root_path
      end
    end
end

Клиентская часть

Я не буду описывать шаги по подключению Oxymoron, так как это всё уже было в этой статье.

AngularJS-контроллеры так же являются однотипными и рассматривались в той самой статье. Опишем их:

javascripts/controllers/groups_ctrl.js

app.controller('GroupsCtrl', ['$scope', 'Group', 'action', 'Theme', function ($scope, Group, action, Theme) {
  var ctrl = this;

  action('index', function () {
    ctrl.groups = Group.get();

    ctrl.destroy_theme = function (theme) {
      if (confirm("Вы уверены?"))
        Theme.destroy({id: theme.id})
    }

    ctrl.destroy_group = function (group) {
      if (confirm("Вы уверены?"))
        Group.destroy({id: group.id})
    }
  })

  action('new', function () {
    ctrl.group = Group.new();
    ctrl.save = Group.create;
  })

  action('edit', function (params) {
    ctrl.group = Group.edit(params);
    ctrl.save = Group.update;
  })
}])

javascripts/controllers/themes_ctrl.js

app.controller('ThemesCtrl', ['$scope', 'Theme', 'Topic', 'action', '$location', function ($scope, Theme, Topic, action, $location) {
  var ctrl = this;

  action('show', function (params) {
    var filter = {
      theme_id: params.id
    }

    ctrl.theme = Theme.get(params);
    
    ctrl.query = function (page) {
      Topic.get({
        filter: filter,
        page: page
      }, function (res) {
        ctrl.topics = res;
      });
    }

    ctrl.query($location.search().page || 1)

    ctrl.destroy = function (topic) {
      if (confirm("Вы уверены?"))
        Topic.destroy({id: topic.id})
    }
  })

  action('new', function () {
    Theme.new(function (res) {
      ctrl.theme = res;
      ctrl.theme.group_id = $location.search().group_id;
    });
    ctrl.save = Theme.create;
  })

  action('edit', function (params) {
    ctrl.theme = Theme.edit(params);
    ctrl.save = Theme.update;
  })
}])

javascripts/controllers/topics_ctrl.js

app.controller('TopicsCtrl', ['$scope', '$location', 'Topic', 'action', 'Post', 'Theme', function ($scope, $location, Topic, action, Post, Theme) {
  var ctrl = this;

  action('show', function (params) {
    var filter = {
      topic_id: params.id
    }

    ctrl.post = {
      topic_id: params.id
    }

    ctrl.topic = Topic.get(params);
     
    ctrl.query = function (page, callback) {
      Post.get({filter: filter, page: page}, function (res) {
        ctrl.posts = res;
        if (callback) callback();
      });
    }

    ctrl.query(1)

    ctrl.send = function () {
      Post.create({post: ctrl.post}, function (res) {
        ctrl.post = {
          topic_id: params.id
        }
        ctrl.query(Math.ceil(ctrl.posts.total_count/10))
      })
    }
  })

  action('new', function () {
    var theme_id = $location.search().theme_id;
    ctrl.theme = Theme.get({id: theme_id});
    ctrl.topic = Topic.new({topic: {theme_id: theme_id}});
    ctrl.save = Topic.create;
  })

  action('edit', function (params) {
    ctrl.topic = Topic.edit(params, function (res) {
      ctrl.theme = Theme.get({id: res.theme_id});
    });
    ctrl.save = Topic.update;
  })
}])

javascripts/controllers/users_ctrl.js

app.controller('UsersCtrl', ['$scope', 'User', 'action', function ($scope, User, action) {
  var ctrl = this;

  action('index', function () {
    ctrl.query = function (page) {
      User.get({page: page}, function (res) {
        ctrl.users = res;
      });
    }

    ctrl.query(1)
  })

  action('show', function (params) {
    ctrl.user = User.get(params);
  })

  ctrl.ban = function (user) {
    User.ban({id: user.id})
    user.banned = true;
  }

  ctrl.unban = function (user) {
    User.unban({id: user.id})
    user.banned = false;
  }
}])

javascripts/controllers/search_ctrl.js

app.controller('SearchCtrl', ['$scope', '$location', '$http', function ($scope, $location, $http) {
  var ctrl = this;

  ctrl.query = function (page) {
    var params = {
      page: page || 1
    }

    if (ctrl.q) {
      params.q = ctrl.q
    }

    $http.get(Routes.search_path(params)).then(function (res) {
      ctrl.posts = res.data;
    })
  }

  $scope.$watch(function () {
    return $location.search().q
  }, function (q) {
    ctrl.q = q;
    ctrl.query()
  })
}])

javascripts/controllers/sign_ctrl.js

app.controller('SignCtrl', ['$scope', '$http', '$interval', 'User', function ($scope, $http, $interval, User) {
  var ctrl = this;

  ctrl.title = {
    in: "Вход",
    up: "Регистрация"
  }

  ctrl.sign = {
    in: function () {
      $http.post(Routes.user_session_path(), {user: ctrl.user})
        .success(function (res) {
          gon.current_user = res.current_user;
        })
    },
    out: function () {
      $http.delete(Routes.destroy_user_session_path())
        .success(function () {
          gon.current_user = undefined;
        })
    },
    up: function () {
      $http.post(Routes.user_registration_path(), {user: ctrl.user})
        .success(function (res) {
          gon.current_user = res.current_user;
        })
    }
  }

  $scope.$watch(function () {
    return gon.current_user
  }, function (current_user) {
    if (current_user) {
      ctrl.method = 'user';
      ctrl.title.user = current_user.name;
    } else {
      ctrl.method = 'in';
    }
  })

  $interval(function () {
    User.metrics(function (res) {
      angular.extend(gon.current_user, res);
    })
  }, 10000)
}])

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

В app/views/components есть render.html.slim со следующим содержимым:

- Dir[File.dirname(__FILE__) + '/_*.html*'].each do |partial|
  script type="text/ng-template" id="#{File.basename(partial).gsub('.slim', '').gsub(/^_/, '')}"
    = render file: partial

Данный шаблон рендерит внутри себя все паршалы, лежащие в директории components и оборачивает их в тег шаблонов для AngularJS. Его необходимо отредерить в основном лейауте. Это автоматизирует работу с директивами.

= render template: "components/render"

application/layout.html.slim

html ng-app="app"
  head
    title Форум
    base href="/"
    = stylesheet_link_tag 'application'
  body ng-controller="MainCtrl as main" ng-class="gon.current_user.role"
    .layout.body
      .search
        input.form-control placeholder="Поиск" type="text" ng-model="main.search" ng-model-options="{debounce: 300}"
      .bredcrumbs ng-yield="bredcrumbs"
      .wrapper
        .content
          ui-view
        .sidebar
          = render "layouts/sidebar"
                  
    = render template: "components/render"
    = Gon::Base.render_data
    script src="https://www.google.com/recaptcha/api.js?onload=vcRecaptchaApiLoaded&render=explicit" async="" defer=""
    = javascript_include_tag 'application'

Давайте создадим директиву post:

javascripts/directives/post_directive.js

app.directive('post', ['Post', function(Post){
  return {
    scope: {
      post: "="
    },
    restrict: 'E',
    templateUrl: 'post.html',
    replace: true,
    link: function($scope, iElm, iAttrs, controller) {
      $scope.gon = gon;
      $scope.destroy = function () {
        if (confirm("Вы уверены?"))
          Post.destroy({id: $scope.post.id})
      }
    }
  };
}]);

components/_post.html.slim

.post.clearfix
  .post__user
    .middle-ib
      a.post__user-avatar ui-sref="user_path(post.user)"
        img ng-src="{{post.user.avatar_url}}" width="75"
    .middle-ib
      .post__user-name
        a.link.text-red ui-sref="user_path(post.user)" ng-bind="post.user.name"
        rating user="post.user"
      .post__user-role
        .text-gray ng-bind="post.user.role"
      .post__user-metrics.text-gray
        .post__user-metric
          span.bold Постов:
          |  
          span ng-bind="post.user.posts_count"
        .post__user-metric
          span.bold На сайте с:
          |  
          span  ng-bind="post.user.created_at | date:'dd.MM.yyyy'"
  .post__content
    a.post__title ng-bind="post.title" ui-sref="topic_path(post.topic)"
    div ng-bind="post.content"
  
  .post__actions.only-moderator
    a.btn.btn-danger.btn-sm ng-click="destroy()" Удалить

Шаблон _post.html.slim автоматически будет подтягиваться директивой post.
По той же аналогии создадим директиву rating:

javascripts/directives/rating_directive.js

app.directive('rating', ['User', function (User) {
  return {
    scope: {
      user: "="
    },
    restrict: 'E',
    templateUrl: 'rating.html',
    replace: true,
    link: function($scope, iElm, iAttrs, controller) {
      $scope.rate = function (positive) {
        User.rate({id: $scope.user.id, positive: positive}, function (res) {
          $scope.user.rating = res.rating;
        })
      }
    }
  };
}]);

components/_rating.html.slim

span.rating
  span.text-red.rating__control ng-click="rate()"
    | ▼
  span.rating__count ng-bind="user.rating"
  span.text-green.rating__control ng-click="rate(true)"
    | ▲

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

Для трекинга онлайна создадим online.js, где по таймеру будем раз в 5 минут посылать запрос на обновление онлайна пользователя:

javascripts/online.js

app.run(['$interval', 'User', function ($interval, User) {
  User.touch();
  
  $interval(function () {
    User.touch();
  }, 5*60*1000)
}])

На серверной стороне мы внедрили reCAPTCHA. Теперь пришло время сделать это и на клиентской. Я использовал скрипт Angular Recaptcha, который содержит внутри себя директиву для удобной работы с рекапчей. В общем виде это выглядит следующим образом:

div ng-model="ctrl.user.recaptcha" vc-recaptcha="" key="'#{Rails.application.secrets[:recaptcha]["public_key"]}'"

Нам осталось написать клиентские сериалайзеры и проект готов. На основе примера ExampleSerializer я написал сериайлайзеры для всех моделей:

javascripts/serializers/post_serializer.js

function PostSerializer (collection) {
  var result = [];

  _.each(collection, function (item) {
    result.push({
      id: item[0],
      title: item[1],
      content: item[2],
      user: {
        id: item[3],
        created_at: item[4],
        name: item[5],
        rating: item[6],
        posts_count: item[7],
        avatar_url: item[8] || "/default_avatar.png"
      },
      topic: {
        id: item[9],
        title: item[10]
      }
    })
  })
 return result
}

javascripts/serializers/theme_serializer.js

function ThemeSerializer (collection) {
  var result = [];

  _.each(collection, function (item) {
    result.push({
      id: item[0],
      title: item[1]
    })
  })
  return result
}

javascripts/serializers/topic_serializer.js

function TopicSerializer (collection) {
  var result = [];

  _.each(collection, function (item) {
    result.push({
      id: item[0],
      title: item[1],
      last_post: item[2],
      posts_count: item[3],
      user: {
        id: item[4],
        name: item[5]
      },
      theme: {
        id: item[6],
        title: item[7]
      }
    })
  })
  return result
}

javascripts/serializers/user_serializer.js

function UserSerializer (collection) {
  var result = [];

  _.each(collection, function (item) {
    result.push({
      id: item[0],
      created_at: item[1],
      updated_at: item[2],
      name: item[3],
      avatar_url: item[4],
      posts_count: item[5],
      rating: item[6],
      banned: item[7]
    })
  })
  return result
}

javascripts/serializers/group_serializer.js

function GroupSerializer (collection) {
  var result = [],
      groups = _.groupBy(collection, function (el) {
        return el[0]
      });

  _.each(groups, function (group) {
    result.push({
      id: group[0][0],
      title: group[0][1],
      themes: _.map(group, function (item) {
        return {
          id: item[2],
          title: item[3],
          posts_count: item[4],
          topics_count: item[5],
          last_post: item[6]
        }
      })
    })
  })
  return result
}

Кеширование

Как вы наверняка заметили, вьюхи нашего приложения не имеют серверных элементов шаблонизации. За исключением Gon. Следовательно мы можем закешировать весь лейаут для production-окружения:

Заголовок спойлера

= cache_if Rails.env.production?, $cache_key
  html ng-app="app"
    head
      title Форум
      base href="/"
      = stylesheet_link_tag 'application'
    body ng-controller="MainCtrl as main" ng-class="gon.current_user.role"
      .layout.body
        .search
          input.form-control placeholder="Поиск" type="text" ng-model="main.search" ng-model-options="{debounce: 300}"
        .bredcrumbs ng-yield="bredcrumbs"
        .wrapper
          .content
            ui-view
          .sidebar
            = render "layouts/sidebar"
                    
      = render template: "components/render"
      script src="https://www.google.com/recaptcha/api.js?onload=vcRecaptchaApiLoaded&render=explicit" async="" defer=""
      = javascript_include_tag 'application'

= Gon::Base.render_data

Для того, чтобы сбрасывать кеш при перезапуске сервера/деплое создадим инициалайзер с динамической глобальной переменной $layout_cache, которая будет выполнять функции cache_key:

config/initializers/layout_cache.rb

$layout_cache = "layout_#{Time.now.to_i}"

Итог

Вот так очень просто шаг за шагом мы написали вполне работоспособный Single Page Application форум, со странным, но хорошо оптимизированным API. За кадром остались

тесты

и

интернационализация

приложения. Об этом расскажу в следующих статьях.

Репозиторий с полным исходным кодом
Развернутое приложение

Как новичку создать форум

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

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

  • Что это и зачем?
    • Как создать
    • Какой движок выбрать?
    • Где и как можно быстро создать форум?

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

Что это и зачем?

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

Перечислим некоторые из них:

  • Форум как часть информационного ресурса. Часто бывает полезным организовать общение между посетителями сайта, также это отличный инструмент для обеспечения обратной связи;
  • Для популяризации проекта. Обсуждение какой-то тематики помогает вызвать к ней интерес пользователей;
  • Как самостоятельный сайт. Форум может предоставить все те же прелести, что и любой другой информационный ресурс.

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

В наше время всегда есть возможность выбора, это касается и создания форумов. Можно выделить следующие варианты:

  • Самостоятельное написание кода. Этот способ подходит для веб-программистов, у которых есть время и достаточные знания. Преимущество такого подхода в практически ничем не ограниченных возможностях. Недостаток один: простым смертным не по плечу такое создание форума;
  • Использование движков для создания форумов. Пожалуй, самый оптимальный вариант. От вас требуется оплатить хостинг и установить понравившийся движок. Ваши возможности будут ограничены программным обеспечением, но практика показывает, что функционала самых популярных платформ хватает «с головой»;
  • Использовать хостинг форумов. Данный способ подойдет для новичков и тех, кто не хочет разбираться с движками. Создание форума займет немного времени, однако ваши возможности будут существенно ограничены. Кроме того, монетизация будет затруднительной, так как форум, по сути, вам полностью не принадлежит.

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

  • vBulletin. Этот движок наиболее популярен среди людей, которые создают масштабные проекты, и это неспроста. Платформа считается лучшей и практически лишена недостатков, также постоянно выходят обновления;

Какой движок выбрать?

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

  • Invision Power Board (IPB). Еще один платный движок, однако, менее популярный, чем предыдущий;

Какой движок выбрать? - 2

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

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

Недостатком движка является достаточно низкая безопасность.

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

  • Forum2x2.ru. При создании форума можно выбирать один из трех движков, также предоставляется разнообразие доменов. Сервис совершенно бесплатный и обладает качественной техподдержкой;
  • Starbb.ru. Быстрое создание форума с качественным дизайном. Если в течение года форум не «разразился» ни одним сообщением, то он будет автоматически удален;
  • Lifeforums.ru. Выбор домена на данном сервисе значительно упрощен (всего один доступный вариант), кроме того созданный с его помощью форум будет переполнен рекламой, избавиться от которой можно лишь за деньги.

В качестве примера создадим свой форум при помощи сервиса Forum2x2.ru. Для начала необходимо заполнить следующую форму:

Где и как можно быстро создать форум?

Теперь укажем данные о нашем форуме, заполнить соответствующую форму:

Где и как можно быстро создать форум? - 2

После этого вас обрадуют следующим сообщением:

«Новорожденный» форум выглядит следующим образом:

Таким образом, перед тем, как создать форум, необходимо определиться с технологиями, которые будут использованы.

Самостоятельное написание кода даст наибольшую гибкость, использование форум-хостингов займёт меньше времени, но наиболее оптимальный вариант – создать свой форум с помощью одного из популярных движков.

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

Как выглядит удобный форум

Специфика форумов

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

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

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

Структура регионального форума на примере площадки
«Независимый Петербург»

Разновидности форумов: особенности и назначение

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

Популярные способы монетизации:

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

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

С точки зрения направленности, выделяют такие типы площадок:

  1. Тематические профессиональные: форум о рыбалке, автомобилях, детях, моде – все это предполагает наличие определенной доли экспертности или опыта у комментаторов.
  2. Проектные: используется для достижения определенного результата – реализации проекта. Такие сайты обычно закрыты от посторонних.
  3. Информационно-развлекательные: предназначены для общения, распространения новостей, развлечения.

Виды ресурсов по структуре содержания:

  1. Табличные: строгая иерархия ленточного типа. Необходима строгая модерация флуда и флейма.
  2. Древовидные: иерархия на основе указания, к какому сообщению относится ответ. В одной теме может происходить одновременно несколько бесед, в которых группы пользователей не мешают друг другу.
  3. Универсальный: наиболее популярный вариант, предоставляющий пользователю свободу выбора между структурой. Такие сообщества поддерживают и табличную и древовидную структуру.с

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

Статистика популярных тематик

Три способа создать форум с преимуществами и недостатками

Создание площадки форумного типа осуществляется следующими методами:

  • написание кода с нуля;
  • конструктор + хостинг;
  • использование CMS.

Рассмотрим каждый из вариантов по отдельности.

Самостоятельная разработка

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

Преимущества:

  • кастомизированная уникальная площадка;
  • неограниченные возможности;
  • реализация нестандартных методик или инструментов.

Недостатки:

  • длительность разработки;
  • затраты средств на привлечение программиста;
  • нет возможности создать человеку без опыта;
  • важность соблюдения четкой структуры кода.

Создание ресурса стоит доверить профессионалу, потому что ошибки в коде приводят к нестабильности работы ресурса, а также влияют на поисковую выдачу.

Пример проекта на собственном движке – Ford Fokus club

Конструктор + хостинг

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

Преимущества:

  • минимальное время для запуска;
  • нет необходимости в знаниях;
  • комплекс услуг в одном с простым подключением;
  • качественная поддержка.

Недостатки:

  • ограниченные возможности;
  • абонентская плата;
  • существенные ограничения для бесплатной версии;
  • площадка в собственности поставщика (домен третьего уровня).

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

Шаблон для конструктора Ucoz

CMS для форума

Это система управления контентом, часто называется «движком». Представляет собой оболочку с набором скриптов для упрощения и автоматизации действий пользователя. Разработать сайт форумного типа может человек без опыта, но знания помогут при отладке площадки.

Преимущества:

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

Недостатки:

  • некоторые ограничения самого движка;
  • желательно иметь знания для отладки и настройки.

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

Простой и удобный форум на движке SMF

Разновидности CMS

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

Движком для проекта могут быть:

  • отдельные CMS;
  • плагины для существующих платформ.

Первый вариант заинтересует желающих завести независимый ресурс, а второй – для организации связки «сайт + форум», где под сайтом выступает любой тип площадки: интернет-магазин, портал, СМИ, блог.

По способу распространения платформы подразделяются на:

  • платные;
  • бесплатные.

Стоимость специализированных движков для форумов достаточно высока: в среднем $500. Цена не означает качество, а вот популярность говорит об активной разработке продукта, выходе обновлений, исправлении ошибок. В платных версиях предусматривается поддержка клиентов, где на возникший вопрос отвечает менеджер разработчика, а в бесплатной – придется самостоятельно искать ответы, спрашивать других пользователей.

Чтобы убедиться в целесообразности покупки CMS, рекомендуется предварительно проверить её функционал. Для этого разработчики предоставляют тестовый период, достаточный для принятия решения. Устанавливать взломанную платформу или движок от непроверенных разработчиков нельзя – код системы может содержать вредоносные элементы, рекламу и быть нестабильным.

Пример оформления в CMS IPS Community Suite 4

ТОП-10 CMS для разработки форумов

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

Рейтинг CMS:

  1. IPS Community Suite – платный движок от зарекомендовавших себя разработчиков. Владельцу ресурса предложен большой выбор инструментов и тем оформления с гибкой настройкой системы.
  2. XenForo – стоит дешевле, но функциональность практически аналогичная. Рациональное решение для поклонников минимализма и легкости платформы с быстрой загрузкой страниц.
  3. vBulletin – для разблокировки полного функционала требуется единоразовый взнос. Для кастомизации ресурса и настройки оформления необходимо подобрать шаблон, и выполнить редактирование.
  4. phpBB – популярное решение среди вебмастеров, выбирающих бесплатные движки. Имеет мощную поддержку сообщества и сапорт. Отличается гибкостью настройки и кастомизации, обилием расширений и решений для удобства управления площадкой.
  5. SMF – бесплатный движок, распространяемый как open source приложение. При достаточном мастерстве вебмастера реально создать качественный проект, не уступающий платным CMS.
  6. WpForo и BBPress – плагины для движка WordPress, доступные в каталоге. Отличаются простотой установки и скоростью настройки. Доступна гибкая настройка функциональности.
  7. punBB – распространяется бесплатно. Особенности платформы – чистота кода и легкость. Отлично подходит для новичка. При небольших возможностях оптимизации, имеет высокую производительность и минимальную нагрузку сервера.
  8. Phorum – продукт с открытым исходным кодом, распространяется бесплатно. Имеет модульную архитектуру, которую удобно расширять самописными плагинами. Отличается обширным комплексом инструментов модерации.
  9. XMB – движок для опытных вебмастеров. Это необходимо для реализации заложенного потенциала. Достоинством платформы выступает минимальная нагрузка сервера.
  10. Vanilla – простая и удобная платная CMS с упрощенной настройкой и отладкой, разработанная на собственном фреймворке. Имеет удобную систему навигации. Подходит для организации корпоративных сайтов и полноценных порталов.

Перейдем к рассмотрению критериев выбора.

Рейтинг ПО для форумов по версии ForumRate.ru

На какие параметры движка обратить внимание

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

  1. Назначение ресурса. Специализированные движки обладают мощной функциональностью, а плагины – это простые решения для небольших сообществ.
  2. Функциональность системы. Определяется доступными инструментами и плагинами.
  3. Популярность движка. Чем больше пользователей, тем шире арсенал готовых решений и выше вероятность самостоятельно найти решение возникшей проблемы.
  4. Частота выхода апдейтов. Демонстрирует стремление разработчиков усовершенствовать продукт новыми возможностями. С обновлениями выпускаются решения известных проблем, что повышает стабильность ресурса.
  5. Абонентская плата и ее размеры. Объясняет целесообразность оформления покупки. Не имеет смысла приобретать дорогостоящий движок ради экспериментального проекта.
  6. Возможности адаптации и механизмы редактирования. Ускоряют запуск, поэтому лучше попробовать несколько вариантов, чтобы подобрать наиболее удобный.

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

Панель администратора в CMS phpBB

Установка CMS для форумов

Процедура инсталляции осуществляется стандартно, и для этого потребуются:

  • хостинг;
  • подготовленная база данных (информация понадобится в процессе);
  • присвоенное доменное имя;
  • архив с установщиком (скачать с официального сайта разработчика).

Инструкция установки движка форума на виртуальный хостинг:

  1. Зайти в панель Администратора хостинга.
  2. Перейти в раздел «Менеджер файлов».
  3. Открыть корневую директорию, обычно – WWW/»доменное имя». Вместо «WWW» может присутствовать «public_html».
  4. Воспользовавшись загрузкой, залить архив с установщиком.
  5. Распаковать архив.
  6. Открыть появившуюся папку, скопировать все содержимое, и перенести его в папку WWW/»доменное имя».
  7. После завершения переноса в браузере перейти по адресу «доменное имя»/install.
  8. Завершить процедуру инсталляции, заполнив поля подключения к базе данных.

Инструкция предназначена для создания самостоятельной площадки, не привязанной к другому сайту. Если же необходимо прикрепить форум к другому ресурсу, то процедура выглядит аналогичным образом, но на этапе подготовки необходимо указать раздел для установки движка – WWW/»доменное имя»/forum или создать поддомен в каталоге на хостинге для получения адреса в формате forum.»доменное имя».

Распакованный установщик в диспетчере файлов хостинга CPanel

Перенос файлов установщика возможно выполнить через FTP-клиент. Для этого в панели Администратора хостинга необходимо разрешить доступ по FTP. Значения заполненной формы пригодятся для создания подключения. Рекомендуется использовать защищенные протоколы передачи данных SFTP или FTPS. Filezilla и другие файловые менеджеры с поддержкой FTP исключают необходимость использования файлового менеджера в панели Администратора на хостинге, что повышает уровень защиты. Протокол поддерживает докачку файлов на случай обрыва соединения, что гарантирует целостность объектов.

Требования к форумам

Чтобы представить, какой должна быть площадка, достаточно поставить себя на место посетителя, взглянуть на сайт глазами пользователя. Главное в сообществе – его польза для аудитории – без этого рассчитывать на популярность ресурса не имеет смысла.

К другим критериям сообщества относятся:

  • удобство;
  • понятная навигация;
  • интересная и актуальная тематика;
  • живое активное общение;
  • приятное оформление.

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

Требования пользователей и поисковых систем

Форум – это площадка, где создателями контента являются сами пользователи (UGC). С ростом посещаемости растет скорость генерации контента. Основным источником трафика для сообществ является поисковая система. От нее зависит, как быстро будет расти аудитория, поэтому рассматривая требования к площадке, нельзя не упомянуть о требованиях поисковых систем.

Чего хотят поисковики

Перечислим основные требования поисковых систем к ресурсу:

  • полезный и уникальный контент;
  • чистый код и четкая структура;
  • отсутствие Flash-элементов в навигации;
  • отсутствие пустых страниц и ссылок на них;
  • корректное оформление;
  • поведенческий фактор.

Придерживаясь этих простых правил с момента запуска, удастся сэкономить много времени на оптимизации при развитии площадки.

Адаптивный дизайн

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

Пример адаптации сайта под различные устройства

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

Способы популяризации форума

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

  • денежное вознаграждение за сообщение или помощь;
  • репутация аккаунта;
  • система рейтингов;
  • продвижение статуса;
  • конкурсы и акции.

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

Подведем итоги

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

Даты основания самых посещаемых форумов Рунета

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

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

Как создать форум на сайте

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

Два варианта размещения форума

Итак, вы уже определились с тем, что форум должен быть частью вашего проекта, а не создаваться на отдельном домене. Теперь вам нужно более конкретно выбрать, где его разместить. Тут есть 2 варианта:

Создать поддомен (или субдомен)

Создать новый каталог в корневой папке

Эти способы достаточно сильно отличаются друг от друга. Чем? Прежде всего тем, что субдомен не получает параметров, которые есть у основного ресурса, это как бы совсем отдельный островочек со своим отдельным функционалом. В таком случае адрес вашего форума будет примерно таким: forum.yoursite.ru

JavaScript. Быстрый старт

Изучите основы JavaScript на практическом примере по созданию веб-приложения

Узнать подробнее

Такой способ немного сложнее следующего, поэтому на нем я остановлюсь подробнее. Именно на том, как создать поддомен. Объясню на примере панели ispmanager. Вам нужно выбрать пункт WWW-домены и выделить нужный адрес, после чего нажать кнопку Изменить.

Тут вам нужно найти настройку Автоподдомены и указать вариант: в отдельной директории, я советую именно этот вариант, так как он позволит полностью отделить раздел от основного ресурса.

Теперь идем в менеджер файлов. Вам нужно попасть в папку со списком ваших доменов. То есть не в корневую папку конкретного сайта, а на уровень выше. Здесь создаем новый каталог, его нужно назвать примерно так: forum.site.ru. Все, теперь для примера можете бросить туда html-файл и проверить, открывается ли он. Если да, значит вы все сделали правильно. Место для форума готово.

Если же вы создадите просто новую папку в корневом каталоге, то такой форум получит все показатели своего ресурса, а еще он также будет влиять на эти показатели. Но при этом это тоже останется как бы отдельным островком, куда вы можете поставить другой шаблон, да даже другой движок. Путь к форуму будет примерно таким: yoursite.ru/forum

То есть это будет как будто отдельный сайт со своей cms, но в то же время он будет всего лишь ответвлением, как будто это какая-то отдельная рубрика.

Допустим, вы выбрали второй способ, с созданием новой папки в корне. Что делать дальше? Дальше, собственно говоря, нужно поставить какой-то движок туда. Скорее всего, он будет отличаться от основного движка. Например, если вы решили прикрутить форум к блогу, то наверняка сам блог работает на WordPress, а вот форум делать на этом движке не так удобно, хотя это и возможно с помощью сторонних расширений.

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

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

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

Настройка и наполнение

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

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

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

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

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

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

JavaScript. Быстрый старт

Изучите основы JavaScript на практическом примере по созданию веб-приложения

Узнать подробнее

Фреймворк YII2. Быстрый старт

Создание блога с использованием фреймворка Yii2!

Смотреть видео

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

Если вам нужна дополнительная помощь по этому или другим вопросам, связанным с PHP, попробуйте связаться с одним из разработчиков PHP в Envato Studio. Они могут помочь вам во всем – от исправлений PHP до разработки надежных приложений PHP.

PHP разработчики на Envato Studio

PHP разработчики на Envato Studio

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

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

пользователей

  • категории
  • темы
  • Сообщений

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

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

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

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

01

02

03

04

05

06

07

08

09

10

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 используется для указания того, что мы хотим создать новую таблицу. За оператором следует имя таблицы, а все столбцы указаны в скобках. Имена всех полей не требуют пояснений, поэтому мы обсудим только типы данных ниже.

ID пользователя

«Первичный ключ используется для уникальной идентификации каждой строки в таблице».

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

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

Все остальные таблицы также имеют первичные ключи и работают одинаково.

user_name

Это текстовое поле, называемое полем VARCHAR в MySQL. Число в скобках – это максимальная длина. Пользователь может выбрать имя пользователя длиной до 30 символов. Это поле не может быть пустым. Внизу таблицы видно, что это поле объявлено UNIQUE, что означает, что одно и то же имя пользователя не может быть зарегистрировано дважды. Часть UNIQUE INDEX сообщает базе данных, что мы хотим добавить уникальный ключ. Затем мы определяем имя уникального ключа, user_name_unique в этом случае. Между скобками находится поле, к которому применяется уникальный ключ, то есть user_name.

user_pass

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

user_email

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

user_date

Это поле, в котором мы будем хранить дату регистрации пользователя. Это тип DATETIME, и поле не может быть NULL.

user_level

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

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

1

2

3

4

5

6

7

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;

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

Таблица тем

1

2

3

4

5

6

7

8

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 относится к категории, к которой относится тема. Мы не можем форсировать эти отношения, просто объявив поле. Мы должны сообщить базе данных, что это поле должно содержать существующий user_id из таблицы users или действительный cat_id из таблицы категорий. Мы добавим некоторые отношения после того, как я обсудил таблицу сообщений.

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

1

2

3

4

5

6

7

8

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 относится к теме, к которой принадлежит сообщение.

«Внешний ключ – это ссылочное ограничение между двумя таблицами. Внешний ключ идентифицирует столбец или набор столбцов в одной (ссылающейся) таблице, которая ссылается на столбец или набор столбцов в другой (ссылочной) таблице».

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

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

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

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

1

ALTER TABLE topics ADD FOREIGN KEY(topic_cat) REFERENCES categories(cat_id) ON DELETE CASCADE ON UPDATE CASCADE;

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

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

1

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

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

Связать сообщения с темами:

1

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

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

1

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN”

“http://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.

    </div>

        <div id=”content”>

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

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

1

2

3

4

5

</div><!– content –>

</div><!– wrapper –>

<div id=”footer”>Created for Nettuts+</div>

</body>

</html>

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

01

02

03

04

05

06

07

08

09

10

11

12

<?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, когда закончите этот урок; Вы заметите, что нижний колонтитул меняется на каждой странице сразу. Наконец, мы добавляем таблицу стилей, которая предоставляет нам некоторую базовую разметку – ничего особенного.

001

002

003

004

005

006

007

008

009

010

011

012

013

014

015

016

017

018

019

020

021

022

023

024

025

026

027

028

029

030

031

032

033

034

035

036

037

038

039

040

041

042

043

044

045

046

047

048

049

050

051

052

053

054

055

056

057

058

059

060

061

062

063

064

065

066

067

068

069

070

071

072

073

074

075

076

077

078

079

080

081

082

083

084

085

086

087

088

089

090

091

092

093

094

095

096

097

098

099

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

body {

    background-color: #4E4E4E;

    text-align: center;

}

#wrapper {

    width: 900px;

    margin: 0 auto;

}

#content {

    background-color: #fff;

    border: 1px solid #000;

    float: left;

    font-family: Arial;

    padding: 20px 30px;

    text-align: left;

    width: 100%;

}

#menu {

    float: left;

    border: 1px solid #000;

    border-bottom: none;

    clear: both;

    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;

/* 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. Мы включим его в каждый файл, который собираемся создать.

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

<?php

//connect.php

$server = ‘localhost’;

$username = ‘usernamehere’;

$password = ‘passwordhere’;

$database = ‘databasenamehere’;

if(!mysql_connect($server, $username, $password))

{

    exit(‘Error: could not establish database connection’);

}

if(!mysql_select_db($database)

{

    exit(‘Error: could not select the database’);

}

?>

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

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

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

<?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-формы, чтобы новый пользователь мог зарегистрироваться.

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

<?php

//signup.php

include ‘connect.php’;

include ‘header.php’;

echo ‘<h3>Sign up</h3>’;

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”>

        Password again: <input type=”password” name=”user_pass_check”>

        E-mail: <input type=”email” name=”user_email”>

        <input type=”submit” value=”Add category” />

     </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. Save the data

    */

    $errors = array();

    if(isset($_POST[‘user_name’]))

    {

        //the user name exists

        if(!ctype_alnum($_POST[‘user_name’]))

        {

            $errors[] = ‘The username can only contain letters and digits.’;

        }

        if(strlen($_POST[‘user_name’]) > 30)

        {

            $errors[] = ‘The username cannot be longer than 30 characters.’;

        }

    }

    else

    {

        $errors[] = ‘The username field must not be empty.’;

    }

    if(isset($_POST[‘user_pass’]))

    {

        if($_POST[‘user_pass’] != $_POST[‘user_pass_check’])

        {

            $errors[] = ‘The two passwords did not match.’;

        }

    }

    else

    {

        $errors[] = ‘The password field cannot 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>’ .

        }

        echo ‘</ul>’;

    }

    else

    {

        //the form has been posted without, so save it

        //notice the use of mysql_real_escape_string, keep everything safe!

        //also notice the sha1 function which hashes the password

        $sql = “INSERT INTO

                    users(user_name, user_pass, user_email ,user_date, user_level)

                VALUES(‘” . mysql_real_escape_string($_POST[‘user_name’]) . “‘,

                       ‘” . sha1($_POST[‘user_pass’]) . “‘,

                       ‘” . mysql_real_escape_string($_POST[‘user_email’]) . “‘,

                        NOW(),

                        0)”;

        $result = mysql_query($sql);

        if(!$result)

        {

            //something went wrong, display the error

            echo ‘Something went wrong while registering.

            //echo mysql_error();

        }

        else

        {

            echo ‘Successfully registered.

        }

    }

}

include ‘footer.php’;

?>

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

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

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

1

2

3

4

5

6

7

INSERT INTO

       users(user_name, user_pass, user_email ,user_date, user_level)

VALUES(‘” . mysql_real_escape_string($_POST[‘user_name’]) . “‘,

       ‘” . sha1($_POST[‘user_pass’]) . “‘,

       ‘” . mysql_real_escape_string($_POST[‘user_email’]) . “‘,

       NOW(),

       0);

В строке 1 у нас есть оператор INSERT INTO, который говорит сам за себя. Имя таблицы указывается во второй строке. Слова в скобках представляют столбцы, в которые мы хотим вставить данные. Оператор VALUES сообщает базе данных, что мы завершили объявление имен столбцов, и пришло время указать значения. Здесь есть что-то новое: mysql_real_escape_string. Функция экранирует специальные символы в неэкранированной строке, поэтому ее можно безопасно разместить в запросе. Эта функция ДОЛЖНА использоваться всегда, за очень немногими исключениями. Слишком много скриптов, которые не используют его и могут быть взломаны очень легко. Не рискуйте, используйте mysql_real_escape_string ().

«Никогда не вставляйте простой пароль как есть. Вы ДОЛЖНЫ всегда его шифровать».

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

Примечание: также возможно использовать md5 (), я всегда использую sha1 (), потому что тесты показали, что он немного быстрее, хотя и немного. Вы можете заменить sha1 на md5, если хотите.

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

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

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

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

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

Процесс входа в систему работает следующим образом:

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

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

001

002

003

004

005

006

007

008

009

010

011

012

013

014

015

016

017

018

019

020

021

022

023

024

025

026

027

028

029

030

031

032

033

034

035

036

037

038

039

040

041

042

043

044

045

046

047

048

049

050

051

052

053

054

055

056

057

058

059

060

061

062

063

064

065

066

067

068

069

070

071

072

073

074

075

076

077

078

079

080

081

082

083

084

085

086

087

088

089

090

091

092

093

094

095

096

097

098

099

100

101

102

103

104

105

106

107

<?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();

        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>’ .

            }

            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 = ‘” . mysql_real_escape_string($_POST[‘user_name’]) . “‘

                    AND

                        user_pass = ‘” . sha1($_POST[‘user_pass’]) . “‘”;

            $result = mysql_query($sql);

            if(!$result)

            {

                //something went wrong, display the error

                echo ‘Something went wrong while signing in. Please try again later.’;

                //echo mysql_error();

            }

            else

            {

                //the query was successfully executed, there are 2 possibilities

                //1.

                //2.

                if(mysql_num_rows($result) == 0)

                {

                    echo ‘You have supplied a wrong user/password combination.

                }

                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 = mysql_fetch_assoc($result))

                    {

                        $_SESSION[‘user_id’] = $row[‘user_id’];

                        $_SESSION[‘user_name’] = $row[‘user_name’];

                        $_SESSION[‘user_level’] = $row[‘user_level’];

                    }

                    echo ‘Welcome, ‘ .

                }

            }

        }

    }

}

include ‘footer.php’;

?>

Это запрос, который находится в файле signin.php:

01

02

03

04

05

06

07

08

09

10

SELECT

    user_id,

    user_name,

    user_level

FROM

    users

WHERE

    user_name = ‘” . mysql_real_escape_string($_POST[‘user_name’]) . “‘

AND

    user_pass = ‘” . sha1($_POST[‘user_pass’])

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

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

01

02

03

04

05

06

07

08

09

10

<?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 = mysql_fetch_assoc($result))

{

    $_SESSION[‘user_id’] = $row[‘user_id’];

    $_SESSION[‘user_name’] = $row[‘user_name’];

}

?>

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

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

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

Заменить:

1

<div id=”userbar”>Hello Example.

С:

01

02

03

04

05

06

07

08

09

10

11

<?php

<div id=”userbar”>

    if($_SESSION[‘signed_in’])

    {

        echo ‘Hello’ .

    }

    else

    {

        echo ‘<a href=”signin.php”>Sign in</a> or <a href=”sign up”>create an account</a>.’;

    }

</div>

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

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

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

1

2

3

4

5

<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 (регистрация пользователя), поэтому я не буду здесь подробно объяснять. Если вы выполнили все шаги, вы сможете понять это довольно быстро.

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

<?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

    $sql = ìINSERT INTO categories(cat_name, cat_description)

       VALUES(” . mysql_real_escape_string($_POST[‘cat_name’]) . ì’,

             ” .

    $result = mysql_query($sql);

    if(!$result)

    {

        //something went wrong, display the error

        echo ‘Error’ .

    }

    else

    {

        echo ‘New category successfully added.’;

    }

}

?>

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

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

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

1

2

3

4

5

6

SELECT

    categories.cat_id,

    categories.cat_name,

    categories.cat_description,

FROM

    categories

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

<?php

//create_cat.php

include ‘connect.php’;

include ‘header.php’;

$sql = “SELECT

            cat_id,

            cat_name,

            cat_description,

        FROM

            categories”;

$result = mysql_query($sql);

if(!$result)

{

    echo ‘The categories could not be displayed, please try again later.’;

}

else

{

    if(mysql_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 = mysql_fetch_assoc($result))

        {

            echo ‘<tr>’;

                echo ‘<td class=”leftpart”>’;

                    echo ‘<h3><a href=”category.php?id”>’ .

                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. Например, у нас есть эта ссылка:

Оператор echo $ _GET [ëcat_id ‘];’ будет отображаться «23». В следующих нескольких шагах мы будем использовать это значение для получения тем при просмотре одной категории, но темы нельзя просмотреть, если мы их еще не создали. Итак, давайте создадим несколько тем!

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

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

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

<?php

if(user is 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

    }

}

?>

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

001

002

003

004

005

006

007

008

009

010

011

012

013

014

015

016

017

018

019

020

021

022

023

024

025

026

027

028

029

030

031

032

033

034

035

036

037

038

039

040

041

042

043

044

045

046

047

048

049

050

051

052

053

054

055

056

057

058

059

060

061

062

063

064

065

066

067

068

069

070

071

072

073

074

075

076

077

078

079

080

081

082

083

084

085

086

087

088

089

090

091

092

093

094

095

096

097

098

099

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

<?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 = mysql_query($sql);

        if(!$result)

        {

            //the query failed, uh-oh 🙁

            echo ‘Error while selecting from database.

        }

        else

        {

            if(mysql_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 = mysql_fetch_assoc($result))

                    {

                        echo ‘<option value=”‘ . $row[‘cat_id’] . ‘”>’ .

                    }

                echo ‘</select>’;

                echo ‘Message: <textarea name=”post_content” /></textarea>

                    <input type=”submit” value=”Create topic” />

                 </form>’;

            }

        }

    }

    else

    {

        //start the transaction

        $query = “BEGIN WORK;”;

        $result = mysql_query($query);

        if(!$result)

        {

            //Damn!

            echo ‘An error occured while creating your topic.

        }

        else

        {

            //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(‘” . mysql_real_escape_string($_POST[‘topic_subject’]) . “‘,

                               NOW(),

                               ” . mysql_real_escape_string($_POST[‘topic_cat’]) . “,

                               ” . $_SESSION[‘user_id’] . “

                               )”;

            $result = mysql_query($sql);

            if(!$result)

            {

                //something went wrong, display the error

                echo ‘An error occured while inserting your data.

                $sql = “ROLLBACK;”;

                $result = mysql_query($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 = mysql_insert_id();

                $sql = “INSERT INTO

                            posts(post_content,

                                  post_date,

                                  post_topic,

                                  post_by)

                        VALUES

                            (‘” . mysql_real_escape_string($_POST[‘post_content’]) . “‘,

                                  NOW(),

                                  ” . $topicid . “,

                                  ” . $_SESSION[‘user_id’] . “

                            )”;

                $result = mysql_query($sql);

                if(!$result)

                {

                    //something went wrong, display the error

                    echo ‘An error occured while inserting your post.

                    $sql = “ROLLBACK;”;

                    $result = mysql_query($sql);

                }

                else

                {

                    $sql = “COMMIT;”;

                    $result = mysql_query($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. Здесь есть что-то особенное, потому что мы используем выпадающий список. Этот раскрывающийся список заполнен данными из базы данных, используя этот запрос:

1

2

3

4

5

6

SELECT

    cat_id,

    cat_name,

    cat_description

FROM

    categories

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

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

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

01

02

03

04

05

06

07

08

09

10

11

<?php

//start the transaction

$query = “BEGIN WORK;”;

$result = mysql_query($query);

//stop the transaction

$sql = “ROLLBACK;”;

$result = mysql_query($sql);

//commit the transaction

$sql = “COMMIT;”;

$result = mysql_query($sql);

?>

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

1

2

3

4

5

6

7

8

9

INSERT INTO

    topics(topic_subject,

               topic_date,

               topic_cat,

               topic_by)

VALUES(‘” . mysql_real_escape_string($_POST[‘topic_subject’]) . “‘,

       NOW(),

       ” . mysql_real_escape_string($_POST[‘topic_cat’]) . “,

       ” . $_SESSION[‘user_id’] . “)

Сначала определяются поля, затем значения для вставки. Первый из них мы видели раньше, это просто строка, которая становится безопасной с помощью mysql_real_escape_string (). Второе значение, NOW (), является функцией SQL для текущего времени. Третье значение, однако, это значение, которое мы не видели раньше. Это относится к (действительному) идентификатору категории. Последнее значение относится к (существующему) user_id, который в данном случае является значением $ _SESSION [«user_id»]. Эта переменная была объявлена ​​во время входа в систему.

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

01

02

03

04

05

06

07

08

09

10

INSERT INTO

        posts(post_content,

        post_date,

        post_topic,

        post_by)

VALUES

        (‘” . mysql_real_escape_string($_POST[‘post_content’]) . “‘,

         NOW(),

         ” . $topicid . “,

         ” . $_SESSION[‘user_id’] . “)

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

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

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

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

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

Необходим для отображения категории

  • cat_name
  • cat_description

Необходим для отображения всех тем

  • topic_id
  • topic_subject
  • topic_date
  • topic_cat

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

1

2

3

4

5

6

7

8

SELECT

    cat_id,

    cat_name,

    cat_description

FROM

    categories

WHERE

    cat_id = ” . mysql_real_escape_string($_GET[‘id’])

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

1

2

3

4

5

6

7

8

9

SELECT

    topic_id,

    topic_subject,

    topic_date,

    topic_cat

FROM

    topics

WHERE

    topic_cat = ” . mysql_real_escape_string($_GET[‘id’])

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

<?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 = ” . mysql_real_escape_string($_GET[‘id’]);

$result = mysql_query($sql);

if(!$result)

{

    echo ‘The category could not be displayed, please try again later.’

}

else

{

    if(mysql_num_rows($result) == 0)

    {

        echo ‘This category does not exist.’;

    }

    else

    {

        //display category data

        while($row = mysql_fetch_assoc($result))

        {

            echo ‘<h2>Topics in ′’ .

        }

        //do a query for the topics

        $sql = “SELECT

                    topic_id,

                    topic_subject,

                    topic_date,

                    topic_cat

                FROM

                    topics

                WHERE

                    topic_cat = ” . mysql_real_escape_string($_GET[‘id’]);

        $result = mysql_query($sql);

        if(!$result)

        {

            echo ‘The topics could not be displayed, please try again later.’;

        }

        else

        {

            if(mysql_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 = mysql_fetch_assoc($result))

                {

                    echo ‘<tr>’;

                        echo ‘<td class=”leftpart”>’;

                            echo ‘<h3><a href=”topic.php?id=’ . $row[‘topic_id’] . ‘”>’ .

                        echo ‘</td>’;

                        echo ‘<td class=”rightpart”>’;

                            echo date(‘dm-Y’, strtotime($row[‘topic_date’]));

                        echo ‘</td>’;

                    echo ‘</tr>’;

                }

            }

        }

    }

}

include ‘footer.php’;

?>

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

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

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

1

2

3

4

5

6

7

SELECT

    topic_id,

    topic_subject

FROM

    topics

WHERE

    topics.topic_id = ” . mysql_real_escape_string($_GET[‘id’])

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

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 = ” . mysql_real_escape_string($_GET[‘id’])

На этот раз нам нужна информация от пользователей и таблицы сообщений – поэтому мы снова используем LEFT JOIN. Условие таково: идентификатор пользователя должен совпадать с полем post_by. Таким образом, мы можем показать имя пользователя, который ответил на каждый пост.

Окончательный вид темы выглядит так:

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

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

1

2

3

4

<form method=”post” action=”reply.php?id=5″>

    <textarea name=”reply-content”></textarea>

    <input type=”submit” value=”Submit reply” />

</form>

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

<?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 (‘” . $_POST[‘reply-content’] . “‘,

                        NOW(),

                        ” . mysql_real_escape_string($_GET[‘id’]) . “,

                        ” . $_SESSION[‘user_id’] . “)”;

        $result = mysql_query($sql);

        if(!$result)

        {

            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’;

?>

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

Заканчивать

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

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

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

Учитывая такой огромный выбор, возникает вопрос: какой вариант является самым оптимальным?

Как бесплатно запустить форум?

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

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

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

В чем же они заключаются? У блога имеется один хозяин – автор. Несмотря на то что люди что-то пишут, обсуждают и комментируют владельцем является тот человек, который его постоянно наполняет новыми материалами, то есть автор блога. На форуме все иначе.

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

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

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

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

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

  1. Выполнении простой регистрации и создание форума.
  2. Определение с поддоменом и шаблоном.
  3. Адаптации дизайна под себя, описание на главной странице чему посвящается форум.
  4. Выполнение настроек форума: разработка разделов, подфорумов, форумов, разных тематик, создание прав для разных посетителей.
  5. Присоединение домена и собственно публикация проекта!

Идея

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

Однако важно помнить, что создание форума с абсолютного нуля — вариант, который нельзя назвать простым для человека, который не изучал основы веб-программирования. А вот у профессионального программиста с самостоятельной разработкой форума не должно возникнуть никаких трудностей. Это обусловлено тем, что форум можно создать практически на любом современном языке веб-программирования.

Чаще всего для этого используется MySQL и PHP. Причем совершенно необязательно начинать разработку форума с нуля. Для решения этой задачи можно использовать готовый набор компонентов, находящийся в свободном доступе на GitHub. Для создания интернет-форума также могут понадобиться такие фреймворки, как Codeigniter, Zend Framework, Symfony, Yii и Laravel.

Для верстки внешнего вида форума обычно используются HTML-фреймворки. Наиболее популярным вариантом считается фреймворк Bootstrap. Для обеспечения динамичной работы отдельных частей проекта также можно использовать JavaScript-библиотеки jQuery или ReactJS.

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

Как создать форум бесплатно?

Для этого просто используйте любой из понравившихся бесплатных конструкторов. Принцип действия у всех один и тот же. Разберем создание форума на примере сервиса Ucoz. Для этого на проекте пройдите простую регистрацию и перейдите по этой ссылке: http://www.ucoz.ru/help/start/kak-sozdat-svoj-sajt-v-ucoz#step3

Вам необходимо написать название. Вы должны помнить, что название веб проекта должно в полной мере отражать его тему. Далее определитесь с внешним видом и кликните на кнопку «Продолжить».

Как создать форум самостоятельно и бесплатно

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

Внимание! При оставлении на веб проекте «Редактор станиц» у вашего форума будут такие страницы как: О сайте, Главная и страница для обратной связи, если конечно запущен модуль почтовых форм.

Чтобы убрать главную страницу посетите настройки. Дальше перейдите в «Системные» и выберите «Главная страница». После этого под надписью при переходе на главную страницу открывать, кликните на форум. Затем просто сохраните.

Как создать форум самостоятельно и бесплатно

Возможно вы желаете, чтобы при посещении сайта сразу открывался форум, а быть может вы запустили только пункт «Форум». Тогда просто удалите раздел «Форум» из меню проекта. Чтобы это сделать вам достаточно пройти в дизайн, а затем в конструктор меню. После этого нужно выделить пункт «Форум». Дальше нажать изменить и затем удалить, и нажать сохранить.

Как создать форум самостоятельно и бесплатно

Делаем первые шаги

На только что созданном вами форуме будут находиться примеры разделов и собственно вы будете видеть тестовый проект с небольшим описанием:

Как создать форум самостоятельно и бесплатно

Скелет форума:

  • Разделы, посвященные определенным тематикам
  • В эти разделы включены разные форумы, в которых есть возможность еще и создать подфорумы
  • В самих форумах могут быть созданы темы для разговоров

На скриншоте отображена примерная структура форума:

Как создать форум самостоятельно и бесплатно

Меняем наименование форумов и категорий

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

Как создать форум самостоятельно и бесплатно

Перед вами откроется окошко, в котором необходимо ввести наименование тематической категории и нажать на кнопку «Применить».

Как создать форум самостоятельно и бесплатно

Перейдя в список разделов кликните на наименование нужной категории. Это нужно для по падения на форум. Теперь выделите форум и внизу кликните на клавишу «Изменить».

Как создать форум самостоятельно и бесплатно

Перед вами откроется окошко, в котором следует написать наименование форума и сделать его микро обзор. После того как вы это выполните нажмите кнопку «Применить».

Как создать форум самостоятельно и бесплатно

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

Если вы желаете сделать под форум, просто кликните «Создать».

Как создать форум самостоятельно и бесплатно

В появившемся окошке пропишите имя и характеристики под форума, а после этого все сохраните!

Создаем дополнительные категории и форумы

Первым делом пройдите в админ панель, затем выберите модули. После этого выберите форум, управление форумами. Как только это сделаете надавите добавить. Перед вами откроется окошко, в котором требуется ввести название новой категории. Теперь кликните на клавишу «Применить».

Как создать форум самостоятельно и бесплатно

Только что созданная категория тут же отобразится в списке!

Для помещения нового форума в только что созданную категорию просто кликните на наименование раздела в списке. И на странице форумов надавите кнопку «Добавить». После этого выйдет страница где вам нужно прописать название вашего форума и описание. Как только это сделаете нажимайте на кнопку «Применить».

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

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

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

Внешний вид проекта

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

Домен

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

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

Бесплатные движки форумов

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

Далее рассмотрим самые популярные бесплатные движки.

phpBB

Этот движок создан на PHP и MySQL. phpBB поддерживает PHP7 и отличается адаптивным дизайном. Среди других возможностей этого движка стоит отметить:

  • наличие настроек пользователя;
  • наличие системы личных сообщений;
  • возможность настройки пользовательской регистрации;
  • наличие вложений, опции быстрого ответа, эмодзи, смайликов, цитирования и т.д.

Отдельно можно отметить удобную панель администратора и возможность использования дополнительных плагинов.

Discource

Этот бесплатный движок разработан на языке Ruby. Для его создания использовался фреймворк Ruby in Rails. В качестве базы данных используется система PostgreSQL. Основными преимуществами этого форумного движка можно назвать его адаптированность под мобильные устройства, наличие динамических уведомлений, автоматическое расширение ссылок и наличие функции антиспама. Discource также предоставляет своим пользователям:

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

Также нужно сказать о наличии 2-факторной аутентификации и возможности обновления в один клик.

Вывод

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

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

Теги:

Цитата
Сообщение от Azerrot
Посмотреть сообщение

Я думал что форум с JavaScript можно сделать

да нет же

— на чистом HTML делаются исключительно статические странички, похожие на напечатанные на пишмашинке (или на отпечатанные в типографии)

— однако всякий HTML-тег изначально имеет какие-то свои стили (которые разработчиками браузеров заложены по умолчанию)
потому придумали CSS, с помощью которого ту же статическую страницу можно было представить не как захотел того разработчик браузера, а как нравится вам

— а вот только потом придумали javascript, который внедряется на страницу и может её динамически изменять: что-то прыгает, что-то летает, что-то цвет меняет, что-то исчезает, что-то появляется…
и всё это (все эти видимые изменения) происходит в конкретном браузере конкретного пользователя — сам код страницы, который хранится на сервере, никоим образом не меняется

javascript работает в конкретном браузере
чтобы было понятно, пример:
когда вы смотрите какой-то фильм по телевизору, вы можете в своём телевизоре уменьшить громкость, приглушить яркость и т.д.
но это никоим образом не повлияет на ту видеокассету, которая проигрывается в этот момент на телецентре — у неё ни громкость, ни яркость не изменятся
и ни один из миллионов телезрителей никогда не узнает, с какой именно громкостью и яркостью вы просматривали этот фильм
вот javascript — это как бы встроенные в страницу «регуляторы громкости и яркости»
понятно?
———-

Цитата
Сообщение от Azerrot
Посмотреть сообщение

Быть может посоветуете тогда какую нибудь другую полезную практическую задачу которая помогла бы закрепить и улучшить знания по JS ?

например, читайте этот форум и пытайтесь отвечать на вопросы, кои здесь появляются
выбирайте именно те, о которых вы пока ничего не знаете — ищите информацию, разбирайтесь… в итоге существенно повысите свой уровень

так что будет и практично (для вас) и полезно (другим людям, которым вы сможете помочь)
———-

Rol1k дал глупый совет
гостевая книга от форума в принципе ничем не отличается — там тоже нужно на сервере сохранять сообщения пользователей
(замечу в скобках, что чат — он тоже требует сохранения сообщений на сервере)

Понравилась статья? Поделить с друзьями:
  • Как написать сайт на пайтоне
  • Как написать сайт на пайтон
  • Как написать сайт на маке
  • Как написать сайт на джанго питон
  • Как написать сайт на джава скрипт