Advertisement
  1. Code
  2. PHP

Создание веб-приложения CodeIgniter с нуля

Scroll to top
Read Time: 16 min

Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

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

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


Настройка базы данных

Во-первых, для данных приложения мы собираемся использовать MongoDB в качестве сервера базы данных. MongoDB - это документно-ориентированная база данных и самая популярная база данных NoSQL. Она хорошо масштабируется и быстрая, что позволяет управлять огромными объемами данных.

MongoDB - это документально ориентированная база данных и ведущая база данных NoSQL.

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

Обратите внимание, что объяснение драйверов в CodeIgniter и т.д. выходит за рамки данной статьи, обратитесь к документации, если у вас есть какие-либо сомнения. Вам просто нужно переместить "mongo_db.php" из папки "config" в папку "config"  вашего приложения и папку "Mongo_db" из папки "libraries" в папку "libraries" в вашем приложении.

Настройка базы данных

Единственным файлом, который нам нужно отредактировать на данный момент, является "mongo_db.php" файл в папке "config", так как моя установка mongo имеет все параметры по умолчанию, я просто отредактирую строку 40 и оставлю имя базы данных, которую я хочу использовать:

1
$config['mongo_db'] = 'billboard';

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


Глобальная конфигурация

Помимо обычных параметров конфигурации, которые должны включать base_url и index_page, если таковые имеются, нам нужно установить string и date хелперы для автозагрузки. Я не буду здесь демонстрировать, как это делается, так как у нас впереди еще много всего, что нужно рассмотреть. Если у вас есть какие-либо сомнения, то лучше обратитесь к документации.

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


Обработка URL-адресов

Это будет RESTful  сервис и нам нужен способ принимать запросы, поступающие на сервер, и обработать их соответствующим образом. Мы могли бы использовать существующую библиотеку (кстати, это классная), но для целей этой статьи я собираюсь создать необходимые функции, используя основные возможности CodeIgniter.

Обработка RESTful запросов

В частности, мы будем использовать возможность расширения core классов. Мы начнем с контроллера, для основной части этого расширения мы используем метод "_remap" в базовом контроллере, чтобы все контроллеры нашего приложения могли его использовать. Начните с создания файла MY_Controller.php внутри папки "core" в папке "application", мы создаем его, как и любой другой контроллер CodeIgniter, следующим образом:

1
<?php
2
if( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );
3
4
class MY_Controller extends CI_Controller {
5
6
}

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

1
public function _remap( $param ) {
2
    $request = $_SERVER['REQUEST_METHOD'];
3
4
    switch( strtoupper( $request ) ) {
5
        case 'GET':
6
            $method = 'read';
7
            break;
8
        case 'POST':
9
            $method = 'save';
10
            break;
11
        case 'PUT':
12
            $method = 'update';
13
            break;
14
        case 'DELETE':
15
            $method = 'remove';
16
            break;
17
        case 'OPTIONS':
18
            $method = '_options';
19
            break;
20
    }
21
22
    $this->$method( $id );
23
}

Несколько вещей, которые следует здесь отметить: во-первых, есть некоторые глаголы REST, которые мы игнорируем (например, PATCH), так как я демонстрирую создание приложения REST, я не хочу добавлять вещи, которые могут сделать его еще более сложным, чем оно должно быть. Во-вторых, мы не учитываем тот случай, когда контроллер не реализует определенный метод, что вполне вероятно может произойти. Теперь мы можем добавить метод по умолчанию для обработки таких запросов, но чтобы не добавлять слишком много сложностей, давайте оставим его так. В-третьих, мы получаем переменную param в объявлении метода, давайте обратимся к ней, а затем я объясню запрос OPTIONS. Выше оператора switch добавьте следующий код:

1
if ( preg_match( "/^(?=.*[a-zA-Z])(?=.*[0-9])/", $param ) ) {
2
    $id = $param;
3
} else {
4
    $id = null;
5
}

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

Запрос OPTIONS

Поскольку мы создаем веб-сервис и клиентское приложение как отдельные части, имеет смысл, что оба они будут размещаться в разных доменах, поэтому мы включим CORS на back-end, а это означает, среди прочего, что наше приложение будет правильно отвечать на запросы OPTIONS.

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

