Статьи

Создание приложения для знакомств с Sinch: RESTful API

В этом уроке мы собираемся создать приложение для знакомств для iOS, подобное Tinder . Для передачи голоса и сообщений мы будем использовать платформу Sinch , используя ее мощный SDK.

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

Мы будем использовать Laravel 5.0 для службы RESTful и будем охватывать основные понятия, такие как маршруты и контроллеры. Мы также собираемся определить пользовательские модели для поддержки интеграции MongoDB в стиле ActiveRecord. Давайте начнем.

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

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

1
laravel new mobilesinch

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

1
php artisan fresh

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

Есть еще одна вещь, о которой мы должны позаботиться, прежде чем писать реальный код для нашего сервиса RESTful. По умолчанию Laravel 5.0 поставляется с промежуточным программным обеспечением для защиты от подделки межсайтовых запросов (CSRF) . Однако, поскольку мы создаем не веб-сайт, а API-интерфейс RESTful, не имеет смысла его использовать. Кроме того, это может вызвать некоторые проблемы на этом пути.

Для этого приложения лучше всего его удалить. В корне папки приложения перейдите в app / Http . Внутри этой папки есть файл с именем Kernel.php . Откройте его и удалите следующую строку:

1
‘App\Http\Middleware\VerifyCsrfToken’,

Вы также можете удалить WelcomeController.php , расположенный в app / Http / Controllers , а также представление по умолчанию welcome.blade.php в папке resources / views . Мы не будем их использовать, но вы можете оставить их там, если хотите. Просто убедитесь, что вы оставили представление 503.blade.php на месте, так как это полезно для отладки приложения.

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

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

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

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

Внутри корневой папки конфигурации создайте новый файл и назовите его mongodb.php . Добавьте следующий код к нему:

1
2
3
4
5
6
7
8
9
<?php
 
return [
    ‘host’ => ‘localhost’,
    ‘port’ => 27017,
    ‘user’ => »,
    ‘pass’ => »,
    ‘db’ => ‘mobilesinch’
];

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

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

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

Этот класс будет выполнять запросы к MongoDB с использованием ActiveRecord-подобного синтаксиса. Внутри приложения / Http папку, создайте новую модель и добавьте в нее файл Base.php . Добавьте следующий код к нему:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<?php namespace App\Http\Models;
 
use Illuminate\Support\Facades\Config;
 
class Base {
    
    private $_config = null;
    
    private $_conn = null;
    
    private $_db = null;
 
    public function __construct() {
        $this->_config = Config::get( ‘mongodb’ );
        
        $this->_connect();
    }
    
    private function _connect() {}
}

Это скелет для нашей Base модели. Он не расширяется ни от чего и использует только фасад Config Laravel для получения параметров конфигурации, которые мы создали ранее.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
$conn = ‘mongodb://’.$this->_config[‘host’];
if( ! empty( $this->_config[‘port’] ) ) {
    $conn .= «:{$this->_config[‘port’]}»;
}
 
$options = array();
if( ! empty( $this->_config[‘user’] ) && ! empty( $this->_config[‘pass’] ) ) {
    $options[‘username’] = $this->_config[‘user’];
    $options[‘password’] = $this->_config[‘pass’];
}
 
try {
    $this->_conn = new \MongoClient( $conn, $options );
 
    $this->_db = $this->_conn->{$this->_config[‘db’]};
    return true;
} catch( \MongoConnectionException $e ) {
    $this->_conn = null;
    return false;
}

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

Если вы знакомы с синтаксисом MongoDB из командной строки, этот метод эквивалентен входу в консоль mongo и use mobilesinch . Обратитесь к официальной документации PHP MongoDB для получения дополнительной информации.

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

1
2
3
4
5
6
7
private $_ws = array();
 
private $_sls = array();
 
private $_lmt = 99999;
 
private $_ost = 0;

Это держатели для запросов where , select , limit и offset . Чтобы установить эти переменные-члены, создайте следующие методы установки:

1
2
3
4
5
protected function _limit( $limit, $offset = null ) {}
 
