Статьи

Сборка Ribbit в PHP

В первой записи этой серии мы позаботились об аспекте пользовательского интерфейса нашего клона Twitter, который называется Ribbit. Теперь мы начнем кодировать приложение на нескольких языках. Этот урок будет использовать стандартный PHP (с доморощенным MVC), но в следующих статьях мы рассмотрим другие реализации, такие как Rails или Laravel.

Есть много чего, так что давайте начнем.



Ribbit

Для незнакомых, MVC расшифровывается как Model-View-Controller. Вы можете использовать MVC как Database-HTML-Logic Code. Разделение вашего кода на эти отдельные части облегчает замену одного или нескольких компонентов, не мешая остальной части вашего приложения. Как вы увидите ниже, этот уровень абстракции также побуждает вас писать небольшие, краткие функции, которые полагаются на функции более низкого уровня.

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


Нам нужны четыре таблицы для этого приложения. Они есть:

  • Users — содержит информацию о пользователе.
  • Ribbits — содержит актуальные риббиты (посты).
  • Follows — список тех, кто следует за кем.
  • UserAuth — таблица для проведения аутентификаций при входе

Я покажу вам, как создавать эти таблицы из терминала. Если вы используете программу администратора (например, phpMyAdmin), то вы можете либо нажать кнопку SQL для непосредственного ввода команд, либо добавить таблицы через графический интерфейс.

Для начала откройте окно терминала и введите следующую команду:

1
mysql -u username -h hostAddress -P portNumber -p

Если вы запускаете эту команду на компьютере с MySQL, а номер порта не был изменен, вы можете опустить -h
и -P аргументы. По умолчанию используется localhost и порт 3306 соответственно. После входа в систему вы можете создать базу данных, используя следующий SQL:

1
2
CREATE DATABASE Ribbit;
USE Ribbit;

Давайте начнем с создания таблицы Users :

01
02
03
04
05
06
07
08
09
10
CREATE TABLE Users (
    id INT NOT NULL AUTO_INCREMENT,
    username VARCHAR(18) NOT NULL,
    name VARCHAR(36),
    password VARCHAR(64),
    created_at DATETIME,
    email TEXT,
    gravatar_hash VARCHAR(32),
    PRIMARY KEY(id, username)
);

Это дает нам следующую таблицу:


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

Следующая таблица, которую я хочу создать, это таблица Ribbits . Эта таблица должна иметь четыре поля: id , user_id , ribbit и ribbit . Код SQL для этой таблицы:

1
2
3
4
5
6
7
CREATE TABLE Ribbits (
    id INT NOT NULL AUTO_INCREMENT,
    user_id INT NOT NULL,
    ribbit VARCHAR(140),
    created_at DATETIME,
    PRIMARY KEY(id, user_id)
);

Стол Риббитс

Это довольно простая вещь, поэтому я не буду вдаваться в подробности.

Далее таблица « Follows . Это просто содержит id как подписчика, так и подписчика:

1
2
3
4
5
6
CREATE Table Follows (
    id INT NOT NULL AUTO_INCREMENT,
    user_id INT NOT NULL,
    followee_id INT,
    PRIMARY KEY(id, user_id)
);

Следует за таблицей

Наконец, у нас есть таблица с именем UserAuth . Он содержит имя пользователя и хэш пароля. Я решил не использовать идентификатор пользователя, потому что программа уже сохраняет имя пользователя при входе в систему и регистрации (два раза, когда записи добавляются в эту таблицу), но программе потребуется сделать дополнительный вызов, чтобы получить пользователя Идентификационный номер. Дополнительные вызовы означают большую задержку, поэтому я решил не использовать идентификатор пользователя.

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

Вот код SQL:

1
2
3
4
5
6
CREATE TABLE UserAuth (
    id INT NOT NULL AUTO_INCREMENT,
    hash VARCHAR(52) NOT NULL,
    username VARCHAR(18),
    PRIMARY KEY(id, hash)
);

И эта финальная таблица выглядит следующим образом:


Таблица UserAuth

Теперь, когда у нас есть все настройки таблиц, у вас должно быть довольно хорошее представление о том, как будет работать весь сайт. Мы можем начать писать класс Model в нашей среде MVC.


Создайте файл с именем model.php и введите следующее объявление класса:

1
2
3
4
5
6
7
8
class Model{
     
    private $db;
     
    function __construct(){
        $this->db = new mysqli(‘localhost’, ‘user’, ‘pass’, ‘Ribbit’);
    }
}