Поскольку наша внутренняя служба, независимо от того, какой контроллер вызывается, собирается получить этот запрос OPTIONS, имеет смысл реализовать метод ответа на него в нашем базовом контроллере. Добавьте следующий метод ниже (или выше) метода _remap в нашем контроллере.

1
private function _options() {
2
    $this->output->set_header( 'Access-Control-Allow-Origin: *' );
3
    $this->output->set_header( "Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS" );
4
    $this->output->set_header( 'Access-Control-Allow-Headers: content-type' );
5
    $this->output->set_content_type( 'application/json' );
6
    $this->output->set_output( "*" );
7
}

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


Управление выводом

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

1
protected function _format_output( $output = null ) {
2
    $this->output->set_header( 'Access-Control-Allow-Origin: *' );
3
4
    if( isset( $output->status ) && $output->status == 'error' ) {
5
        $this->output->set_status_header( 409, $output->desc );
6
    }
7
    $this->_parse_data( $output );
8
9
    $this->output->set_content_type( 'application/json' );
10
    $this->output->set_output( json_encode( $output ) );
11
}

Опять же, чтобы BackboneJS обрабатывал ответ сервера, он должен знать, что его хост принят сервером, следуя заголовку Allow-Origin, а затем, если результат является невалидным, то мы устанавливаем заголовок состояния, указывающий на это. Этот status станет более понятным, когда мы будем создавать модели на бэкенде. Затем мы используем хелпер parse_data, который будет частным методом (который мы скоро напишем), затем мы устанавливаем тип содержимого как JSON и, наконец, мы конвертируем ответ в объект JSON , Опять же, здесь мы могли (и должны) поддерживать другие выходные форматы (например, XML).

Теперь давайте создадим вспомогательный метод parse_data (и я объясню его позже), добавьте следующий код в базовый контроллер:

1
private function _parse_data( &$data ) {
2
    if ( ! is_array( $data ) && ! is_object( $data ) )
3
        return $data;
4
5
    foreach ( $data as $key => $value ) {
6
        if ( is_object( $value ) || is_array( $value ) ) {
7
            if( is_object( $data ) ) {
8
                $data->{$key} = $this->_parse_data( $value );
9
            } else {
10
                $data[ $key ] = $this->_parse_data( $value );
11
            }
12
        }
13
14
        if ( isset( $value->sec ) ) {
15
            if( is_object( $data ) ) {
16
                $data->{$key} = date( 'd.m.Y', $value->sec );
17
            } else {
18
                $data[ $key ] = date( 'd.m.Y', $value->sec );
19
            }
20
        }
21
22
        if ( is_object( $value ) && isset( $value->{'$id'} ) ) {
23
            if( is_object( $data ) ) {
24
                $data->{$key} = $value->__toString();
25
            } else {
26
                $data[ $key ] = $value->__toString();
27
            }
28
        }
29
    }
30
31
    return $data;
32
}

Во-первых, обратите внимание, что мы анализируем данные только для массивов и объектов, и мы делаем это рекурсивно. Этот предварительный анализ имеет отношение к тому факту, что MongoDB использует даты и идентификаторы как объекты, но нашим клиентам эта информация не нужна. Теперь для случая идентификаторов нам просто нужно его строковое значение, поэтому для свойства '$id' мы вызываем метод toString. Впоследствии мы преобразуем даты в формат day.month.year, это делается для удобства в дизайне клиентского приложения, опять же, не самый гибкий подход, но он работает для этого примера.


Обработка ввода

Поскольку мы отправляем JSON обратно в клиентское приложение, логично, что мы также принимаем данные в формате JSON. CodeIgniter не поддерживает это по умолчанию, как например Laravel, на самом деле CodeIgniter даже не поддерживает put и delete параметры. Это связано главным образом с тем, что фреймворк не предназначен для создания RESTful сервиса, однако усилия, которые необходимо предпринять для его адаптации, минимальны по сравнению с получаемыми преимуществами, по крайней мере, с моей точки зрения.

Поэтому мы начнем с поддержки данных JSON, которые посылает BackboneJS. Создайте новый файл внутри папки "core", на этот раз он будет называться "MY_Input.php" и он будет иметь следующую основную структуру:

1
<?php
2
if( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );
3
4
class MY_Input extends CI_Input {
5
6
}

