Desarrollando una aplicación de citas con Sinch: RESTful API
This sponsored post features a product relevant to our readers while meeting our editorial guidelines for being objective and educational.
() translation by (you can also view the original English article)
En este tutorial, vamos a crear una aplicación de citas para iOS similar a Tinder. Para voz y mensajería, aprovecharemos la plataforma Sinch, haciendo uso de su poderoso SDK.
En la primera parte, nos centraremos en el desarrollo de una API RESTful para almacenar y recuperar información del usuario. En la segunda parte, el cliente de iOS se conectará a esta API para encontrar usuarios cercanos según la ubicación actual del usuario.
Utilizaremos Laravel 5.0 para el servicio RESTful y cubriremos conceptos básicos, como rutas y controladores. También vamos a definir modelos personalizados para admitir la integración de MongoDB de una manera similar a ActiveRecord. Empecemos.
1. Configuración básica
Voy a asumir que ya has instalado Composer y el último instalador de Laravel. Si no lo has hecho, entonces sigue la documentación oficial de Laravel 5.0. El proceso de instalación no debería tomar más de un par de minutos.
Desde la línea de comandos, navega hasta la ubicación donde deseas crear la aplicación para el servicio RESTful y ejecuta el siguiente comando:
1 |
laravel new mobilesinch |
Después de un par de segundos, el comando te dirá que la aplicación mobilesinch se ha creado correctamente. Navega a la nueva carpeta y ejecuta el siguiente comando:
1 |
php artisan fresh |
Cualquier aplicación Laravel 5.0, por defecto, se envía con algunos 'scaffoldings' básicos para el registro y la autenticación de usuarios. Este comando se encarga de eliminar esto, ya que queremos comenzar con una pizarra limpia.
Hay otra cosa de la que tenemos que ocuparnos antes de escribir el código real para nuestro servicio RESTful. De forma predeterminada, Laravel 5.0 se entrega con un middleware para la protección de falsificación de solicitud entre sitios (CSRF). Sin embargo, dado que no estamos construyendo un sitio web sino una API REST, no tiene sentido tener esto en su lugar. Además, puede causar algunos problemas en el camino.
Para esta aplicación, lo mejor es eliminarlo. En la raíz de la carpeta de la aplicación, navega a app/Http. Dentro de esa carpeta, hay un archivo llamado Kernel.php. Ábrelo y quita la siguiente línea:
1 |
'App\Http\Middleware\VerifyCsrfToken', |
También puedes eliminar WelcomeController.php, que se encuentra dentro de app/Http/Controllers, así como la vista predeterminada welcome.blade.php dentro de la carpeta resources/views. No los usaremos, pero puedes dejarlos allí si quieres. Solo asegúrate de dejar la vista 503.blade.php en su lugar, ya que es útil para depurar la aplicación.
2. Modelo Base
La aplicación de citas que pretende crear este tutorial tiene una funcionalidad similar a Tinder en la que puedes encontrar usuarios cerca de tu ubicación actual. Para que esto funcione, la API debe realizar una búsqueda basada en la ubicación del usuario, conocida como una consulta geoespacial. Aunque podríamos hacer esto con MySQL, el estándar de la industria se inclina hacia MongoDB, y personalmente me gusta mucho más.
En lugar de usar la fachada de DB de Laravel, crearemos nuestra propia clase que los modelos de la aplicación ampliarán para realizar consultas en MongoDB.
Esta será una clase simple y no se integrará en el modelo Eloquent de Laravel, aunque podamos, me gustaría que sea simple por el momento.
Paso 1: Configuración de MongoDB
Antes de escribir la clase para realizar consultas MongoDB, debemos configurar la información de la base de datos, tal como lo haríamos para MySQL o PostgreSQL, o cualquier otro servidor de base de datos.
Dentro de la carpeta raíz config, crea un nuevo archivo y asígnale el nombre mongodb.php. Agrega el siguiente código:
1 |
<?php
|
2 |
|
3 |
return [ |
4 |
'host' => 'localhost', |
5 |
'port' => 27017, |
6 |
'user' => '', |
7 |
'pass' => '', |
8 |
'db' => 'mobilesinch' |
9 |
];
|
Establecemos el host y el puerto para nuestro servidor MongoDB, si corresponde, configuramos el usuario y la contraseña para la conexión, y definimos la base de datos que usaremos, mobilesinch.
Como MongoDB es una base de datos orientada a documentos y no tiene esquemas, no necesitamos más configuración, ninguna definición de migración o cualquier otra cosa para estructurar las tablas. Simplemente funciona.
Paso 2: Conexión a la base de datos
Tenemos el archivo de configuración en su lugar y ahora es el momento de crear la clase real que manejará las interacciones con la base de datos.
Esta clase realizará consultas a MongoDB utilizando una sintaxis similar a ActiveRecord. Dentro de la carpeta app/Http, crea una nueva, Models, y agrega un archivo Base.php dentro de ella. Agrega el siguiente código:
1 |
<?php namespace App\Http\Models; |
2 |
|
3 |
use Illuminate\Support\Facades\Config; |
4 |
|
5 |
class Base { |
6 |
|
7 |
private $_config = null; |
8 |
|
9 |
private $_conn = null; |
10 |
|
11 |
private $_db = null; |
12 |
|
13 |
public function __construct() { |
14 |
$this->_config = Config::get( 'mongodb' ); |
15 |
|
16 |
$this->_connect(); |
17 |
}
|
18 |
|
19 |
private function _connect() {} |
20 |
}
|
Estos son los huesos básicos para nuestra clase de modelo Base
. No se extiende desde nada y solo se basa en la fachada Config
de Laravel para recuperar los parámetros de configuración que creamos anteriormente.
A continuación, necesitamos crear una conexión con la base de datos. Agrega el siguiente código al método privado _connect
:
1 |
$conn = 'mongodb://'.$this->_config['host']; |
2 |
if( ! empty( $this->_config['port'] ) ) { |
3 |
$conn .= ":{$this->_config['port']}"; |
4 |
}
|
5 |
|
6 |
$options = array(); |
7 |
if( ! empty( $this->_config['user'] ) && ! empty( $this->_config['pass'] ) ) { |
8 |
$options['username'] = $this->_config['user']; |
9 |
$options['password'] = $this->_config['pass']; |
10 |
}
|
11 |
|
12 |
try { |
13 |
$this->_conn = new \MongoClient( $conn, $options ); |
14 |
|
15 |
$this->_db = $this->_conn->{$this->_config['db']}; |
16 |
return true; |
17 |
} catch( \MongoConnectionException $e ) { |
18 |
$this->_conn = null; |
19 |
return false; |
20 |
}
|
En este método, creamos una cadena de conexión y establecemos el nombre de usuario y la contraseña, si se proporcionan. Luego utilizamos el controlador MongoDB de PHP para crear una conexión y establecer la base de datos en la que se especifica en el archivo de configuración.
Si estás familiarizado con la sintaxis de MongoDB desde la línea de comandos, este método es el equivalente a ingresar a la consola de Mongo y al teclear use mobilesinch
. Consulta la documentación oficial de PHP MongoDB para obtener más información.
Paso 3: Métodos de ayuda
Antes de continuar con las operaciones CRUD de la base de datos, hay algunos métodos que nuestra clase Base
debe implementar. Estos se utilizan para establecer filtros, seleccionar declaraciones y otras variables de consulta que se utilizan para realizar operaciones de base de datos. Comencemos agregando las variables miembro necesarias. Sobre el constructor de la clase, agregue el siguiente código:
1 |
private $_ws = array(); |
2 |
|
3 |
private $_sls = array(); |
4 |
|
5 |
private $_lmt = 99999; |
6 |
|
7 |
private $_ost = 0; |
Estos son los titulares de las consultas de base de datos, where, select, limit y offset . Para establecer estas variables miembro, crea los siguientes métodos de establecimiento:
1 |
protected function _limit( $limit, $offset = null ) {} |
2 |
|
3 |
protected function _select( $select = "" ) {} |
4 |
|
5 |
protected function _where( $key, $value = null ) {} |
El método _limit
será útil para paginar los resultados de una operación READ. El usuario puede configurar el parámetro limit
para especificar el número de registros para recuperar y, opcionalmente, un parámetro offset
para especificar la página para leer. Agrega el siguiente código al método _limit
:
1 |
if ( $limit !== NULL && is_numeric( $limit ) && $limit >= 1 ) { |
2 |
$this->_lmt = $limit; |
3 |
}
|
4 |
if ( $offset !== NULL && is_numeric( $offset ) && $offset >= 1 ) { |
5 |
$this->_ost = $offset; |
6 |
}
|
El método _select
se utilizará para determinar qué campos de un registro debe devolver una consulta READ. La declaración de selección debe proporcionarse como una cadena separada por comas.
1 |
$fields = explode( ',', $select ); |
2 |
foreach ( $fields as $field ) { |
3 |
$this->_sls[trim( $field )] = true; |
4 |
}
|
Finalmente, el método _where
se usará para filtrar los resultados de la consulta y puede ser una matriz o un par key/value.
1 |
if ( is_array( $key ) ) { |
2 |
foreach( $key as $k => $v ) { |
3 |
$this->_ws[$k] = $v; |
4 |
}
|
5 |
} else { |
6 |
$this->_ws[$key] = $value; |
7 |
}
|
Ahora tenemos soporte para limitar y filtrar consultas, pero tenemos que agregar algunos otros métodos de tipo 'helper'. El primero se usará para combinar cualquier declaración where antes de emitir una consulta con el parámetro de la consulta where.
En un momento, cuando escribamos nuestros métodos CRUD, esto tendrá más sentido. En la parte inferior de la clase, agregue el siguiente método privado:
1 |
private function _set_where( $where = null ) { |
2 |
if ( is_array( $where ) ) { |
3 |
$where = array_merge( $where, $this->_ws ); |
4 |
foreach ( $where as $k => $v ) { |
5 |
if ( $k == "_id" && ( gettype( $v ) == "string" ) ) { |
6 |
$this->_ws[$k] = new \MongoId( $v ); |
7 |
} else { |
8 |
$this->_ws[$k] = $v; |
9 |
}
|
10 |
}
|
11 |
} else if( is_string( $where ) ) { |
12 |
$wheres = explode( ',', $where ); |
13 |
foreach ( $wheres as $wr ) { |
14 |
$pair = explode( '=', trim( $wr ) ); |
15 |
if ( $pair[0] == "_id" ) { |
16 |
$this->_ws[trim( $pair[0] )] = new \MongoId( trim( $pair[1] ) ); |
17 |
} else { |
18 |
$this->_ws[trim( $pair[0] )] = trim( $pair[1] ); |
19 |
}
|
20 |
}
|
21 |
}
|
22 |
}
|
Parece un poco intimidante, pero en realidad es bastante simple. Primero verifica si el parámetro where
es una matriz. Si es así, combina los valores dados con los existentes utilizando el método de ayuda _where
.
Sin embargo, este método también admite una cadena para establecer lo que devuelve una operación READ. Esta cadena debe tener el siguiente formato:
1 |
name=John,last_name=Smith |
Este ejemplo ejecutará una consulta y devolverá los campos donde el campo name
se establece en John
y el campo last_name
se establece en Smith
.
Sin embargo, ten en cuenta que, tanto para una matriz como para una cadena, verificamos si hay un campo _id
presente. Si este es el caso y es una cadena, creamos un nuevo objeto MongoId
a partir de él. Los identificadores son objetos en MongoDB y compararlos con una cadena devolverá false
, por lo que esta conversión es necesaria.
Otra cosa que tenemos que hacer es restablecer todos los parámetros de consulta una vez que se haya realizado una operación para que no afecten las consultas posteriores. El método _flush
se encargará de esto.
1 |
private function _flush() { |
2 |
$this->_ws = array(); |
3 |
$this->_sls = array(); |
4 |
$this->_lmt = 99999; |
5 |
$this->_ost = 0; |
6 |
}
|
Paso 4: Operaciones CRUD
Ahora contamos con toda la funcionalidad requerida para filtrar y limitar los resultados de nuestras consultas. Es hora de las operaciones reales de la base de datos, que se basarán en el controlador MongoDB de PHP. Si no estás seguro de algo, consulta la documentación.
Operación CREATE
La primera operación que vamos a admitir es la de crear un registro en el sistema. Agrega el siguiente código a la clase Base
:
1 |
protected function _insert( $collection, $data ) { |
2 |
if ( is_object( $data ) ) { |
3 |
$data = ( array ) $data; |
4 |
}
|
5 |
|
6 |
$result = false; |
7 |
try { |
8 |
if ( $this->_db->{$collection}->insert( $data ) ) { |
9 |
$data['_id'] = ( string ) $data['_id']; |
10 |
$result = ( object ) $data; |
11 |
}
|
12 |
} catch( \MongoCursorException $e ) { |
13 |
$result = new \stdClass(); |
14 |
$result->error = $e->getMessage(); |
15 |
}
|
16 |
$this->_flush(); |
17 |
|
18 |
return $result; |
19 |
}
|
A pesar de que el controlador PHP espera que los datos insertados sean una matriz, nuestra clase admitirá tanto matrices como objetos. Primero verificamos lo que se nos pasa y lo lanzamos en consecuencia. Luego intentamos insertar el registro en la base de datos y devolver el registro insertado como un objeto, incluido el _id
.
Operación READ
Vamos a implementar dos métodos de lectura, uno se usará para recuperar un solo registro, mientras que el otro se usará para obtener una lista de registros. Empecemos por lo primero.
1 |
protected function _findOne( $collection, $where = array() ) { |
2 |
$this->_set_where( $where ); |
3 |
|
4 |
$row = $this->_db->{$collection}->findOne( $this->_ws, $this->_sls ); |
5 |
$this->_flush(); |
6 |
return ( object ) $row; |
7 |
}
|
Definimos la cláusula where para la consulta y usamos el controlador MongoDB de PHP para realizar una operación findOne
. Luego vaciamos los parámetros de consulta y devolvemos el registro como un objeto.
El controlador MongoDB de PHP devuelve el resultado como una matriz, mientras que yo personalmente prefiero los objetos. Esa es la verdadera razón para el elenco.
A continuación, implementamos el método _find
para obtener una lista de registros.
1 |
protected function _find( $collection, $where = array() ) { |
2 |
$this->_set_where( $where ); |
3 |
|
4 |
$docs = $this->_db->{$collection} |
5 |
->find( $this->_ws, $this->_sls ) |
6 |
->limit( $this->_lmt ) |
7 |
->skip( $this->_ost ); |
8 |
$this->_flush(); |
9 |
|
10 |
$result = array(); |
11 |
foreach( $docs as $row ) { |
12 |
$result[] = ( object ) $row; |
13 |
}
|
14 |
return $result; |
15 |
}
|
En el método _find
, usamos el método find
del controlador, estableciendo el límite de la consulta mediante los parámetros limit
y skip
para admitir la paginación.
Sin embargo, este método devuelve un objeto MongoCursor
, que luego repetimos para obtener los registros reales. Como antes, emitimos cada registro como un objeto, agregándolo a la matriz de resultados.
Operación UPDATE
Ya tenemos soporte para crear y leer registros de la base de datos. Ahora debemos poder editar esos registros y agregar o modificar los datos de un registro. Crea un nuevo método _update
e implementarlo de la siguiente manera:
1 |
protected function _update( $collection, $data, $where = array() ) { |
2 |
if ( is_object( $data ) ) { |
3 |
$data = ( array ) $data; |
4 |
}
|
5 |
$this->_set_where( $where ); |
6 |
|
7 |
if ( array_key_exists( '$set', $data ) ) { |
8 |
$newdoc = $data; |
9 |
} else { |
10 |
$newdoc = array( '$set' => $data ); |
11 |
}
|
12 |
|
13 |
$result = false; |
14 |
try { |
15 |
if( $this->_db->{$collection}->update( $this->_ws, $newdoc ) ) { |
16 |
$result = ( object ) $data; |
17 |
}
|
18 |
} catch( \MongoCursorException $e ) { |
19 |
$result = new \stdClass(); |
20 |
$result->error = $e->getMessage(); |
21 |
}
|
22 |
$this->_flush(); |
23 |
|
24 |
return $result; |
25 |
}
|
Al igual que con la operación CREATE, admitimos matrices y objetos, lo que significa que verificamos y emitimos en consecuencia. Combinamos las cláusulas where pasadas al método utilizando el método auxiliar. El resto no es diferente del método _insert
creado.
Sin embargo, hay una cosa especial a tener en cuenta. Cuando actualizamos un documento de MongoDB y pasamos los datos al método _update
, el documento será reemplazado. Si solo estamos actualizando un campo y pasamos los datos para ese campo, el documento se convertirá en ese campo. Es por esto que necesitamos crear una matriz con la clave $set
y la información adicional. El resultado es que nuestro registro no se reemplazará con la nueva información.
Operación DELETE
Finalmente, el controlador debe admitir la operación DELETE para eliminar documentos de la base de datos.
1 |
protected function _remove( $collection, $where = array() ) { |
2 |
$this->_set_where( $where ); |
3 |
|
4 |
$result = false; |
5 |
try { |
6 |
if ( $this->_db->{$collection}->remove( $this->_ws ) ) { |
7 |
$result = true; |
8 |
}
|
9 |
} catch( \MongoCursorException $e ) { |
10 |
$result = new \stdClass(); |
11 |
$result->error = $e->getMessage(); |
12 |
}
|
13 |
$this->_flush(); |
14 |
|
15 |
return $result; |
16 |
}
|
Como antes, establecemos la cláusula where para la operación de eliminación y confiamos en el controlador MongoDB de PHP para realizar una operación remove
en la base de datos.
Y eso es todo para nuestro modelo Base
. Es mucho código, pero ahora podemos realizar operaciones en MongoDB para los modelos que heredan de la clase Base
.
3. Modelo Session
El modelo Session
se encargará de crear, eliminar y encontrar una sesión en la base de datos. Crea un nuevo archivo dentro de la carpeta Models de la aplicación, asígnale el nombre Session.php y agrega el siguiente código:
1 |
<?php namespace App\Http\Models; |
2 |
|
3 |
class Session extends Base { |
4 |
|
5 |
private $_col = "sessions"; |
6 |
|
7 |
public function create( $user ) { |
8 |
$this->_where( 'user_id', ( string ) $user->_id ); |
9 |
$existing = $this->_findOne( $this->_col ); |
10 |
|
11 |
if ( !empty( ( array ) $existing ) ) { |
12 |
$this->_where( 'user_id', ( string ) $user->_id ); |
13 |
$this->_remove( $this->_col ); |
14 |
}
|
15 |
|
16 |
$session = new \stdClass(); |
17 |
$session->user_id = ( string ) $user->_id; |
18 |
$session->user_name = $user->name; |
19 |
$session = $this->_insert( $this->_col, $session ); |
20 |
|
21 |
return $session; |
22 |
}
|
23 |
|
24 |
public function find( $token ) { |
25 |
$this->_where( '_id', $token ); |
26 |
return $this->_findOne( $this->_col ); |
27 |
}
|
28 |
|
29 |
public function remove( $token ) { |
30 |
$this->_where( '_id', $token ); |
31 |
return $this->_remove( $this->_col ); |
32 |
}
|
33 |
}
|
Este modelo se extiende desde la clase Base
que creamos anteriormente para admitir las operaciones de MongoDB. También establece la colección que se utilizará para sessions
.
El método create
se utiliza para crear un registro de sesión de usuario. Antes de intentar crearlo, el método verifica si el usuario ya tiene una sesión activa. Si lo hace, lo elimina de la base de datos y crea el nuevo registro con la información de usuario pasada.
El método find
se utiliza para recuperar un registro de sesión de la base de datos utilizando un token de sesión. Ten en cuenta que simplemente establece la cláusula where para la consulta y delega la tarea de encontrar el registro en el método _findOne
de la clase Base
.
Para finalizar una sesión de usuario, implementamos el método remove
. Usando el token de sesión, delega el trabajo pesado al método _remove
de la clase Base
. Ten en cuenta que el modelo no verifica el token de sesión que se pasa. Esto debe ser manejado por el controlador. La única preocupación para el modelo es la manipulación de datos.
4. Modelo User
El otro modelo que necesita nuestra API REST es uno para manejar las interacciones relacionadas con el usuario. Dentro de la carpeta Models de la aplicación, crea un nuevo archivo User.php y agrega el siguiente código:
1 |
<?php namespace App\Http\Models; |
2 |
|
3 |
use App\Http\Models\Base as Model; |
4 |
|
5 |
class User extends Model { |
6 |
|
7 |
private $_col = "users"; |
8 |
|
9 |
private $_error = null; |
10 |
|
11 |
public function get( $where ) {} |
12 |
|
13 |
public function get_error() {} |
14 |
|
15 |
public function create( $user ) {} |
16 |
|
17 |
public function remove( $id ) {} |
18 |
|
19 |
public function retrieve( $id, $distance, $limit = 9999, $page = 1 ) {} |
20 |
|
21 |
public function update( $id, $data ) {} |
22 |
}
|
El modelo User
es un poco más complicado. Comencemos con los métodos para recuperar usuarios. El método get
se encargará de recuperar un solo registro, utilizando la identificación del usuario. Agrega el siguiente código al método get
:
1 |
if ( is_array( $where ) ) { |
2 |
return $this->_findOne( $this->_col, $where ); |
3 |
} else { |
4 |
$this->_where( '_id', $where ); |
5 |
return $this->_findOne( $this->_col ); |
6 |
}
|
Estamos asumiendo que en el caso de que el parámetro where
no sea una matriz, sea la identificación del usuario. El método get
luego delega la tarea de encontrar el registro en el método _findOne
de la clase Base
.
El método get_error
es un método auxiliar que le dará al controlador más información sobre fallas en el modelo.
1 |
return $this->_error; |
La última operación de lectura en el modelo User
es la del método retrieve
. Esto traerá una lista de usuarios. Agrega el siguiente código al método retrieve
.
1 |
if ( !empty( $id ) && !empty( $distance ) ) { |
2 |
$this->_where( '_id', $id ); |
3 |
$this->_select( 'location' ); |
4 |
$user = $this->_findOne( $this->_col ); |
5 |
|
6 |
if ( empty( ( array ) $user ) ) { |
7 |
$this->_error = "ERROR_INVALID_USER"; |
8 |
return false; |
9 |
}
|
10 |
|
11 |
$this->_where( '$and', array( |
12 |
array( |
13 |
'_id' => array( '$ne' => new \MongoId( $id ) ) |
14 |
),
|
15 |
array( |
16 |
'location' => array( |
17 |
'$nearSphere' => array( |
18 |
'$geometry' => array( |
19 |
'type' => "Point", |
20 |
'coordinates' => $user->location['coordinates'] |
21 |
),
|
22 |
'$maxDistance' => ( float ) $distance |
23 |
)
|
24 |
)
|
25 |
)
|
26 |
) ); |
27 |
}
|
28 |
|
29 |
$this->_limit( $limit, ( $limit * --$page ) ); |
30 |
return $this->_find( $this->_col ); |
Este método soporta paginación y consultas geoespaciales. Si se pasan los parámetros id
y distance
, intenta buscar usuarios cercanos según la ubicación del usuario.
Si el id
no coincide con ningún registro, devuelve falso. Si el usuario existe, prepara una consulta geoespacial utilizando un índice MongoDB 2dsphere.
Ten en cuenta que también estamos configurando la consulta para no devolver al usuario que coincide con el _id
del usuario que realiza la búsqueda. Finalmente, establece el límite de consulta y los parámetros de desplazamiento, delegando la tarea al método _find
de la clase Base
.
Para eliminar usuarios, necesitamos implementar el método de eliminación. Agrega el siguiente código al método remove
:
1 |
$this->_where( '_id', $id ); |
2 |
$user = $this->_findOne( $this->_col ); |
3 |
|
4 |
if ( empty( ( array ) $user ) ) { |
5 |
$this->_error = "ERROR_INVALID_ID"; |
6 |
return false; |
7 |
} else { |
8 |
$this->_where( '_id', $id ); |
9 |
if ( !$this->_remove( $this->_col ) ) { |
10 |
$this->_error = "ERROR_REMOVING_USER"; |
11 |
return false; |
12 |
}
|
13 |
}
|
14 |
|
15 |
return $user; |
Verificamos que el _id
dado corresponde a un usuario existente e intentamos eliminarlo utilizando el método _remove
de la clase Base
. Si algo salió mal, establecemos la propiedad _error
del modelo y devolvemos false
.
Otra operación que nuestro modelo debería soportar es la creación de registros de usuarios. Agrega el siguiente código al método create
:
1 |
if ( is_array( $user ) ) { |
2 |
$user = ( object ) $user; |
3 |
}
|
4 |
$this->_where( '$or', array( |
5 |
array( |
6 |
"email" => $user->email |
7 |
),
|
8 |
array( |
9 |
"mobile" => $user->mobile |
10 |
)
|
11 |
)
|
12 |
);
|
13 |
$existing = $this->_findOne( $this->_col ); |
14 |
|
15 |
if ( empty( ( array ) $existing ) ) { |
16 |
$user = $this->_insert( $this->_col, $user ); |
17 |
} else { |
18 |
$user = $existing; |
19 |
}
|
20 |
|
21 |
$user->_id = ( string ) $user->_id; |
22 |
|
23 |
return $user; |
En este método, nos aseguramos de que no haya un usuario asociado con el correo electrónico o móvil dado. Si eso es cierto, devolvemos el usuario correspondiente. Si no lo es, delegamos la tarea para crear un usuario al método _insert
de la clase Base
.
Antes de devolver el registro de usuario, colocamos el _id
en una cadena. ¿Porqué es eso? El objeto que se nos devuelve define el campo _id
como un objeto MongoId. La aplicación cliente, sin embargo, no necesita este objeto.
El modelo User
también debe admitir la actualización de registros de usuario. Agrega el siguiente código al método update
:
1 |
if ( is_array( $data ) ) { |
2 |
$data = ( object ) $data; |
3 |
}
|
4 |
if ( isset( $data->email ) || isset( $data->mobile ) ) { |
5 |
$this->_where( '$and', array( |
6 |
array( |
7 |
'_id' => array( '$ne' => new \MongoId( $id ) ) |
8 |
),
|
9 |
array( |
10 |
'$or' => array( |
11 |
array( |
12 |
'email' => ( isset( $data->email ) ) ? $data->email : "" |
13 |
),
|
14 |
array( |
15 |
'mobile' => ( isset( $data->mobile ) ) ? $data->mobile : "" |
16 |
)
|
17 |
)
|
18 |
)
|
19 |
)
|
20 |
);
|
21 |
$existing = $this->_findOne( $this->_col ); |
22 |
if ( !empty( ( array ) $existing ) && $existing->_id != $id ) { |
23 |
$this->_error = "ERROR_EXISTING_USER"; |
24 |
return false; |
25 |
}
|
26 |
}
|
27 |
|
28 |
$this->_where( '_id', $id ); |
29 |
return $this->_update( $this->_col, ( array ) $data ); |
Al igual que en la clase Base
, el método update
acepta tanto matrices como objetos como los datos para el usuario. Esto hace que el método sea mucho más flexible.
Antes de actualizar el registro de usuario, nos aseguramos de que email
y mobile
del usuario no estén en uso por otro usuario. Si ese es el caso, establecemos el error en EXISTING_USER
y devolvemos false
. De lo contrario, delegamos la operación de actualización a la clase Base
.
5. Clase BaseController
Al igual que los modelos de la aplicación heredados de la clase Base
, los controladores también heredan de una clase padre común, distinta de la clase Controller
de Laravel. Sin embargo, esta clase BaseController
no estará cerca de la complejidad del modelo Base
.
La clase solo se utilizará para manejar algunas tareas simples. Para crear la clase, usamos el comando artisan de Laravel. Desde la línea de comandos, navega hasta la raíz de tu aplicación y ejecuta el siguiente comando:
1 |
php artisan make:controller BaseController --plain
|
Esto creará un archivo llamado BaseController.php dentro de la carpeta Controllers de la aplicación en la carpeta app/Http. Dado que estamos utilizando el indicador --plain
, el controlador no tendrá ningún método, lo cual es lo que queremos.
Este controlador no utilizará la clase Request
, por lo que puedes continuar y eliminar la siguiente línea:
1 |
use Illuminate\Http\Request; |
Debido a que necesitamos acceso al modelo Session
, agrega la siguiente línea a la declaración de la clase BaseController
:
1 |
use App\Http\Models\Session as SessionModel; |
Ahora estamos listos para implementar los métodos de la clase BaseController
. Comienza agregando los siguientes métodos dentro de la declaración de clase:
1 |
protected function _check_session( $token = "", $id = "" ) { |
2 |
$result = false; |
3 |
if ( !empty( $token ) ) { |
4 |
$SessionModel = new SessionModel(); |
5 |
$session = $SessionModel->find( $token ); |
6 |
|
7 |
if ( !empty( ( array ) $session ) ) { |
8 |
if ( !empty( $id ) ) { |
9 |
if ( $session->user_id == $id ) { |
10 |
$result = $session; |
11 |
}
|
12 |
} else { |
13 |
$result = $session; |
14 |
}
|
15 |
}
|
16 |
}
|
17 |
|
18 |
return $result; |
19 |
}
|
20 |
|
21 |
protected function _response( $result ) { |
22 |
if ( is_object( $result ) && property_exists( $result, "status" ) ) { |
23 |
return response()->json( $result, $result->status ); |
24 |
} else { |
25 |
return response()->json( $result ); |
26 |
}
|
27 |
}
|
El método _check_session
se utiliza para verificar el token de sesión, que se pasa como primer argumento. Algunas tareas en la aplicación requieren que el usuario esté conectado. Por ejemplo, al actualizar un registro de usuario, el usuario correspondiente a la sesión activa debe coincidir con el _id
del registro que debe actualizarse.
La implementación es bastante sencilla. Obtenemos la sesión para el token de sesión y, si el ID del usuario que corresponde con la sesión coincide con el ID que se pasa como segundo argumento, devolvemos la sesión. De lo contrario, devolvemos false
.
El otro método auxiliar se encarga de enviar un resultado al cliente que consume la API. Por el momento, solo apoyamos a JSON. Si el resultado a devolver es un objeto y tiene un parámetro de estado, lo configuramos utilizando el método de ayuda response
de Laravel. De lo contrario, simplemente devolvemos el resultado.
6. Clase SessionController
El siguiente controlador que implementaremos es el que maneja las solicitudes del recurso de Sessions
. Desde la línea de comandos, navega hasta la raíz de tu aplicación y ejecuta el siguiente comando:
1 |
php artisan make:controller SessionController --plain
|
Esto creará un nuevo archivo llamado SessionController.php dentro de la carpeta Controllers de la aplicación en la carpeta de app/Http. Antes de implementar esta clase, necesitamos cuidar algunas cosas.
La clase SessionController
actualmente hereda de la clase Controller
de Laravel. Necesitamos configurar esto para usar nuestra clase BaseController
. Esto significa que necesitamos reemplazar
1 |
use App\Http\Controllers\Controller; |
con
1 |
use App\Http\Controllers\BaseController; |
También necesitamos cambiar la cláusula extends
de la clase. En lugar de extenderse desde Controller
, asegúrate de que tu clase esté extendiendo la clase BaseController
. También debemos incluir los modelos utilizados en el controlador. Debajo de la última declaración, agrega las siguientes líneas:
1 |
use App\Http\Models\Session as SessionModel; |
2 |
use App\Http\Models\User as UserModel; |
Normalmente, solo usaríamos SessionModel
, pero verás por qué también estamos usando UserModel
en un momento. En cuanto a la propia clase del controlador, agrega el siguiente código:
1 |
private $_model = null; |
2 |
|
3 |
public function __construct() { |
4 |
$this->_model = new SessionModel(); |
5 |
}
|
6 |
|
7 |
public function create( Request $request ) {} |
8 |
|
9 |
public function destroy( $token ) {} |
Establecemos el objeto model
del controlador en el constructor y declaramos un par de métodos, que son las acciones admitidas por el recurso Sessions
.
Paso 1: Eliminación de la sesión
Para eliminar una sesión de usuario, simplemente usamos el token de sesión, que se proporciona como un parámetro en la URL del recurso. Lo declararemos más adelante en las rutas de aplicación. Dentro del método destroy
, agrega el siguiente código:
1 |
$result = new \stdClass(); |
2 |
if ( !$this->_model->remove( $token ) ) { |
3 |
$result->error = "ERROR_REMOVING_SESSION"; |
4 |
$result->status = 403; |
5 |
}
|
6 |
|
7 |
return $this->_response( $result ); |
El método utiliza el método remove
de SessionModel
y devuelve el resultado utilizando el método _response
de la clase BaseController
. Si la eliminación de la sesión es exitosa, devolvemos un objeto vacío. Si ocurrió un error, devolvemos un error con un código de estado 403
.
Paso 2: Creación de la sesión
El método para crear una sesión es un poco más complicado. Ten en cuenta que en la declaración del método estamos utilizando el objeto Request
de Laravel. Utilizamos este objeto para acceder a los parámetros POST de la solicitud. Dentro del método create
, agrega el siguiente código:
1 |
$email = $request->get( 'email' ); |
2 |
$mobile = $request->get( 'mobile' ); |
3 |
$fbId = $request->get( 'fbId' ); |
4 |
|
5 |
$result = new \stdClass(); |
6 |
if ( ( empty( $email ) && empty( $mobile ) ) || empty( $fbId ) ) { |
7 |
$result->error = "ERROR_INVALID_PARAMETERS"; |
8 |
$result->status = 403; |
9 |
} else {} |
10 |
|
11 |
return $this->_response( $result ); |
Aún no hemos creado el objeto de sesión, porque hay algo que debemos discutir primero. La aplicación va a utilizar Facebook Login solamente. Desde el SDK de Facebook, obtenemos la información del usuario al realizar una operación de inicio de sesión. En el controlador POST de recursos API de Session
, necesitamos admitir dos cosas:
- iniciar una sesión para un usuario
- crear el usuario cuando no existe y luego iniciando la sesión
Esta es también la razón para incluir UserModel
en el controlador. En la cláusula else
vacía anterior, agrega el siguiente código:
1 |
$UserModel = new UserModel(); |
2 |
$where = ( !empty( $email ) ) ? array( 'email' => $email ) : array( 'mobile' => $mobile ); |
3 |
$user = $UserModel->get( $where ); |
4 |
|
5 |
if ( empty( ( array ) $user ) ) { |
6 |
|
7 |
} else { |
8 |
if ( $fbId != $user->fbId ) { |
9 |
$result->error = "ERROR_INVALID_CREDENTIALS"; |
10 |
$result->status = 403; |
11 |
}
|
12 |
}
|
13 |
|
14 |
if ( !property_exists( $result, "error" ) ) { |
15 |
$result = $this->_model->create( $user ); |
16 |
$result->token = $result->_id; |
17 |
unset( $result->_id ); |
18 |
}
|
Primero verificamos si hay un usuario existente con email
o mobile
. Si el usuario existe, verificamos que el ID de Facebook dada coincida con el ID de Facebook para el registro del usuario. Si ese es el caso, creamos el objeto de sesión. Si no lo es, el método devuelve un error INVALID_CREDENTIALS
con un código de estado 403
.
Comenzar una sesión ya está completo. Ten en cuenta que esto no es extra seguro. Sin embargo, para los fines de este tutorial, funcionará bien.
En el caso de que no haya ningún usuario asociado con el correo electrónico o el dispositivo móvil, deseamos crear un nuevo registro. En la cláusula if
anterior que está vacía, agrega el siguiente código:
1 |
name = $request->get( 'name' ); |
2 |
$gender = $request->get( 'gender' ); |
3 |
$location = $request->get( 'location' ); |
4 |
|
5 |
if ( empty( $name ) || empty( ( array ) $location ) || empty( $gender ) ) { |
6 |
$result->error = "ERROR_INVALID_PARAMETERS"; |
7 |
$result->status = 403; |
8 |
} else { |
9 |
if ( gettype( $location ) == "string" ) { |
10 |
$location = json_decode( $location ); |
11 |
}
|
12 |
$locObj = new \stdClass(); |
13 |
$locObj->type = "Point"; |
14 |
$locObj->coordinates = array( $location->lon, $location->lat ); |
15 |
|
16 |
$user->name = $name; |
17 |
$user->fbId = $fbId; |
18 |
$user->email = $email; |
19 |
$user->mobile = $mobile; |
20 |
$user->gender = $gender; |
21 |
$user->location = $locObj; |
22 |
|
23 |
$user = $UserModel->create( $user ); |
24 |
}
|
Primero recuperamos el resto de los parámetros requeridos de la solicitud y luego verificamos si el parámetro location
se proporciona como un objeto JSON o un objeto JSON codificado (string). El método espera que este parámetro esté en el siguiente formato:
1 |
{
|
2 |
"lat" : 37.427208696456866, |
3 |
"lon" : -122.17097282409668 |
4 |
}
|
Luego, transformamos esta ubicación en una ubicación de MongoDB 2dSphere. Para ejecutar consultas geoespaciales, este campo debe tener el siguiente formato:
1 |
{
|
2 |
"type" : "Point" |
3 |
"coordinates" : [ -122.17097282409668, 37.427208696456866 ] |
4 |
}
|
Podríamos pedirle al cliente que envíe la ubicación en este formato. Sin embargo, es mejor que no sobrecarguemos al cliente con reformatear la ubicación del usuario ya que esto es específico para nuestra implementación.
Después de configurar el objeto de ubicación, verificamos que los parámetros requeridos por el usuario existen y, si ese es el caso, creamos un nuevo objeto de usuario usando el método create
de la clase UserModel
.
Eso es. Aunque podríamos iniciar una sesión enviando solo los parámetros email
y fbId
o los parámetros mobile
y fbId
, si se proporciona el resto de la información del usuario, nuestro controlador se encargará de crear un nuevo usuario cuando sea necesario y comenzar una sesión.
7. Clase UserController
El último controlador que necesita la aplicación es el encargado de manejar el recurso Users
. Una vez más, usamos el comando artisan de Laravel. Desde la línea de comandos, navega hasta la raíz de tu aplicación y ejecuta el siguiente comando:
1 |
php artisan make:controller UserController --plain |
Esto creará un archivo UserController.php dentro de la carpeta Controllers de la aplicación en la carpeta app/Http. Al igual que con la clase SessionController
, asegúrate de que la clase UserController
hereda de BaseController
y que incluya la clase UserModel
. Dentro de la declaración de clase, agrega el siguiente código:
1 |
private $_model = null; |
2 |
|
3 |
public function __construct() { |
4 |
$this->_model = new UserModel(); |
5 |
}
|
6 |
|
7 |
public function create( Request $request ) {} |
8 |
|
9 |
public function get( Request $request, $id ) {} |
10 |
|
11 |
public function remove( Request $request, $id ) {} |
12 |
|
13 |
public function retrieve( Request $request ) {} |
14 |
|
15 |
public function update( Request $request, $id ) {} |
Al igual que con la clase SessionController
, inicializamos el objeto del modelo y declaramos los métodos que serán apoyados por el recurso Users
. Comencemos con los de las operaciones GET. En el método get
, agrega el siguiente código:
1 |
$token = $request->get( 'token' ); |
2 |
|
3 |
$result = new \stdClass(); |
4 |
if ( !$this->_check_session( $token ) ) { |
5 |
$result->error = "PERMISSION_DENIED"; |
6 |
$result->status = 403; |
7 |
} else { |
8 |
$result = $this->_model->get( $id ); |
9 |
}
|
10 |
|
11 |
return $this->_response( $result ); |
Para recuperar un registro del sistema, requerimos que el usuario tenga una sesión activa. No tiene que coincidir con el ID del usuario recuperado. Si el usuario no tiene una sesión válida, devolvemos un error PERMISSION_DENIED
con un código de estado 403
. De lo contrario, devolvemos el registro de usuario como un objeto JSON.
A continuación, para una lista de usuarios, necesitamos implementar el método de recuperación. Agrega el siguiente código al método retrieve
:
1 |
$token = $request->get( 'token' ); |
2 |
$distance = $request->get( 'distance' ); |
3 |
|
4 |
$session = $this->_check_session( $token ); |
5 |
$result = $this->_model->retrieve( ( isset( $session->user_id ) ? $session->user_id : "" ), $distance, $request->get( 'limit' ), $request->get( 'page' ) ); |
6 |
if ( !is_array( $result ) && !$result ) { |
7 |
$result = new \stdClass(); |
8 |
$result->error = $this->_model->get_error(); |
9 |
$result->status = 403; |
10 |
}
|
11 |
|
12 |
return $this->_response( $result ); |
Comenzamos por buscar los parámetros de solicitud, el token de sesión del usuario y los parámetros de distancia en particular. Este método, sin embargo, no requiere una sesión activa. Si una sesión es válida, pasamos el ID de usuario al método retrieve
de la clase UserModel
.
Si se pasa un parámetro distance
, se ejecuta una consulta geoespacial. Si no, se realiza una consulta regular de tipo find
. En caso de errores, recuperamos el error del modelo y lo devolvemos al usuario con un código de estado 403
. De lo contrario, devolvemos una matriz que contiene los usuarios encontrados.
La creación del usuario se asignará a la operación POST del recurso Users
. Agrega el siguiente código al método create
:
1 |
$email = $request->get( 'email' ); |
2 |
$fbId = $request->get( 'fbId' ); |
3 |
$gender = $request->get( 'gender' ); |
4 |
$location = $request->get( 'location' ); |
5 |
$mobile = $request->get( 'mobile' ); |
6 |
$name = $request->get( 'name' ); |
7 |
|
8 |
if ( gettype( $location ) == "string" ) { |
9 |
$location = json_decode( $location ); |
10 |
}
|
11 |
$locObj = new \stdClass(); |
12 |
$locObj->type = "Point"; |
13 |
$locObj->coordinates = array( $location->lon, $location->lat ); |
14 |
|
15 |
$result = new \stdClass(); |
16 |
if ( empty( $name ) || empty( ( array ) $location ) || empty( $fbId ) || empty( $gender ) || ( empty( $email ) && empty( $mobile ) ) ) { |
17 |
$result->error = "ERROR_INVALID_PARAMETERS"; |
18 |
$result->status = 403; |
19 |
} else { |
20 |
$user = array( |
21 |
"email" => $email, |
22 |
"fbId" => $fbId, |
23 |
"gender" => $gender, |
24 |
"location" => $locObj, |
25 |
"mobile" => $mobile, |
26 |
"name" => $name |
27 |
);
|
28 |
$result = $this->_model->create( $user ); |
29 |
}
|
30 |
|
31 |
return $this->_response( $result ); |
Primero recuperamos la información necesaria para el usuario y, como en el controlador de creación de sesión, convertimos la ubicación del usuario al formato apropiado.
Después de esto, verificamos que se haya pasado la información requerida. Ten en cuenta que aunque los campos email
y mobile
son opcionales, al menos uno debe estar presente.
Después de estas comprobaciones, invocamos el método create
de la clase UserModel
para insertar el nuevo usuario en la base de datos. Finalmente, devolvemos el nuevo usuario o un error.
Para eliminar un usuario, necesitamos implementar el método de eliminación. Agrega el siguiente código al método de remove
:
1 |
$token = $request->get( 'token' ); |
2 |
$result = new \stdClass(); |
3 |
|
4 |
if ( !$this->_check_session( $token, $id ) ) { |
5 |
$result->error = "PERMISSION_DENIED"; |
6 |
$result->status = 403; |
7 |
} else { |
8 |
$result = $this->_model->remove( $id ); |
9 |
if ( !$result ) { |
10 |
$result = new \stdClass(); |
11 |
$result->error = $this->_model->get_error(); |
12 |
$result->status = 403; |
13 |
}
|
14 |
}
|
15 |
|
16 |
return $this->_response( $result ); |
Este es uno de esos métodos en los que queremos que el _id
del usuario se elimine para que coincida con el _id
del usuario con la sesión activa. Esto es lo primero que verificamos. Si ese es el caso, delegamos al método remove
del modelo. De lo contrario, establecemos el error en PERMISSION_DENIED
y enviamos el resultado al usuario.
Finalmente, implementemos la operación de actualización del usuario. Dentro del método update
, agrega el siguiente código:
1 |
$token = $request->get( 'token' ); |
2 |
$data = new \stdClass(); |
3 |
if ( !empty( $email = $request->get( 'email' ) ) ) { |
4 |
$data->email = $email; |
5 |
}
|
6 |
if ( !empty( $fbId = $request->get( 'fbId' ) ) ) { |
7 |
$data->fbId = $fbId; |
8 |
}
|
9 |
if ( !empty( $gender = $request->get( 'gender' ) ) ) { |
10 |
$data->gender = $gender; |
11 |
}
|
12 |
if ( !empty( $location = $request->get( 'location' ) ) ) { |
13 |
if ( gettype( $location ) == "string" ) { |
14 |
$location = json_decode( $location ); |
15 |
}
|
16 |
$locObj = new \stdClass(); |
17 |
$locObj->type = "Point"; |
18 |
$locObj->coordinates = array( $location->lon, $location->lat ); |
19 |
|
20 |
$data->location = $locObj; |
21 |
}
|
22 |
if ( !empty( $mobile = $request->get( 'mobile' ) ) ) { |
23 |
$data->mobile = $mobile; |
24 |
}
|
25 |
if ( !empty( $name = $request->get( 'name' ) ) ) { |
26 |
$data->name = $name; |
27 |
}
|
28 |
|
29 |
$result = new \stdClass(); |
30 |
if ( !$this->_check_session( $token, $id ) ) { |
31 |
$result->error = "PERMISSION_DENIED"; |
32 |
$result->status = 403; |
33 |
} else { |
34 |
$result = $this->_model->update( $id, $data ); |
35 |
if ( !$result ) { |
36 |
$result = new \stdClass(); |
37 |
$result->error = $this->_model->get_error(); |
38 |
$result->status = 403; |
39 |
}
|
40 |
}
|
41 |
|
42 |
return $this->_response( $result ); |
Validamos los datos que se pasan y configuramos el objeto apropiado para que actualizar. En el caso del parámetro location
, primero lo reformateamos.
Nuevamente, este método solo debe ser accesible para usuarios con una sesión activa que corresponda a su propio _id
. Esto significa que primero comprobamos que ese es el caso.
Luego invocamos el método update
de la clase UserModel
y devolvemos el resultado al cliente.
8. Router de la aplicación
Con esa última pieza de código nuestra API está completa. Tenemos nuestros controladores y modelos en su lugar. Lo último que tenemos que hacer es asignar las solicitudes entrantes a los puntos finales apropiados.
Para eso, necesitamos editar el archivo route.php de tu aplicación. Se encuentra dentro de la carpeta app\Http. Si lo abres, deberías ver algo como esto:
1 |
Route::get( '/', 'WelcomeController@index' ); |
Cuando la aplicación recibe una solicitud GET sin ningún recurso especificado, el método index
de la clase WelcomeController
debería manejarlo. Sin embargo, es probable que ya hayas eliminado WelcomeController
al comienzo de este tutorial. Si intentas navegar a este punto final en un navegador, obtendrás un error. Vamos a reemplazar esa última línea con el siguiente código:
1 |
Route::post( 'sessions', 'SessionController@create' ); |
2 |
|
3 |
Route::delete( 'sessions/{token}', 'SessionController@destroy' ); |
4 |
|
5 |
Route::delete( 'users/{id}', 'UserController@remove' ); |
6 |
|
7 |
Route::get( 'users', 'UserController@retrieve' ); |
8 |
|
9 |
Route::get( 'users/{id}', 'UserController@get' ); |
10 |
|
11 |
Route::post( 'users', 'UserController@create' ); |
12 |
|
13 |
Route::put( 'users/{id}', 'UserController@update' ); |
Asignamos las solicitudes API a los métodos previamente agregados a nuestros controladores. Por ejemplo, la siguiente llamada
1 |
[ DELETE ] - http://YOUR_API_URL/sessions/abc |
se traduce en una solicitud DELETE
a la URL dada. Esto significa que en el SessionController se llamará al método delete
con un token de abc
.
Conclusión
Eso es todo para la API REST con Laravel 5.0. Tenemos soporte para la administración de usuarios y sesiones, que es exactamente lo que necesitamos para implementar el cliente iOS.
En la siguiente parte de este tutorial, Jordan te mostrará cómo integrar esta API en una aplicación de iOS. También te mostrará cómo integrar el SDK de Sinch para mensajes y llamadas de voz.