Статьи

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

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

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


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

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

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

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

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


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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function _remap( $param ) {
    $request = $_SERVER[‘REQUEST_METHOD’];
 
    switch( strtoupper( $request ) ) {
        case ‘GET’:
            $method = ‘read’;
            break;
        case ‘POST’:
            $method = ‘save’;
            break;
        case ‘PUT’:
            $method = ‘update’;
            break;
        case ‘DELETE’:
            $method = ‘remove’;
            break;
        case ‘OPTIONS’:
            $method = ‘_options’;
            break;
    }
 
    $this->$method( $id );
}

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

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

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

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

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

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

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

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


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

01
02
03
04
05
06
07
08
09
10
11
protected function _format_output( $output = null ) {
    $this->output->set_header( ‘Access-Control-Allow-Origin: *’ );
 
    if( isset( $output->status ) && $output->status == ‘error’ ) {
        $this->output->set_status_header( 409, $output->desc );
    }
    $this->_parse_data( $output );
 
    $this->output->set_content_type( ‘application/json’ );
    $this->output->set_output( json_encode( $output ) );
}

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

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

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
private function _parse_data( &$data ) {
    if ( ! is_array( $data ) && ! is_object( $data ) )
        return $data;
 
    foreach ( $data as $key => $value ) {
        if ( is_object( $value ) || is_array( $value ) ) {
            if( is_object( $data ) ) {
                $data->{$key} = $this->_parse_data( $value );
            } else {
                $data[ $key ] = $this->_parse_data( $value );
            }
        }
 
        if ( isset( $value->sec ) ) {
            if( is_object( $data ) ) {
                $data->{$key} = date( ‘dmY’, $value->sec );
            } else {
                $data[ $key ] = date( ‘dmY’, $value->sec );
            }
        }
 
        if ( is_object( $value ) && isset( $value->{‘$id’} ) ) {
            if( is_object( $data ) ) {
                $data->{$key} = $value->__toString();
            } else {
                $data[ $key ] = $value->__toString();
            }
        }
    }
 
    return $data;
}

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


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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public function json() {
    if ( !self::$request_params ) {
        $payload = file_get_contents( ‘php://input’ );
 
        if ( is_array( $payload ) ) {
            self::$request_params = $payload;
        } else if ( ( substr( $payload, 0, 1 ) == «{» ) && ( substr( $payload, ( strlen( $payload ) — 1 ), 1 ) == «}» ) ) {
            self::$request_params = json_decode( $payload );
        } else {
            parse_str( $payload, self::$request_params );
        }
    }
 
    return (object) self::$request_params;
}

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

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

1
private static $request_params = null;

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

01
02
03
04
05
06
07
08
09
10
11
public function post( $index = NULL, $xss_clean = FALSE ) {
    $request_vars = ( array ) $this->json();
    if ( $index === NULL && !empty( $request_vars ) ) {
        $post = array();
        foreach( array_keys( $request_vars ) as $key ) {
            $post[$key] = $this->_fetch_from_array( $request_vars, $key, $xss_clean );
        }
        return $post;
    }
    return $this->_fetch_from_array( $request_vars, $index, $xss_clean );
}

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

01
02
03
04
05
06
07
08
09
10
11
public function put( $index = NULL, $xss_clean = FALSE ) {
    $request_vars = ( array ) $this->json();
    if ( $index === NULL && !empty( $request_vars ) ) {
        $put = array();
        foreach( array_keys( $request_vars ) as $key ) {
            $put[$key] = $this->_fetch_from_array( $request_vars, $key, $xss_clean );
        }
        return $put;
    }
    return $this->_fetch_from_array( $request_vars, $index, $xss_clean );
}

И тогда нам также нужен метод DELETE :

01
02
03
04
05
06
07
08
09
10
11
public function delete( $index = NULL, $xss_clean = FALSE ) {
    $request_vars = ( array ) $this->json();
    if ( $index === NULL && !empty( $request_vars ) ) {
        $delete = array();
        foreach( array_keys( $request_vars ) as $key ) {
            $delete[$key] = $this->_fetch_from_array( $request_vars, $key, $xss_clean );
        }
        return $delete;
    }
    return $this->_fetch_from_array( $request_vars, $index, $xss_clean );
}

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

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


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

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

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

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

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

1
protected $_error;

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
<?php
if ( !defined( ‘BASEPATH’ ) ) exit( ‘No direct script access allowed’ );
 
class Session extends MY_Controller {
 
    public function __construct() {}
 
    public function save() {}
 
    public function remove( $id = null ) {}
}

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

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

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
<?php
if ( !defined( ‘BASEPATH’ ) ) exit( ‘No direct script access allowed’ );
 
class Session_Model extends MY_Model {
 
    public function __construct() {}
 
    public function create() {}
 
    public function destroy( $id ) {}
}

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
public function destroy( $id ) {
    $filters = array( ‘_id’ => $this->mongo_db->gen_id( $id ) );
 
    $query = $this->mongo_db->get_where( ‘sessions’, $filters );
    if ( $query->num_rows() == 0 ) {
        $this->_set_error( ‘INVALID_CREDENTIALS’ );
        return false;
    }
 
    $this->mongo_db->remove( ‘sessions’, $filters );
    return ‘SESSION_TERMINATED’;
}

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

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

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
public function create() {
    $query = $this->mongo_db->get_where( ‘users’, array( ’email’ => $this->input->post( ’email’ ) ) );
    if ( $query->num_rows() != 1 ) {
        $this->_set_error( ‘INVALID_CREDENTIALS’ );
        return false;
    }
 
    $this->load->library( ‘encrypt’ );
    $user = $query->row();
    $salt = $this->encrypt->decode( $user->salt );
    if ( $user->pass != sha1( $this->input->post( ‘pass’ ) . $salt ) ) {
        $this->_set_error( ‘INVALID_CREDENTIALS’ );
        return false;
    }
 
    $this->mongo_db->remove( ‘sessions’, array( ‘user_id’ => $user->_id->__toString() ) );
 
    $session = array(
        ‘timestamp’ => now(),
        ‘user_id’ => $user->_id->__toString(),
        ‘persistent’ => $this->input->post( ‘persistent’ )
    );
 
    if ( !$this->mongo_db->insert( ‘sessions’, $session ) ) {
        $this->_set_error( ‘ERROR_REGISTERING_SESSION’ );
        return false;
    }
 
    $result = new stdClass();
    $result->id = $this->mongo_db->insert_id();
    $result->user_id = $user->_id->__toString();
 
    return $result;
}

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

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


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