Это выглядит знакомо вам, если вы писали классы PHP в прошлом. Этот код в основном создает класс с именем Model . У него есть одно частное свойство с именем $db которое содержит объект mysqli . Внутри конструктора я инициализировал свойство $db используя информацию о подключении к моей базе данных. Порядок параметров: адрес, имя пользователя, пароль и имя базы данных.

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

Первая функция, которую я хочу реализовать, это select() . Он принимает строку для имени таблицы и массив свойств для построения WHERE . Вот вся функция, и она должна идти сразу после конструктора:

01
02
03
04
05
06
07
08
09
10
11
12
//— private function for performing standard SELECTs
private function select($table, $arr){
    $query = «SELECT * FROM » .
    $pref = » WHERE «;
    foreach($arr as $key => $value)
    {
        $query .= $pref .
        $pref = » AND «;
    }
    $query .= «;»;
    return $this->db->query($query);
}

Функция строит строку запроса, используя имя таблицы и массив свойств. Затем он возвращает объект результата, который мы получаем, передав строку запроса через функцию query() mysqli . Следующие две функции очень похожи; это функция insert() функция delete() :

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
//— private function for performing standard INSERTs
   private function insert($table, $arr)
   {
       $query = «INSERT INTO » .
       $pref = «»;
       foreach($arr as $key => $value)
       {
           $query .= $pref .
           $pref = «, «;
       }
       $query .= «) VALUES («;
       $pref = «»;
       foreach($arr as $key => $value)
       {
           $query .= $pref .
           $pref = «, «;
       }
       $query .= «);»;
       return $this->db->query($query);
   }
    
   //— private function for performing standard DELETEs
   private function delete($table, $arr){
       $query = «DELETE FROM » .
       $pref = » WHERE «;
       foreach($arr as $key => $value)
       {
           $query .= $pref .
           $pref = » AND «;
       }
       $query .= «;»;
       return $this->db->query($query);
   }

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

1
2
3
4
5
//— private function for checking if a row exists
private function exists($table, $arr){
    $res = $this->select($table, $arr);
    return ($res->num_rows > 0) ?
}

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


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

Вот завершенная структура класса; просто сохраните это в файл с именем router.php :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class Router{
    private $routes;
     
    function __construct(){
        $this->routes = array();
    }
     
    public function lookup($query)
    {
        if(array_key_exists($query, $this->routes))
        {
            return $this->routes[$query];
        }
        else
        {
            return false;
        }
    }
}

Этот класс имеет одно частное свойство, называемое routes , которое является «телефонной книгой» для наших контроллеров. Также есть простая функция lookup() , которая возвращает строку, если путь существует в свойстве routes . Чтобы сэкономить время, я перечислю десять функций, которые будет иметь наш контроллер:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function __construct(){
       $this->routes = array(
           «home» => «indexPage»,
           «signup» => «signUp»,
           «login» => «login»,
           «buddies» => «buddies»,
           «ribbit» => «newRibbit»,
           «logout» => «logout»,
           «public» => «publicPage»,
           «profiles» => «profiles»,
           «unfollow» => «unfollow»,
           «follow» => «follow»
       );
   }

Список идет в формате 'url' => 'function name' . Например, если кто-то заходит на ribbit.com/home , то маршрутизатор говорит контроллеру выполнить indexPage() .

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

1
2
3
4
RewriteEngine On
RewriteRule ^/?Resource/(.*)$ /$1 [L]
RewriteRule ^$ /home [redirect]
RewriteRule ^([a-zA-Z]+)/?([a-zA-Z0-9/]*)$ /app.php?page=$1&query=$2 [L]

Это может показаться немного пугающим, если вы никогда не использовали apache mod_rewrite. Но не волнуйся; Я проведу вас через это построчно.

В инфраструктуре MVC все HTTP-запросы обычно направляются на один контроллер.

Первая строка сообщает Apache о включении mod_rewrite; остальные строки — это правила перезаписи. С помощью mod_rewrite вы можете принять входящий запрос с определенным URL-адресом и передать запрос в другой файл. В нашем случае мы хотим, чтобы все запросы обрабатывались одним файлом, чтобы мы могли обрабатывать их с помощью контроллера. Модуль mod_rewrite также позволяет нам иметь URL-адреса, такие как ribbit.com/profile/username вместо ribbit.com/profile.php?username=username делая общее впечатление от вашего приложения более профессиональным.

Я сказал, что мы хотим, чтобы все запросы передавались в один файл, но это на самом деле не точно. Мы хотим, чтобы Apache обычно обрабатывал запросы на ресурсы, такие как изображения, CSS-файлы и т. Д. Первое правило перезаписи говорит Apache обрабатывать запросы, которые начинаются с Resource/ обычным способом. Это регулярное выражение, которое берет все после слова Resource/ (обратите внимание на скобки для группировки) и использует его как реальный URL-адрес файла. Например, ссылка ribbit.com/Resource/css/main.css загружает файл, расположенный по адресу ribbit.com/css/main.css .