Теперь каждый раз, когда мы используем $this->input в нашем приложении, мы будем ссылаться на этот класс, мы создадим несколько новых методов и переопределим несколько существующих. Прежде всего, мы добавим поддержку данных JSON, добавим следующий метод в новый класс.

1
public function json() {
2
    if ( !self::$request_params ) {
3
        $payload    = file_get_contents( 'php://input' );
4
5
        if ( is_array( $payload ) ) {
6
            self::$request_params   = $payload;
7
        } else if ( ( substr( $payload, 0, 1 ) == "{" ) && ( substr( $payload, ( strlen( $payload ) - 1 ), 1 ) == "}" ) ) {
8
            self::$request_params   = json_decode( $payload );
9
        } else {
10
            parse_str( $payload, self::$request_params );
11
        }
12
    }
13
14
    return (object) self::$request_params;
15
}

$request_params - статическая переменная, используемая для хранения строки запроса/данных, отправленных клиентом. Она статична, чтобы сделать объект независимым, чтобы мы могли получить доступ к нему с любого контроллера в любой момент времени. Данные получены из php://input, а не из глобальной переменной $_POST. Это делается для получения данных, отправленных через запросы PUT и DELETE. Наконец, полученный пейлоад проверяется, является ли массивом, объектом, закодированным JSON, или строкой запроса, и обрабатывается соответствующим образом. Затем результат возвращается как объект.

Чтобы этот метод работал, нам нужно создать статическую переменную $request_params и добавить ее объявление в начало класса.

1
private static $request_params  = null;

Обработка регулярных запросов

Затем нам нужно переопределить метод post обычного класса ввода и использовать новый JSON место глобальной переменной $ _POST, добавим следующий метод в новый класс ввода.

1
public function post( $index = NULL, $xss_clean = FALSE ) {
2
    $request_vars   = ( array ) $this->json();
3
    if ( $index === NULL && !empty( $request_vars ) ) {
4
        $post       = array();
5
        foreach( array_keys( $request_vars ) as $key ) {
6
            $post[$key]  = $this->_fetch_from_array( $request_vars, $key, $xss_clean );
7
        }
8
        return $post;
9
    }
10
    return $this->_fetch_from_array( $request_vars, $index, $xss_clean );
11
}

Это почти то же самое, что и метод post из исходного класса CI_Input, с той лишь разницей, что он использует наш новый метод JSON вместо глобальной переменной $_POST для получения данных из post запроса. Теперь давайте сделаем то же самое для метода PUT.

1
public function put( $index = NULL, $xss_clean = FALSE ) {
2
    $request_vars   = ( array ) $this->json();
3
    if ( $index === NULL && !empty( $request_vars ) ) {
4
        $put = array();
5
        foreach( array_keys( $request_vars ) as $key ) {
6
            $put[$key]   = $this->_fetch_from_array( $request_vars, $key, $xss_clean );
7
        }
8
        return $put;
9
    }
10
    return $this->_fetch_from_array( $request_vars, $index, $xss_clean );
11
}

А затем нам также нужен метод DELETE:

1
public function delete( $index = NULL, $xss_clean = FALSE ) {
2
    $request_vars   = ( array ) $this->json();
3
    if ( $index === NULL && !empty( $request_vars ) ) {
4
        $delete = array();
5
        foreach( array_keys( $request_vars ) as $key ) {
6
            $delete[$key]   = $this->_fetch_from_array( $request_vars, $key, $xss_clean );
7
        }
8
        return $delete;
9
    }
10
    return $this->_fetch_from_array( $request_vars, $index, $xss_clean );
11
}

Теперь технически нет необходимости в дополнительных методах, поскольку метод post может обрабатывать параметры в запросах PUT и DELETE, но семантически так лучше (на мой взгляд).

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


Базовая модель

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

1
<?php
2
if( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );
3
4
class MY_Model extends CI_Model {
5
6
}

Эта базовая модель будет служить только для установки и получения ошибок. Добавьте следующий метод для установки ошибки модели:

1
protected function _set_error( $desc, $data = null ) {
2
    $this->_error           = new stdClass();
3
    $this->_error->status   = 'error';
4
    $this->_error->desc     = $desc;
5
    if ( isset( $data ) ) {
6
        $this->_error->data = $data;
7
    }
8
}

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