protected function _select( $select = «» ) {}
 
protected function _where( $key, $value = null ) {}

Метод _limit будет полезен для разбиения на страницы результатов операции READ. Пользователь может установить параметр limit чтобы указать количество записей для извлечения и, необязательно, параметр offset чтобы указать страницу для чтения. Добавьте следующий код в метод _limit :

1
2
3
4
5
6
if ( $limit !== NULL && is_numeric( $limit ) && $limit >= 1 ) {
    $this->_lmt = $limit;
}
if ( $offset !== NULL && is_numeric( $offset ) && $offset >= 1 ) {
    $this->_ost = $offset;
}

Метод _select будет использоваться для определения того, какие поля записи должен возвращать запрос READ. Оператор select должен быть представлен в виде строки, разделенной запятыми.

1
2
3
4
$fields = explode( ‘,’, $select );
foreach ( $fields as $field ) {
    $this->_sls[trim( $field )] = true;
}

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

1
2
3
4
5
6
7
if ( is_array( $key ) ) {
    foreach( $key as $k => $v ) {
        $this->_ws[$k] = $v;
    }
} else {
    $this->_ws[$key] = $value;
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
private function _set_where( $where = null ) {
    if ( is_array( $where ) ) {
        $where = array_merge( $where, $this->_ws );
        foreach ( $where as $k => $v ) {
            if ( $k == «_id» && ( gettype( $v ) == «string» ) ) {
                $this->_ws[$k] = new \MongoId( $v );
            } else {
                $this->_ws[$k] = $v;
            }
        }
    } else if( is_string( $where ) ) {
        $wheres = explode( ‘,’, $where );
        foreach ( $wheres as $wr ) {
            $pair = explode( ‘=’, trim( $wr ) );
            if ( $pair[0] == «_id» ) {
                $this->_ws[trim( $pair[0] )] = new \MongoId( trim( $pair[1] ) );
            } else {
                $this->_ws[trim( $pair[0] )] = trim( $pair[1] );
            }
        }
    }
}

Это выглядит немного пугающе, но на самом деле все довольно просто. Сначала проверяется, является ли параметр where массивом. Если это так, он объединяет данные значения с существующими, используя вспомогательный метод _where .

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

1
name=John,last_name=Smith

В этом примере будет выполнен запрос и возвращены поля, в которых для поля name установлено значение John а для поля last_name указано значение Smith .

Обратите внимание, что для массива или строки мы проверяем наличие поля _id . Если это так, и это строка, мы создаем новый объект MongoId из него. Идентификаторы — это объекты в MongoDB, и сравнение их со строкой вернет false , поэтому это преобразование необходимо.

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

1
2
3
4
5
6
private function _flush() {
    $this->_ws = array();
    $this->_sls = array();
    $this->_lmt = 99999;
    $this->_ost = 0;
}

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

Первая операция, которую мы собираемся поддерживать, — это создание записи в системе. Добавьте следующий код в Base класс:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
protected function _insert( $collection, $data ) {
    if ( is_object( $data ) ) {
        $data = ( array ) $data;
    }
 
    $result = false;
    try {
        if ( $this->_db->{$collection}->insert( $data ) ) {
            $data[‘_id’] = ( string ) $data[‘_id’];
            $result = ( object ) $data;
        }
    } catch( \MongoCursorException $e ) {
        $result = new \stdClass();
        $result->error = $e->getMessage();
    }
    $this->_flush();
 
    return $result;
}

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

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

1
2
3
4
5
6
7
protected function _findOne( $collection, $where = array() ) {
    $this->_set_where( $where );
 
    $row = $this->_db->{$collection}->findOne( $this->_ws, $this->_sls );
    $this->_flush();
    return ( object ) $row;
}

Мы определяем предложение where для запроса и используем PHP-драйвер MongoDB для выполнения операции findOne . Затем мы сбрасываем параметры запроса и возвращаем запись как объект.

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

Далее мы реализуем метод _find для получения списка записей.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
protected function _find( $collection, $where = array() ) {
    $this->_set_where( $where );
    
    $docs = $this->_db->{$collection}
        ->find( $this->_ws, $this->_sls )
        ->limit( $this->_lmt )
        ->skip( $this->_ost );
    $this->_flush();
 
    $result = array();
    foreach( $docs as $row ) {
        $result[] = ( object ) $row;
    }
    return $result;
}

В методе _find мы используем метод find драйвера, устанавливающий limit запросов и параметры skip для поддержки разбиения на страницы.

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

У нас уже есть поддержка для создания и чтения записей из базы данных. Теперь мы должны иметь возможность редактировать эти записи и добавлять или изменять данные записи. Создайте новый метод _update и реализуйте его следующим образом:

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
protected function _update( $collection, $data, $where = array() ) {
    if ( is_object( $data ) ) {
        $data = ( array ) $data;
    }
    $this->_set_where( $where );
 
    if ( array_key_exists( ‘$set’, $data ) ) {
        $newdoc = $data;
    } else {
        $newdoc = array( ‘$set’ => $data );
    }
 
    $result = false;
    try {
        if( $this->_db->{$collection}->update( $this->_ws, $newdoc ) ) {
            $result = ( object ) $data;
        }
    } catch( \MongoCursorException $e ) {
        $result = new \stdClass();
        $result->error = $e->getMessage();
    }
    $this->_flush();
 
    return $result;
}

Как и в случае операции CREATE, мы поддерживаем и массивы, и объекты, что означает, что мы проверяем и приводим соответственно Мы объединяем предложения where, переданные методу, используя вспомогательный метод. Остальное ничем не отличается от уже созданного метода _insert .

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
protected function _remove( $collection, $where = array() ) {
    $this->_set_where( $where );
 
    $result = false;
    try {
        if ( $this->_db->{$collection}->remove( $this->_ws ) ) {
            $result = true;
        }
    } catch( \MongoCursorException $e ) {
        $result = new \stdClass();
        $result->error = $e->getMessage();
    }
    $this->_flush();
 
    return $result;
}

Как и прежде, мы устанавливаем условие where для операции удаления и полагаемся на PHP-драйвер MongoDB для выполнения операции remove в базе данных.

И это все для нашей Base модели. Это много кода, но теперь мы можем выполнять операции в MongoDB для моделей, которые наследуются от Base класса.

Модель Session будет отвечать за создание, удаление и поиск сеанса в базе данных. Создайте новый файл в папке Models приложения, назовите его Session.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
<?php namespace App\Http\Models;
 
class Session extends Base {
 
    private $_col = «sessions»;
 
    public function create( $user ) {
        $this->_where( ‘user_id’, ( string ) $user->_id );
        $existing = $this->_findOne( $this->_col );
 
        if ( !empty( ( array ) $existing ) ) {
            $this->_where( ‘user_id’, ( string ) $user->_id );
            $this->_remove( $this->_col );
        }
 
        $session = new \stdClass();
        $session->user_id = ( string ) $user->_id;
        $session->user_name = $user->name;
        $session = $this->_insert( $this->_col, $session );
 
        return $session;
    }
 
    public function find( $token ) {
        $this->_where( ‘_id’, $token );
        return $this->_findOne( $this->_col );
    }
 
    public function remove( $token ) {
        $this->_where( ‘_id’, $token );
        return $this->_remove( $this->_col );
    }
}

Эта модель расширяется от Base класса, который мы создали ранее, для поддержки операций MongoDB. Он также устанавливает коллекцию для sessions .

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

Метод find используется для извлечения записи сеанса из базы данных с использованием токена сеанса. Обратите внимание, что он просто устанавливает предложение where для запроса и делегирует задачу поиска записи методу _findOne Base класса.

Чтобы завершить сеанс пользователя, мы реализуем метод remove . Используя маркер сеанса, он делегирует тяжелую _remove методу _remove Base класса. Обратите внимание, что модель не проверяет переданный токен сеанса. Это должно быть обработано контроллером. Единственная проблема для модели — манипулирование данными.

Другая модель, в которой нуждается наш REST API, — это модель для взаимодействия с пользователем. В папке Models приложения создайте новый файл User.php и добавьте в него следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php namespace App\Http\Models;
 
use App\Http\Models\Base as Model;
 
class User extends Model {
 
    private $_col = «users»;
 
    private $_error = null;
 
    public function get( $where ) {}
 
    public function get_error() {}
 
    public function create( $user ) {}
 
    public function remove( $id ) {}
 
    public function retrieve( $id, $distance, $limit = 9999, $page = 1 ) {}
 
    public function update( $id, $data ) {}
}

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

1
2
3
4
5
6
if ( is_array( $where ) ) {
    return $this->_findOne( $this->_col, $where );
} else {
    $this->_where( ‘_id’, $where );
    return $this->_findOne( $this->_col );
}

Мы предполагаем, что в случае, where параметр where не является массивом, это идентификатор пользователя. Затем метод get делегирует задачу поиска записи методу _findOne Base класса.

Метод get_error — это вспомогательный метод, который даст контроллеру больше информации о сбое в модели.

1
return $this->_error;

Последней операцией чтения в модели User является метод для метода retrieve . Это приведет к получению списка пользователей. Добавьте следующий код в метод retrieve :

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
if ( !empty( $id ) && !empty( $distance ) ) {
    $this->_where( ‘_id’, $id );
    $this->_select( ‘location’ );
    $user = $this->_findOne( $this->_col );
 
    if ( empty( ( array ) $user ) ) {
        $this->_error = «ERROR_INVALID_USER»;
        return false;
    }
 
    $this->_where( ‘$and’, array(
            array(
                ‘_id’ => array( ‘$ne’ => new \MongoId( $id ) )
            ),
            array(
                ‘location’ => array(
                    ‘$nearSphere’ => array(
                        ‘$geometry’ => array(
                            ‘type’ => «Point»,
                            ‘coordinates’ => $user->location[‘coordinates’]
                        ),
                        ‘$maxDistance’ => ( float ) $distance
                    )
                )
            )
        ) );
}
 
$this->_limit( $limit, ( $limit * —$page ) );
return $this->_find( $this->_col );

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

Если id не совпадает ни с одной записью, он возвращает false . Если пользователь существует, он подготавливает геопространственный запрос, используя индекс 2dsphere MongoDB.

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
$this->_where( ‘_id’, $id );
$user = $this->_findOne( $this->_col );
 
if ( empty( ( array ) $user ) ) {
    $this->_error = «ERROR_INVALID_ID»;
    return false;
} else {
    $this->_where( ‘_id’, $id );
    if ( !$this->_remove( $this->_col ) ) {
        $this->_error = «ERROR_REMOVING_USER»;
        return false;
    }
}
 
return $user;

Мы проверяем, что данный _id соответствует существующему пользователю, и _remove удалить его, используя метод _remove Base класса. Если что-то пошло не так, мы устанавливаем свойство модели _error и возвращаем false .

Еще одна операция, которую должна поддерживать наша модель, — создание пользовательских записей. Добавьте следующий код в метод create :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if ( is_array( $user ) ) {
    $user = ( object ) $user;
}
$this->_where( ‘$or’, array(
        array(
            «email» => $user->email
        ),
        array(
            «mobile» => $user->mobile
        )
    )
);
$existing = $this->_findOne( $this->_col );
 
if ( empty( ( array ) $existing ) ) {
    $user = $this->_insert( $this->_col, $user );
} else {
    $user = $existing;
}
 
$user->_id = ( string ) $user->_id;
 
return $user;

В этом методе мы удостоверяемся, что еще нет пользователя, связанного с данным email или mobile . Если это правда, мы возвращаем соответствующего пользователя. Если это не так, мы делегируем задачу создания пользователя методу _insert Base класса.

Прежде чем мы вернем запись пользователя, мы приводим _id к строке. Это почему? Возвращаемый нам объект определяет поле _id как объект MongoId . Однако клиентскому приложению этот объект не нужен.

Модель User также должна поддерживать обновление пользовательских записей. Добавьте следующий код в метод update :

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
if ( is_array( $data ) ) {
    $data = ( object ) $data;
}
if ( isset( $data->email ) || isset( $data->mobile ) ) {
        $this->_where( ‘$and’, array(
            array(
                ‘_id’ => array( ‘$ne’ => new \MongoId( $id ) )
            ),
            array(
                ‘$or’ => array(
                    array(
                        ’email’ => ( isset( $data->email ) ) ?
                    ),
                    array(
                        ‘mobile’ => ( isset( $data->mobile ) ) ?
                    )
                )
            )
        )
    );
    $existing = $this->_findOne( $this->_col );
    if ( !empty( ( array ) $existing ) && $existing->_id != $id ) {
        $this->_error = «ERROR_EXISTING_USER»;
        return false;
    }
}
 
$this->_where( ‘_id’, $id );
return $this->_update( $this->_col, ( array ) $data );

Как и в Base классе, метод update принимает как массивы, так и объекты в качестве данных для пользователя. Это делает метод намного более гибким.

Прежде чем обновить запись о пользователе, мы должны убедиться, что email и mobile не используются другим пользователем. Если это так, мы устанавливаем ошибку в EXISTING_USER и возвращаем false . В противном случае мы делегируем операцию обновления Base классу.

Так же, как модели приложения наследуются от Base класса, контроллеры также наследуются от общего родительского класса, отличного от класса Controller Laravel. Этот класс BaseController , тем не BaseController , не BaseController со сложностью Base модели.

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

1
php artisan make:controller BaseController —plain

Это создаст файл с именем BaseController.php в папке Controllers приложения в папке app / Http . Так как мы используем --plain flag, у контроллера не будет никаких методов, чего мы и хотим.

Этот контроллер не будет использовать класс Request поэтому вы можете удалить следующую строку:

1
use Illuminate\Http\Request;

Поскольку нам нужен доступ к модели Session , добавьте следующую строку в объявление класса BaseController :

1
use App\Http\Models\Session as SessionModel;

Теперь мы готовы реализовать методы класса BaseController . Начните с добавления следующих методов внутри объявления класса:

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
protected function _check_session( $token = «», $id = «» ) {
    $result = false;
    if ( !empty( $token ) ) {
        $SessionModel = new SessionModel();
        $session = $SessionModel->find( $token );
 
        if ( !empty( ( array ) $session ) ) {
            if ( !empty( $id ) ) {
                if ( $session->user_id == $id ) {
                    $result = $session;
                }
            } else {
                $result = $session;
            }
        }
    }
 
    return $result;
}
 
protected function _response( $result ) {
    if ( is_object( $result ) && property_exists( $result, «status» ) ) {
        return response()->json( $result, $result->status );
    } else {
        return response()->json( $result );
    }
}

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

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

Другой вспомогательный метод заботится об отправке результата обратно клиенту, который использует API. На данный момент мы поддерживаем только JSON. Если возвращаемый результат является объектом и имеет параметр состояния, мы устанавливаем его с помощью вспомогательного метода response Laravel. В противном случае мы просто возвращаем результат.

Следующий контроллер, который мы реализуем, — это тот, который обрабатывает запросы на ресурс Sessions . В командной строке перейдите в корень приложения и выполните следующую команду:

1
php artisan make:controller SessionController —plain

Это создаст новый файл с именем SessionController.php в папке Controllers приложения в папке app / Http . Прежде чем мы реализуем этот класс, нам нужно позаботиться о нескольких вещах.

Класс SessionController настоящее время наследуется от класса Controller от Laravel. Нам нужно установить это для использования нашего класса BaseController . Это означает, что мы должны заменить

1
use App\Http\Controllers\Controller;

с

1
use App\Http\Controllers\BaseController;

Нам также нужно изменить предложение extends класса. Вместо расширения из Controller убедитесь, что ваш класс расширяет класс BaseController . Нам также необходимо включить модели, используемые в контроллере. Под последним объявлением добавьте следующие строки:

1
2
use App\Http\Models\Session as SessionModel;
use App\Http\Models\User as UserModel;

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

1
2
3
4
5
6
7
8
9
private $_model = null;
 
public function __construct() {
    $this->_model = new SessionModel();
}
 
public function create( Request $request ) {}
 
public function destroy( $token ) {}

Мы устанавливаем объект model контроллера в конструкторе и объявляем несколько методов, которые являются действиями, поддерживаемыми ресурсом Sessions .

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

1
2
3
4
5
6
7
$result = new \stdClass();
if ( !$this->_model->remove( $token ) ) {
    $result->error = «ERROR_REMOVING_SESSION»;
    $result->status = 403;
}
 
return $this->_response( $result );

Метод использует метод SessionModel и возвращает результат, используя метод BaseController класса BaseController . Если удаление успешно завершено, мы возвращаем пустой объект. Если произошла ошибка, мы возвращаем ошибку с кодом состояния 403 .

Метод создания сеанса немного сложнее. Обратите внимание, что в объявлении метода мы используем объект Request Laravel. Мы используем этот объект для доступа к параметрам POST запроса. Внутри метода create добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
$email = $request->get( ’email’ );
$mobile = $request->get( ‘mobile’ );
$fbId = $request->get( ‘fbId’ );
 
$result = new \stdClass();
if ( ( empty( $email ) && empty( $mobile ) ) || empty( $fbId ) ) {
    $result->error = «ERROR_INVALID_PARAMETERS»;
    $result->status = 403;
} else {}
 
return $this->_response( $result );

Мы еще не создали объект сеанса, потому что сначала нам нужно кое-что обсудить. Приложение будет использовать только логин Facebook. Из SDK Facebook мы получаем информацию о пользователе при выполнении операции входа в систему. В обработчике POST ресурса Session API нам нужно поддерживать две вещи:

  • начать сеанс для пользователя
  • создание пользователя, когда он не существует, а затем начало сеанса

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
$UserModel = new UserModel();
$where = ( !empty( $email ) ) ?
$user = $UserModel->get( $where );
 
if ( empty( ( array ) $user ) ) {
    
} else {
    if ( $fbId != $user->fbId ) {
        $result->error = «ERROR_INVALID_CREDENTIALS»;
        $result->status = 403;
    }
}
 
if ( !property_exists( $result, «error» ) ) {
    $result = $this->_model->create( $user );
    $result->token = $result->_id;
    unset( $result->_id );
}

Сначала мы проверяем существующего пользователя с помощью email или mobile . Если пользователь существует, мы проверяем, что данный идентификатор Facebook совпадает с идентификатором Facebook для записи пользователя. Если это так, мы создаем объект сеанса. Если это не так, метод возвращает ошибку INVALID_CREDENTIALS с кодом состояния 403 .

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name = $request->get( ‘name’ );
$gender = $request->get( ‘gender’ );
$location = $request->get( ‘location’ );
 
if ( empty( $name ) || empty( ( array ) $location ) || empty( $gender ) ) {
    $result->error = «ERROR_INVALID_PARAMETERS»;
    $result->status = 403;
} else {
    if ( gettype( $location ) == «string» ) {
        $location = json_decode( $location );
    }
    $locObj = new \stdClass();
    $locObj->type = «Point»;
    $locObj->coordinates = array( $location->lon, $location->lat );
 
    $user->name = $name;
    $user->fbId = $fbId;
    $user->email = $email;
    $user->mobile = $mobile;
    $user->gender = $gender;
    $user->location = $locObj;
 
    $user = $UserModel->create( $user );
}

Сначала мы извлекаем остальные обязательные параметры из запроса, а затем проверяем, задан ли параметр location как объект JSON или закодированный объект JSON (строка). Метод ожидает, что этот параметр будет в следующем формате:

1
2
3
4
{
    «lat» : 37.427208696456866,
    «lon» : -122.17097282409668
}

Затем мы преобразуем это местоположение в местоположение MongoDB 2dSphere . Для выполнения геопространственных запросов это поле должно иметь следующий формат:

1
2
3
4
{
    «type» : «Point»
    «coordinates» : [ -122.17097282409668, 37.427208696456866 ]
}

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

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

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

Последний контроллер, который требуется приложению, — это тот, который отвечает за обработку ресурса Users . Еще раз, мы используем команду ремесленника Laravel. В командной строке перейдите в корень приложения и выполните следующую команду:

1
php artisan make:controller UserController —plain

Это создаст файл UserController.php в папке Controllers приложения в папке app / Http . Как и в SessionController классом SessionController , убедитесь, что класс UserController наследуется от BaseController и включает в себя класс UserModel . Внутри объявления класса добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private $_model = null;
 
public function __construct() {
    $this->_model = new UserModel();
}
 
public function create( Request $request ) {}
 
public function get( Request $request, $id ) {}
 
public function remove( Request $request, $id ) {}
 
public function retrieve( Request $request ) {}
 
public function update( Request $request, $id ) {}

Как и в SessionController классом SessionController , мы инициализируем объект модели и объявляем методы, которые будут поддерживаться ресурсом Users . Начнем с тех, что для операций GET. В методе get добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
$token = $request->get( ‘token’ );
 
$result = new \stdClass();
if ( !$this->_check_session( $token ) ) {
    $result->error = «PERMISSION_DENIED»;
    $result->status = 403;
} else {
    $result = $this->_model->get( $id );
}
 
return $this->_response( $result );

Чтобы получить запись из системы, мы требуем, чтобы у пользователя был активный сеанс. Он не должен совпадать с идентификатором полученного пользователя. Если у пользователя нет действительного сеанса, мы возвращаем ошибку PERMISSION_DENIED с кодом состояния 403 . В противном случае мы возвращаем запись пользователя в виде объекта JSON.

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

01
02
03
04
05
06
07
08
09
10
11
12
$token = $request->get( ‘token’ );
$distance = $request->get( ‘distance’ );
 
$session = $this->_check_session( $token );
$result = $this->_model->retrieve( ( isset( $session->user_id ) ? $session->user_id : «» ), $distance, $request->get( ‘limit’ ), $request->get( ‘page’ ) );
if ( !is_array( $result ) && !$result ) {
    $result = new \stdClass();
    $result->error = $this->_model->get_error();
    $result->status = 403;
}
 
return $this->_response( $result );

Мы начнем с выборки параметров запроса, в частности токена сеанса пользователя и параметров расстояния. Этот метод, однако, не требует активного сеанса. Если сеанс действителен, мы передаем идентификатор пользователя в метод retrieve класса UserModel .

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

Создание пользователя будет сопоставлено с операцией POST ресурса Users . Добавьте следующий код в метод 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
$email = $request->get( ’email’ );
$fbId = $request->get( ‘fbId’ );
$gender = $request->get( ‘gender’ );
$location = $request->get( ‘location’ );
$mobile = $request->get( ‘mobile’ );
$name = $request->get( ‘name’ );
 
if ( gettype( $location ) == «string» ) {
    $location = json_decode( $location );
}
$locObj = new \stdClass();
$locObj->type = «Point»;
$locObj->coordinates = array( $location->lon, $location->lat );
 
$result = new \stdClass();
if ( empty( $name ) || empty( ( array ) $location ) || empty( $fbId ) || empty( $gender ) || ( empty( $email ) && empty( $mobile ) ) ) {
    $result->error = «ERROR_INVALID_PARAMETERS»;
    $result->status = 403;
} else {
    $user = array(
        «email» => $email,
        «fbId» => $fbId,
        «gender» => $gender,
        «location» => $locObj,
        «mobile» => $mobile,
        «name» => $name
    );
    $result = $this->_model->create( $user );
}
 
return $this->_response( $result );

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
$token = $request->get( ‘token’ );
$result = new \stdClass();
 
if ( !$this->_check_session( $token, $id ) ) {
    $result->error = «PERMISSION_DENIED»;
    $result->status = 403;
} else {
    $result = $this->_model->remove( $id );
    if ( !$result ) {
        $result = new \stdClass();
        $result->error = $this->_model->get_error();
        $result->status = 403;
    }
}
 
return $this->_response( $result );

Это один из тех методов, в которых мы хотим _idудалить пользователя, чтобы он соответствовал _idактивному сеансу пользователя. Это первое, что мы проверяем. Если это так, мы делегируем метод модели remove. В противном случае мы устанавливаем ошибку PERMISSION_DENIEDи отправляем результат обратно пользователю.

Наконец, давайте реализуем операцию обновления пользователя. Внутри updateметода добавьте следующий код:

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
$token = $request->get( 'token' );
$data = new \stdClass();
if ( !empty( $email = $request->get( 'email' ) ) ) {
    $data->email = $email;
}
if ( !empty( $fbId = $request->get( 'fbId' ) ) ) {
    $data->fbId = $fbId;
}
if ( !empty( $gender = $request->get( 'gender' ) ) ) {
    $data->gender = $gender;
}
if ( !empty( $location = $request->get( 'location' ) ) ) {
    if ( gettype( $location ) == "string" ) {
        $location = json_decode( $location );
    }
    $locObj = new \stdClass();
    $locObj->type = "Point";
    $locObj->coordinates = array( $location->lon, $location->lat );
        
    $data->location = $locObj;
}
if ( !empty( $mobile = $request->get( 'mobile' ) ) ) {
    $data->mobile = $mobile;
}
if ( !empty( $name = $request->get( 'name' ) ) ) {
    $data->name = $name;
}
 
$result = new \stdClass();
if ( !$this->_check_session( $token, $id ) ) {
    $result->error = "PERMISSION_DENIED";
    $result->status = 403;
} else {
    $result = $this->_model->update( $id, $data );
    if ( !$result ) {
        $result = new \stdClass();
        $result->error = $this->_model->get_error();
        $result->status = 403;
    }
}
 
return $this->_response( $result );

Мы проверяем переданные данные и устанавливаем соответствующий объект для обновления. В случае locationпараметра мы сначала переформатируем его.

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

Затем мы вызываем updateметод UserModelкласса и возвращаем результат клиенту.

С этим последним фрагментом кода наш API завершен. У нас есть наши контроллеры и модели на месте. Последнее, что мы должны сделать, — сопоставить входящие запросы с соответствующими конечными точками.

Для этого нам нужно отредактировать файл route.php нашего приложения . Он находится внутри папки app / Http . Если вы откроете его, вы должны увидеть что-то вроде этого:

1
Route::get( '/', 'WelcomeController@index' );

Когда приложение получает запрос GET без указания какого-либо ресурса, indexметод WelcomeControllerкласса должен его обработать. Тем не менее, вы, вероятно, уже удалили WelcomeControllerв начале этого урока. Если вы попытаетесь перейти к этой конечной точке в браузере, вы получите ошибку. Давайте заменим эту последнюю строку следующим кодом:

01
02
03
04
05
06
07
08
09
10
11
12
13
Route::post( 'sessions', 'SessionController@create' );
 
Route::delete( 'sessions/{token}', 'SessionController@destroy' );
 
Route::delete( 'users/{id}', 'UserController@remove' );
 
Route::get( 'users', 'UserController@retrieve' );
 
Route::get( 'users/{id}', 'UserController@get' );
 
Route::post( 'users', 'UserController@create' );
 
Route::put( 'users/{id}', 'UserController@update' );

Мы сопоставляем запросы API с методами, ранее добавленными в наши контроллеры. Например, следующий вызов

1
[ DELETE ] - http://YOUR_API_URL/sessions/abc

переводит в запрос DELETE по указанному URL. Это означает , что в SessionController  в deleteметоде , с которым будет знамением abc.

Вот и все для RESTful API, использующего Laravel 5.0. У нас есть поддержка управления пользователями и сессиями, и это именно то, что нам нужно для реализации клиента iOS.

In the next part of this tutorial, Jordan will be showing you how to integrate this API in an iOS application. He will also show you how to integrate the Sinch SDK for messaging and voice calls.