Следующее правило указывает Apache перенаправлять пустые запросы (то есть запросы к корню веб-сайтов) в /home .

Слово «перенаправление» в квадратных скобках в конце строки указывает Apache фактически перенаправить браузер, в отличие от перезаписи URL-адреса на другой (как в предыдущем правиле).

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

Последнее правило — это то, к которому мы пришли; он принимает все запросы (кроме тех, которые начинаются с Resource/ ) и отправляет их в файл PHP с именем app.php . Это файл, который загружает контроллер и запускает все приложение.

Символ « ^ » представляет начало строки, а « $ » — конец. Таким образом, регулярное выражение можно перевести на английский как: «Возьмите все от начала URL до первого слеша и поместите его в группу 1. Затем возьмите все после слеша и поместите в группу 2. Наконец, передайте ссылка на Apache, как если бы он сказал app.php?page=group1&query=group2 . « « [L] » в первой и третьей строке указывает Apache остановиться после этой строки. Поэтому, если запрос является URL-адресом ресурса, он не должен переходить к следующему правилу; это должно сломаться после первого.

Я надеюсь, что все это имело смысл; следующая картина лучше иллюстрирует происходящее.

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

Теперь, когда у нас все настроено по URL, давайте создадим контроллер.


Контроллер — это место, где происходит большая часть магии; все остальные части приложения, включая модель и маршрутизатор, соединяются здесь. Давайте начнем с создания файла с именем controller.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
require(«model.php»);
require(«router.php»);
 
class Controller{
     
    private $model;
    private $router;
     