1
protected $_error;

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

1
public function get_error() {
2
    return $this->_error;
3
}

Обработка сеансов

Контроллер сеанса

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

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

Ниже вы найдете структуру для нашего контроллера Session:

1
<?php
2
if ( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );
3
4
class Session extends MY_Controller {
5
6
    public function __construct() {}
7
8
    public function save() {}
9
10
    public function remove( $id = null ) {}
11
}

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

1
public function __construct() {
2
    parent::__construct();
3
4
    $this->load->model( 'session_model', 'model' );
5
}

Этот простой конструктор просто вызывает свой родительский конструктор (как и каждый контроллер в CodeIgniter), а затем загружает модель контроллера. Код для метода save выглядит следующим образом.

1
public function save() {
2
    $result = $this->model->create();
3
    if ( !$result ) {
4
        $result = $this->model->get_error();
5
    }
6
    $this->_format_output( $result );
7
}

А вот код для метода remove:

1
public function remove( $id = null ) {
2
    $result = $this->model->destroy( $id );
3
    if ( !$result ) {
4
        $result = $this->model->get_error();
5
    }
6
    $this->_format_output( $result );
7
}

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

Модель сеанса

Теперь перейдем к модели сеанса. Вот ее основная структура:

1
<?php
2
if ( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );
3
4
class Session_Model extends MY_Model {
5
6
    public function __construct() {}
7
8
    public function create() {}
9
10
    public function destroy( $id ) {}
11
}

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

1
public function __construct() {
2
    $this->load->driver( 'mongo_db' );
3
}

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

1
public function destroy( $id ) {
2
    $filters    = array( '_id' => $this->mongo_db->gen_id( $id ) );
3
4
    $query     = $this->mongo_db->get_where( 'sessions', $filters );
5
    if ( $query->num_rows() == 0 ) {
6
        $this->_set_error( 'INVALID_CREDENTIALS' );
7
        return false;
8
    }
9
10
    $this->mongo_db->remove( 'sessions', $filters );
11
    return 'SESSION_TERMINATED';
12
}

В этом методе мы проверяем, есть ли сеанс для данного session_id, и если так, то мы попытаемся удалить его, отправив сообщение об успешном завершении, если все будет хорошо, или установим ошибку и вернем false, если что-то пойдет не так. Обратите внимание, что при использовании session_id мы используем специальный метод $this->mongo_db->gen_id, так как, как я уже упоминал ранее, идентификаторы в MongoDB являются объектами, поэтому мы используем строку id для его создания.

Наконец, давайте напишем метод create, который завершит первую часть этой серии уроков.

1
public function create() {
2
    $query      = $this->mongo_db->get_where( 'users', array( 'email' => $this->input->post( 'email' ) ) );
3
    if ( $query->num_rows() != 1 ) {
4
        $this->_set_error( 'INVALID_CREDENTIALS' );
5
        return false;
6
    }
7
8
    $this->load->library( 'encrypt' );
9
    $user   = $query->row();
10
    $salt   = $this->encrypt->decode( $user->salt );
11
    if ( $user->pass != sha1( $this->input->post( 'pass' ) . $salt ) ) {
12
        $this->_set_error( 'INVALID_CREDENTIALS' );
13
        return false;
14
    }
15
16
    $this->mongo_db->remove( 'sessions', array( 'user_id' => $user->_id->__toString() ) );
17
18
    $session    = array(
19
        'timestamp'     => now(),
20
        'user_id'       => $user->_id->__toString(),
21
        'persistent'    => $this->input->post( 'persistent' )
22
    );
23
24
    if ( !$this->mongo_db->insert( 'sessions', $session ) ) {
25
        $this->_set_error( 'ERROR_REGISTERING_SESSION' );
26
        return false;
27
    }
28
29
    $result                 = new stdClass();
30
    $result->id             = $this->mongo_db->insert_id();
31
    $result->user_id        = $user->_id->__toString();
32
33
    return $result;
34
}

Прежде всего, мы проверяем, есть ли пользователь, связанный с данным email. Затем мы декодируем связанную с пользователем salt (которую я объясню во второй части этой серии, когда мы рассмотрим регистрацию пользователя) и проверяем, что данный пароль соответствует сохраненному паролю пользователя.

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


Заключение

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

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.