    //Constructor
    function __construct(){
        //initialize private variables
        $this->model = new Model();
        $this->router = new Router();
         
        //Proccess Query String
        $queryParams = false;
        if(strlen($_GET[‘query’]) > 0)
        {
            $queryParams = explode(«/», $_GET[‘query’]);
        }
         
        $page = $_GET[‘page’];
         
        //Handle Page Load
        $endpoint = $this->router->lookup($page);
        if($endpoint === false)
        {
            header(«HTTP/1.0 404 Not Found»);
        }
        else
        {
            $this->$endpoint($queryParams);
             
        }
    }

С помощью mod_rewrite вы можете принять входящий запрос с определенным URL-адресом и передать запрос в другой файл.

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

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

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

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

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

1
2
3
private function redirect($url){
    header(«Location: /» . $url);
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
private function loadView($view, $data = null){
    if (is_array($data))
    {
        extract($data);
    }
 
    require(«Views/» . $view . «.php»);
}
 
private function loadPage($user, $view, $data = null, $flash = false){
    $this->loadView(«header», array(‘User’ => $user));
    if ($flash !== false)
    {
        $flash->display();
    }
 
    $this->loadView($view, $data);
    $this->loadView(«footer»);
}

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

Есть одна последняя функция, которую нам нужно реализовать, которая требуется на всех страницах: checkAuth() . Эта функция проверяет, вошел ли пользователь в систему, и, если да, передает данные пользователя на страницу. В противном случае возвращается false. Вот функция:

01
02
03
04
05
06
07
08
09
10
private function checkAuth(){
    if(isset($_COOKIE[‘Auth’]))
    {
        return $this->model->userForAuth($_COOKIE[‘Auth’]);
    }
    else
    {
        return false;
    }
}

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

Теперь давайте реализуем эту функцию в классе модели.


В классе Model сразу после функции exists() добавьте следующую функцию:

01
02
03
04
05
06
07
08
09
10
11
12
13
public function userForAuth($hash){
    $query = «SELECT Users.* FROM Users JOIN (SELECT username FROM UserAuth WHERE hash = ‘»;
    $query .= $hash .
    $res = $this->db->query($query);
    if($res->num_rows > 0)
    {
        return $res->fetch_object();
    }
    else
    {
        return false;
    }
}

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

Это все, что мы должны сделать в этом классе на данный момент. Давайте вернемся в файл controller.php и реализуем класс Flash .

В функции loadPage() была возможность передать flash объект, сообщение, которое появляется над всем содержимым.

Например: если не прошедший проверку подлинности пользователь пытается что-то опубликовать, приложение отображает сообщение, похожее на «Вы должны войти в систему, чтобы выполнить это действие». Существуют различные виды вспышек: ошибки, предупреждения и уведомления, и я решил, что проще создать класс Flash а не передавать несколько переменных (таких как msg и type . Кроме того, у класса будет возможность выводить HTML-код флэш-памяти.

Вот полный класс Flash , вы можете добавить его в controller.php до определения класса Controller :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class Flash{
     
    public $msg;
    public $type;
     
    function __construct($msg, $type)
    {
        $this->msg = $msg;
        $this->type = $type;
    }
     
    public function display(){
        echo «<div class=\»flash » . $this->type . «\»>» .
    }
}

Этот класс прост. Он имеет два свойства и функцию для вывода HTML флеш-памяти.

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

1
2
3
<?php
   require(«controller.php»);
   $app = new Controller();

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


Создайте папку в корне вашего сайта под названием Views . Как вы, возможно, уже догадались, этот каталог будет содержать все фактические представления. Если вы не знакомы с концепцией представления, вы можете думать о них как о файлах, которые генерируют фрагменты HTML, которые создают страницу. По сути, у нас будет представление для верхнего и нижнего колонтитула и по одному для каждой страницы. Эти части объединяются в конечный результат (то есть header + page_view + footer = final_page).

Давайте начнем с нижнего колонтитула; это просто стандартный HTML. Создайте файл с именем footer.php внутри папки Views и добавьте следующий HTML:

1
2
3
4
5
6
7
8
9
</div>
    </div>
    <footer>
        <div class=»wrapper»>
            Ribbit — A Twitter Clone Tutorial<img src=»http://cdn.tutsplus.com/net.tutsplus.com/authors/jeremymcpeak//Resource/gfx/logo-nettuts.png»>
        </div>
    </footer>
</body>
</html>

Я думаю, что это очень хорошо демонстрирует две вещи:

  • Это просто кусочки реальной страницы.
  • Чтобы получить доступ к изображениям, находящимся в папке gfx , я добавил Resources/ в начало пути (для правила mod_rewrite).

Далее, давайте создадим файл 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
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html>
    <head>
        <link rel=»stylesheet/less» href=»/Resource/style.less»>
        <script src=»/Resource/less.js»></script>
    </head>
    <body>
        <header>
            <div class=»wrapper»>
                <img src=»http://cdn.tutsplus.com/net.tutsplus.com/authors/jeremymcpeak//Resource/gfx/logo.png»>
                <span>Twitter Clone
                <?php if($User !== false){ ?>
                    <nav>
                        <a href=»/buddies»>Your Buddies</a>
                        <a href=»/public»>Public Ribbits</a>
                        <a href=»/profiles»>Profiles</a>
                    </nav>
                    <form action=»/logout» method=»get»>
                        <input type=»submit» id=»btnLogOut» value=»Log Out»>
                    </form>
                <?php }else{ ?>
                    <form method=»post» action=»/login»>
                        <input name=»username» type=»text» placeholder=»username»>
                        <input name=»password» type=»password» placeholder=»password»>
                        <input type=»submit» id=»btnLogIn» value=»Log In»>
                    </form>
                <?php } ?>
            </div>
        </header>
        <div id=»content»>
            <div class=»wrapper»>

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

Последнее представление для домашней страницы — это фактическое представление home.php . Это представление содержит изображение приветствия и форму регистрации. Вот код для home.php :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<img src=»http://cdn.tutsplus.com/net.tutsplus.com/authors/jeremymcpeak//Resource/gfx/frog.jpg»>
<div class=»panel right»>
  <h1>New to Ribbit?</h1>
  <p>
  <form action=»/signup» method=»post»>
    <input name=»email» type=»text» placeholder=»Email»>
    <input name=»username» type=»text» placeholder=»Username»>
    <input name=»name» type=»text» placeholder=»Full Name»>
    <input name=»password» type=»password» placeholder=»Password»>
    <input name=»password2″ type=»password» placeholder=»Confirm Password»>
    <input type=»submit» value=»Create Account»>
  </form>
  </p>
</div>

Вместе эти три представления завершают домашнюю страницу. Теперь давайте напишем функцию для домашней страницы.


Нам нужно написать функцию в классе Controller именем indexPage() для загрузки домашней страницы (это то, что мы настроили в классе маршрутизатора). Следующая завершенная функция должна идти в классе Controller после функции checkAuth() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private function indexPage($params){
  $user = $this->checkAuth();
  if($user !== false) { $this->redirect(«buddies»);
  else
  {
    $flash = false;
    if($params !== false)
    {
      $flashArr = array(
        «0» => new Flash(«Your Username and/or Password was incorrect.», «error»),
        «1» => new Flash(«There’s already a user with that email address.», «error»),
        «2» => new Flash(«That username has already been taken.», «error»),
        «3» => new Flash(«Passwords don’t match.», «error»),
        «4» => new Flash(«Your Password must be at least 6 characters long.», «error»),
        «5» => new Flash(«You must enter a valid Email address.», «error»),
        «6» => new Flash(«You must enter a username.», «error»),
        «7» => new Flash(«You have to be signed in to acces that page.», «warning»)
      );
      $flash = $flashArr[$params[0]];
    }
    $this->loadPage($user, «home», array(), $flash);
  }
}

Первые две строки проверяют, вошел ли пользователь в систему. Если это так, функция перенаправляет пользователя на страницу «друзей», где он может читать сообщения своих друзей и просматривать свой профиль. Если пользователь не вошел в систему, он продолжает загружать домашнюю страницу, проверяя, есть ли какие-либо вспышки для отображения. Так, например, если пользователь переходит на ribbit.com/home/0 , то эта функция показывает первую ошибку и так далее в течение следующих семи вспышек. После этого мы вызываем loadPage() для отображения всего на экране.

На этом этапе, если у вас все настроено правильно (например, Apache и наш код), вы сможете перейти в корень своего сайта (например, localhost) и увидеть домашнюю страницу.

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


Следующим логическим шагом является создание функции регистрации, вы можете добавить ее сразу после indexPage() :

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
private function signUp(){
  if($_POST[’email’] == «» || strpos($_POST[’email’], «@») === false){
    $this->redirect(«home/5»);
  }
  else if($_POST[‘username’] == «»){
    $this->redirect(«home/6»);
  }
  else if(strlen($_POST[‘password’]) < 6)
  {
    $this->redirect(«home/4»);
  }
  else if($_POST[‘password’] != $_POST[‘password2’])
  {
    $this->redirect(«home/3»);
  }
  else{
    $pass = hash(‘sha256’, $_POST[‘password’]);
     
    $signupInfo = array(
      ‘username’ => $_POST[‘username’],
      ’email’ => $_POST[’email’],
      ‘password’ => $pass,
      ‘name’ => $_POST[‘name’]
    );
     
    $resp = $this->model->signupUser($signupInfo);
     
    if($resp === true)
    {
      $this->redirect(«buddies/1»);
    }
    else
    {
      $this->redirect(«home/» . $resp);
    }
  }
}

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

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

Эти проверки должны выполняться в классе Model потому что нам нужно соединение с базой данных. Вернемся к классу Model и реализуем signupUser() . Вы должны поместить это сразу после функции userForAuth() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public function signupUser($user){
  $emailCheck = $this->exists(«Users», array(«email» => $user[’email’]));
   
  if($emailCheck){
    return 1;
  }
  else {
    $userCheck = $this->exists(«Users», array(«username» => $user[‘username’]));
     
    if($userCheck){
      return 2;
    }
    else{
      $user[‘created_at’] = date( ‘Ymd H:i:s’);
      $user[‘gravatar_hash’] = md5(strtolower(trim($user[’email’])));
      $this->insert(«Users», $user);
      $this->authorizeUser($user);
      return true;
    }
  }
}

Мы используем функцию exists() для проверки предоставленного адреса электронной почты или имени пользователя, возвращая код ошибки, который уже существует. Если все прошло, то мы добавляем несколько последних полей created_at и gravatar_hash и вставляем их в базу данных.

Прежде чем вернуть true , мы авторизуем пользователя. Эта функция добавляет cookie-файл Auth и вставляет учетные данные в UserAuth данных UserAuth . Давайте теперь добавим функцию authorizeUser() :

01
02
03
04
05
06
07
08
09
10
public function authorizeUser($user){
  $chars = «qazwsxedcrfvtgbyhnujmikolp1234567890QAZWSXEDCRFVTGBYHNUJMIKOLP»;
  $hash = sha1($user[‘username’]);
  for($i = 0; $i<12; $i++)
  {
    $hash .= $chars[rand(0, 61)];
  }
  $this->insert(«UserAuth», array(«hash» => $hash, «username» => $user[‘username’]));
  setcookie(«Auth», $hash);
}

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

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

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


Чтобы завершить функции для домашней страницы, давайте реализуем функции login() и logout() . Добавьте следующее в класс Controller после функции login() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private function login(){
  $pass = hash(‘sha256’, $_POST[‘password’]);
  $loginInfo = array(
    ‘username’ => $_POST[‘username’],
    ‘password’ => $pass
  );
  if($this->model->attemptLogin($loginInfo))
  {
    $this->redirect(«buddies/0»);
  }
  else
  {
    $this->redirect(«home/0»);
  }
}

Это просто берет поля POST из формы входа и пытается войти. При успешном входе в систему пользователь переходит на страницу «друзей». В противном случае он перенаправляет обратно на домашнюю страницу, чтобы отобразить соответствующую ошибку. Далее я покажу вам функцию logout() :

1
2
3
4
private function logout() {
  $this->model->logoutUser($_COOKIE[‘Auth’]);
  $this->redirect(«home»);
}

Функция logout() даже проще, чем login() . Он выполняет одну из функций Model , чтобы стереть куки и удалить запись из базы данных.

Давайте перейдем к классу Model и добавим необходимые функции для обновления. Первый — attemptLogin() который пытается войти в систему и возвращает true или false . Тогда у нас есть logoutUser() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public function attemptLogin($userInfo){
  if($this->exists(«Users», $userInfo)){
    $this->authorizeUser($userInfo);
    return true;
  }
  else{
    return false;
  }
}
 
public function logoutUser($hash){
  $this->delete(«UserAuth», array(«hash» => $hash));
  setcookie («Auth», «», time() — 3600);
}

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

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
<div id=»createRibbit» class=»panel right»>
    <h1>Create a Ribbit</h1>
    <p>
        <form action=»/ribbit» method=»post»>
            <textarea name=»text» class=»ribbitText»></textarea>
            <input type=»submit» value=»Ribbit!»>
        </form>
    </p>
</div>
<div id=»ribbits» class=»panel left»>
    <h1>Your Ribbit Profile</h1>
    <div class=»ribbitWrapper»>
        <img class=»avatar» src=»http://www.gravatar.com/avatar/<?php echo $User->gravatar_hash; ?>»>
        <span class=»name»><?php echo $User->name;
        <p>
            <?php echo $userData->ribbit_count .
            <span class=»spacing»><?php echo $userData->followers .
            <span class=»spacing»><?php echo $userData->following .
            <?php echo $userData->ribbit;
        </p>
    </div>
</div>
<div class=»panel left»>
    <h1>Your Ribbit Buddies</h1>
    <?php foreach($fribbits as $ribbit){ ?>
            <div class=»ribbitWrapper»>
                <img class=»avatar» src=»http://www.gravatar.com/avatar/<?php echo $ribbit->gravatar_hash; ?>»>
                <span class=»name»><?php echo $ribbit->name;
                <span class=»time»>
                <?php
                    $timeSince = time() — strtotime($ribbit->created_at);
                    if($timeSince < 60)
                    {
                        echo $timeSince .
                    }
                    else if($timeSince < 3600)
                    {
                        echo floor($timeSince / 60) .
                    }
                    else if($timeSince < 86400)
                    {
                        echo floor($timeSince / 3600) .
                    }
                    else{
                        echo floor($timeSince / 86400) .
                    }
                ?>
                
                <p><?php echo $ribbit->ribbit;
            </div>
  <?php } ?>
</div>

Первый div это форма для создания новых «ribbits». Следующий div отображает информацию о профиле пользователя, а последний раздел — цикл for который отображает каждый «ribbit». Опять же, я не буду вдаваться в подробности ради времени, но все здесь довольно просто.

Теперь в классе Controller нам нужно добавить функцию buddies() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
private function buddies($params){
  $user = $this->checkAuth();
  if($user === false){ $this->redirect(«home/7»);
  else
  {
    $userData = $this->model->getUserInfo($user);
    $fribbits = $this->model->getFollowersRibbits($user);
    $flash = false;
    if(isset($params[0]))
    {
      $flashArr = array(
        «0» => new Flash(«Welcome Back, » . $user->name, «notice»),
        «1» => new Flash(«Welcome to Ribbit, Thanks for signing up.», «notice»),
        «2» => new Flash(«You have exceeded the 140 character limit for Ribbits», «error»)
      );
      $flash = $flashArr[$params[0]];
    }
    $this->loadPage($user, «buddies», array(‘User’ => $user, «userData» => $userData, «fribbits» => $fribbits), $flash);
  }
}

Эта функция имеет ту же структуру, что и indexPage() : сначала мы проверяем, вошел ли пользователь в систему, и перенаправляем его на домашнюю страницу, если нет.

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

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

Теперь в классе Model мы должны ввести две функции, которые мы назвали выше. Сначала у нас есть функция getUserInfo:

01
02
03
04
05
06
07
08
09
10
public function getUserInfo($user)
{
  $query = «SELECT ribbit_count, IF(ribbit IS NULL, ‘You have no Ribbits’, ribbit) as ribbit, followers, following «;
  $query .= «FROM (SELECT COUNT(*) AS ribbit_count FROM Ribbits WHERE user_id = » . $user->id . «) AS RC «;
  $query .= «LEFT JOIN (SELECT user_id, ribbit FROM Ribbits WHERE user_id = » . $user->id . » ORDER BY created_at DESC LIMIT 1) AS R «;
  $query .= «ON R.user_id = » .
  $query .= «) AS FE JOIN (SELECT COUNT(*) AS following FROM Follows WHERE user_id = » . $user->id . «) AS FR;»;
  $res = $this->db->query($query);
  return $res->fetch_object();
}

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

Затем у нас была getFollowersRibbits() которая выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public function getFollowersRibbits($user)
{
  $query = «SELECT name, username, gravatar_hash, ribbit, Ribbits.created_at FROM Ribbits JOIN («;
  $query .= «SELECT Users.* FROM Users LEFT JOIN (SELECT followee_id FROM Follows WHERE user_id = «;
  $query .= $user->id .
  $query .= «) AS Users on user_id = Users.id ORDER BY Ribbits.created_at DESC LIMIT 10;»;
  $res = $this->db->query($query);
  $fribbits = array();
  while($row = $res->fetch_object())
  {
    array_push($fribbits, $row);
  }
  return $fribbits;
}

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

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


Этот шаг довольно прост. У нас нет возможности работать; нам просто нужна функция в классах Controller и Model . В Controller добавьте следующую функцию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
private function newRibbit($params){
  $user = $this->checkAuth();
  if($user === false){ $this->redirect(«home/7»);
  else{
    $text = mysql_real_escape_string($_POST[‘text’]);
    if(strlen($text) > 140)
    {
      $this->redirect(«buddies/2»);
    }
    else
    {
      $this->model->postRibbit($user, $text);
      $this->redirect(«buddies»);
    }
  }
}

Опять же, мы начинаем с проверки, вошел ли пользователь в систему, и, если это так, мы гарантируем, что размер поста не превышает 140 символов. Затем мы вызовем postRibbit() из модели и перенаправим обратно на страницу друзей.

Теперь в классе Model добавьте postRibbit() :

1
2
3
4
5
6
7
8
public function postRibbit($user, $text){
  $r = array(
    «ribbit» => $text,
    «created_at» => date( ‘Ymd H:i:s’),
    «user_id» => $user->id
  );
  $this->insert(«Ribbits», $r);
}

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


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

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
private function publicPage($params){
  $user = $this->checkAuth();
  }
  else
  {
    $q = false;
    if(isset($_POST['query']))
    {
      $q = $_POST['query'];
    }
    $ribbits = $this->model->getPublicRibbits($q);
    $this->loadPage($user, "public", array('ribbits' => $ribbits));
  }
}
 
private function profiles($params){
  $user = $this->checkAuth();
  }
  else{
    $q = false;
    if(isset($_POST['query']))
    {
      $q = $_POST['query'];
    }
    $profiles = $this->model->getPublicProfiles($user, $q);
    $this->loadPage($user, "profiles", array('profiles' => $profiles));
  }
}

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

Для ribbits просто создайте файл с именем public.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
<div class=»panel right»>
    <h1>Search Ribbits</h1>
    <p>
        </p><form action="/public" method="post">
            <input name=»query» type=»text»>
            <input type="submit" value="Search!">
        </form>
    <p></p>
</div>
<div id=»ribbits» class=»panel left»>
    <h1>Public Ribbits</h1>
        <?php foreach($ribbits as $ribbit){ ?>
            <div class=»ribbitWrapper»>
                <img class="avatar" src="http://www.gravatar.com/avatar/<?php echo $ribbit->gravatar_hash; ?>">
                ?>
                <span class="time">
                <?php
                    $timeSince = time() - strtotime($ribbit->created_at);
                    if($timeSince < 60)
                    {
                        echo $timeSince . "s";
                    }
                    else if($timeSince < 3600)
                    {
                        echo floor($timeSince / 60) . "m";
                    }
                    else if($timeSince < 86400)
                    {
                        echo floor($timeSince / 3600) . "h";
                    }
                    else{
                        echo floor($timeSince / 86400) . "d";
                    }
                ?>
                
                ?></p>
            </div>
        <?php } ?>
</div>

Первый div — это форма поиска ribbit, а второй div отображает общедоступные ribbits.

И вот последний вид, который является profiles.phpвидом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div class=»panel right»>
    <h1>Search for Profiles</h1>
    <p>
        </p><form action="/profiles" method="post">
            <input name=»query» type=»text»>
            <input type="submit" value="Search!">
        </form>
    <p></p>
</div>
<div id=»ribbits» class=»panel left»>
    <h1>Public Profiles</h1>
    <?php foreach($profiles as $user){ ?>
    <div class=»ribbitWrapper»>
        <img class="avatar" src="http://www.gravatar.com/avatar/<?php echo $user->gravatar_hash; ?>">
        ?>
        ?>
            <a href="<?php echo ($user->followed) ? "unfollow" : "follow"; ?>/<?php echo $user->id; ?>"><?php echo ($user->followed) ? "unfollow" : "follow"; ?></a></span>
        <p>
            ?>
        </p>
    </div>
    <?php } ?>
</div>

Это очень похоже на public.phpвид.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public function getPublicRibbits($q){
  if($q === false)
  {
    $query = "SELECT name, username, gravatar_hash, ribbit, Ribbits.created_at FROM Ribbits JOIN Users ";
    $query .= "ON user_id = Users.id ORDER BY Ribbits.created_at DESC LIMIT 10;";
  }
  else{
    $query = "SELECT name, username, gravatar_hash, ribbit, Ribbits.created_at FROM Ribbits JOIN Users ";
    $query .= "ON user_id = Users.id WHERE ribbit LIKE \"%" . $q ."%\" ORDER BY Ribbits.created_at DESC LIMIT 10;";  
  }
  $res = $this->db->query($query);
  $ribbits = array();
  while($row = $res->fetch_object())
  {
    array_push($ribbits, $row);
  }
  return $ribbits;
}

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

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
public function getPublicProfiles($user, $q){
  if($q === false)
  {
    $query = "SELECT id, name, username, gravatar_hash FROM Users WHERE id != " . $user->id;
    $query .= " ORDER BY created_at DESC LIMIT 10";
  }
  else{
    $query = "SELECT id, name, username, gravatar_hash FROM Users WHERE id != " . $user->id;
    $query .= " AND (name LIKE \"%" . $q . "%\" OR username LIKE \"%" . $q . "%\") ORDER BY created_at DESC LIMIT 10";
  }
  $userRes = $this->db->query($query);
  if($userRes->num_rows > 0){
    $userArr = array();
    $query = "";
    while($row = $userRes->fetch_assoc()){
      $i = $row['id'];
      $i .
      $query .= "AS ribbit, followed FROM (SELECT COUNT(*) as followers FROM Follows WHERE followee_id = " . $i . ") ";
      $query .= "AS F LEFT JOIN (SELECT user_id, ribbit FROM Ribbits WHERE user_id = " . $i;
      $i .
      $i .
      $userArr[$i] = $row;
    }
    $this->db->multi_query($query);
    $profiles = array();
    do{
      $row = $this->db->store_result()->fetch_object();
      $i = $row->id;
      $userArr[$i]['followers'] = $row->followers;
      $userArr[$i]['followed'] = $row->followed;
      $userArr[$i]['ribbit'] = $row->ribbit;
      array_push($profiles, (object)$userArr[$i]);
    }while($this->db->next_result());
    return $profiles;
  }
  else
  {
    return null;
  }
}

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

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

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

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


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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
private function follow($params){
  $user = $this->checkAuth();
  }
  else{
    $this->model->follow($user, $params[0]);
    $this->redirect("profiles");
  }
}
 
private function unfollow($params){
  $user = $this->checkAuth();
  }
  else{
    $this->model->unfollow($user, $params[0]);
    $this->redirect("profiles");
  }
}

Эти функции, как вы, вероятно, видите, практически идентичны. Единственное отличие состоит в том, что один добавляет запись в Followsтаблицу, а другой удаляет запись. Теперь давайте закончим с функциями в Modelклассе:

1
2
3
4
5
6
7
public function follow($user, $fId){
  $this->insert("Follows", array("user_id" => $user->id, "followee_id" => $fId));
}
 
public function unfollow($user, $fId){
  $this->delete("Follows", array("user_id" => $user->id, "followee_id" => $fId));
}

Эти функции в основном одинаковы; они отличаются только методами, которые они вызывают.

Сайт теперь полностью готов к работе !!! Последнее, что я хочу добавить, это еще один .htaccessфайл в Viewsпапке. Вот его содержание:

1
2
Order allow,deny
Deny from all

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


Мы определенно создали клон Twitter с нуля!

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

Обратите внимание, что из-за ограничений по длине мне пришлось опустить множество функций, которые вы можете найти в реальном производственном приложении, таких как Ajax, защита от SQL-инъекций и счетчик символов для поля Ribbit (вероятно, много и другие вещи). Тем не менее, в целом, я думаю, что мы достигли многого!

Я надеюсь, что вам понравилась эта статья, не стесняйтесь оставить мне комментарий, если у вас есть какие-либо мысли или вопросы. Спасибо за чтение!