In this tutorial, we're going to create a dating application for iOS similar to Tinder. For voice and messaging, we will leverage the Sinch platform, making use of its powerful SDK.
In the first part, we will focus on the development of a RESTful API to store and retrieve user information. In the second part, the iOS client will hook into this API to find nearby users based on the user's current location.
We will use Laravel 5.0 for the RESTful service and will be covering basic concepts, such as routes and controllers. We are also going to define custom models to support MongoDB integration in an ActiveRecord-like manner. Let's get started.
1. Basic Setup
I'm going to assume that you've already installed Composer and the latest Laravel installer. If you haven't, then follow the official Laravel 5.0 documentation. The installation process shouldn't take longer than a couple of minutes.
From the command line, navigate to the location where you want to create the application for the RESTful service and execute the following command:
laravel new mobilesinch
After a couple of seconds, the command will tell you that the mobilesinch application has been successfully created. Navigate to the new folder and execute the following command:
php artisan fresh
Any Laravel 5.0 application, by default, ships with some basic scaffolding for user registration and authentication. This command takes care of removing this since we want to start with a clean slate.
There's one other thing that we have to take care of before writing the actual code for our RESTful service. By default, Laravel 5.0 ships with a middleware for cross-site request forgery (CSRF) protection. However, since we are not building a website but a RESTful API, it makes no sense to have this in place. Also, it can cause some problems along the way.
For this application, it's best to remove it. In the root of the application folder, navigate to app/Http. Inside that folder, there's a file named Kernel.php. Open it and remove the following line:
'App\Http\Middleware\VerifyCsrfToken',
You can also remove WelcomeController.php, located inside app/Http/Controllers, as well as the default welcome.blade.php view inside the resources/views folder. We won't be using them, but you can leave them there if you want. Just make sure that you leave the 503.blade.php view in place as it's useful to debug the application.
2. Base
Model
The dating application this tutorial is aiming to create has a Tinder-like functionality in which you can find users near your current location. For this to work, the API needs to perform a search based on the user's location, known as a geospatial query. While we could do this with MySQL, the industry standard leans toward MongoDB, and I personally like it much more.
Instead of using Laravel's DB facade, we will create our own class that the application's models will extend to perform queries in MongoDB.
This will be a simple class and won't be integrated into Laravel's Eloquent model, even though we could, I'd like to keep it simple for the time being.
Step 1: MongoDB Configuration
Before writing the class to perform MongoDB queries, we need to set up the database information, just as we would do for MySQL or PostgreSQL, or any other database server.
Inside the root config folder, create a new file and name it mongodb.php. Add the following code to it:
<?php return [ 'host' => 'localhost', 'port' => 27017, 'user' => '', 'pass' => '', 'db' => 'mobilesinch' ];
We set the host and port for our MongoDB server, if any, set the user and password for the connection, and define the database that we will be using, mobilesinch.
Since MongoDB is a document-oriented database and it is schemaless, we need no further configuration, no migrations definition or anything else to structure the tables. It just works.
Step 2: Database Connection
We have the configuration file in place and it's now time to create the actual class that will handle the interactions with the database.
This class will perform queries to MongoDB using ActiveRecord-like syntax. Inside the app/Http folder, create a new one, Models, and add a Base.php file inside it. Append the following code to it:
<?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() {} }
These are the bare-bones for our Base
model class. It does not extend from anything and only relies on Laravel's Config
facade to retrieve the configuration parameters we created earlier.
Next, we need to create a connection with the database. Add the following code to the private _connect
method:
$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; }
In this method, we create a connection string and set the username and password if any are given. We then use PHP's MongoDB driver to create a connection and set the database to the one specified in the configuration file.
If you are familiar with MongoDB syntax from the command line, this method is the equivalent to entering the mongo console and typing use mobilesinch
. Refer to the official PHP MongoDB documentation for more information.
Step 3: Helper Methods
Before continuing with the database CRUD operations, there are some methods that our Base
class must implement. These are used to set filters, select statements, and other query variables that are used to perform database operations. Let's start with adding the necessary member variables. Above the class constructor, add the following code:
private $_ws = array(); private $_sls = array(); private $_lmt = 99999; private $_ost = 0;
These are holders for the where, select, limit and offset of the database queries. To set these member variables, create the following setter methods:
protected function _limit( $limit, $offset = null ) {} protected function _select( $select = "" ) {} protected function _where( $key, $value = null ) {}
The _limit
method will be useful for paginating the results of a READ operation. The user can set the limit
parameter to specify the number of records to retrieve and optionally an offset
parameter to specify the page to read from. Add the following code to the _limit
method:
if ( $limit !== NULL && is_numeric( $limit ) && $limit >= 1 ) { $this->_lmt = $limit; } if ( $offset !== NULL && is_numeric( $offset ) && $offset >= 1 ) { $this->_ost = $offset; }
The _select
method will be used to determine which fields of a record a READ query must return. The select statement must be provided as a comma separated string.
$fields = explode( ',', $select ); foreach ( $fields as $field ) { $this->_sls[trim( $field )] = true; }
Finally, the _where
method will be used to filter the query results and can either be an array or a key/value pair.
if ( is_array( $key ) ) { foreach( $key as $k => $v ) { $this->_ws[$k] = $v; } } else { $this->_ws[$key] = $value; }
We now have support in place to limit and filter queries, but we have to add some other helper methods. The first one will be used to combine any where statement set before issuing a query with the query's where parameter.
In a moment, when we write our CRUD methods, this will make more sense. At the bottom of the class, add the following private method:
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] ); } } } }
It looks a little intimidating, but it's quite simple actually. It first checks if the where
parameter is an array. If it is, it combines the given values with the existing ones using the _where
helper method.
This method, however, also supports a string to set what is returned by a READ operation. This string should have the following format:
name=John,last_name=Smith
This example will run a query and return the fields where the name
field is set to John
and the last_name
field is set to Smith
.
Note, however, that for both an array or a string, we check if an _id
field is present. If this is the case and it's a string, we create a new MongoId
object from it. Ids are objects in MongoDB and comparing them to a string will return false
, which is why this conversion is necessary.
Another thing we have to do, is reset all the query parameters once an operation has been performed so they won't affect subsequent queries. The _flush
method will take care of this.
private function _flush() { $this->_ws = array(); $this->_sls = array(); $this->_lmt = 99999; $this->_ost = 0; }
Step 4: CRUD Operations
We now have all the required functionality in place to filter and limit our query results. It's time for the actual database operations, which will rely on PHP's MongoDB driver. If you are uncertain about something, refer to the documentation.
CREATE Operation
The first operation that we are going to support is the one to create a record in the system. Add the following code to the Base
class:
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; }
Even though the PHP driver expects the inserted data to be an array, our class will support both arrays and objects. We first verify which is passed to us and cast it accordingly. We then attempt to insert the record into the database and return the inserted record as an object, including the _id
.
READ Operation
We're going to implement two read methods, one will be used to retrieve a single record while the other will be used to fetch a list of records. Let's begin with the former.
protected function _findOne( $collection, $where = array() ) { $this->_set_where( $where ); $row = $this->_db->{$collection}->findOne( $this->_ws, $this->_sls ); $this->_flush(); return ( object ) $row; }
We define the where clause for the query and use PHP's MongoDB driver to perform a findOne
operation. We then flush the query parameters and return the record as an object.
PHP's MongoDB driver returns the result as an array while I personally prefer objects. That's the true reason for the cast.
Next, we implement the _find
method to fetch a list of records.
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; }
In the _find
method, we use the driver's find
method, setting the query limit
and skip
parameters to support pagination.
This method, however, returns a MongoCursor
object, which we then iterate over to obtain the actual records. As before, we cast each record as an object, appending it to the result array.
UPDATE Operation
We already have support to create and read records from the database. We now need to be able to edit those records and add or modify a record's data. Create a new method _update
and implement it as follows:
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; }
As with the CREATE operation, we support both arrays and objects, which means we check and cast accordingly. We combine the where clauses passed to the method using the helper method. The rest is no different than the already created _insert
method.
There is a special thing to note though. When we update a MongoDB document and pass the data to the _update
method, the document will be replaced. If we are only updating one field and pass the data for that field, the document will become that field. This is why we need to create an array with the $set
key and the added information. The result is that our record won't be replaced with the new information.
DELETE Operation
Finally, the driver must support the DELETE operation to remove documents from the database.
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; }
As before, we set the where clause for the delete operation and rely on PHP's MongoDB driver to perform a remove
operation on the database.
And that's it for our Base
model. It's a lot of code, but we can now perform operations in MongoDB for the models that inherit from the Base
class.
3. Session
Model
The Session
model will be in charge of creating, removing, and finding a session in the database. Create a new file inside the application's Models folder, name it Session.php, and add the following code to it:
<?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 ); } }
This model extends from the Base
class we created earlier to support MongoDB operations. It also sets the collection to be used to sessions
.
The create
method is used to create a user session record. Before attempting to create it, the method verifies if the user already has an active session. If it does, it removes it from the database and creates the new record with the passed in user information.
The find
method is used to retrieve a session record from the database using a session token. Note that it simply sets the where clause for the query and delegates the task of finding the record to the _findOne
method of the Base
class.
To end a user session, we implement the remove
method. Using the session token, it delegates the heavy lifting to the _remove
method of the Base
class. Note that the model makes no check for the session token that's passed in. This should be handled by the controller. The only concern for the model is data manipulation.
4. User
Model
The other model that our REST API needs is one to handle user related interactions. Inside the application's Models folder, create a new User.php file, and add the following code to it:
<?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 ) {} }
The User
model is a little more complicated. Let's begin with the methods to retrieve users. The get
method will be in charge of retrieving a single record, using the user's id. Add the following code to the get
method:
if ( is_array( $where ) ) { return $this->_findOne( $this->_col, $where ); } else { $this->_where( '_id', $where ); return $this->_findOne( $this->_col ); }
We are assuming that in the case the where
parameter isn't an array, it's the user's id. The get
method then delegates the task of finding the record to the _findOne
method of the Base
class.
The get_error
method is a helper method that will give the controller more information about failure in the model.
return $this->_error;
The last read operation in the User
model is the one for the retrieve
method. This will fetch a list of users. Add the following code to the retrieve
method:
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 );
This method supports pagination and geospatial queries. If the id
and distance
parameters are passed in, it attempts to search for nearby users based on the user's location.
If the id
does not match any record, it returns false
. If the user does exist, it prepares a geospatial query using a MongoDB 2dsphere index.
Note that we are also setting the query not to return the user matching the _id
of the user performing the search. Finally, it sets the query limit and offset parameters, delegating the task to the _find
method of the Base
class.
To remove users, we need to implement the remove
method. Add the following code to the remove
method:
$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;
We check that the given _id
corresponds to an existing user and attempt to remove it using the _remove
method of the Base
class. If something went wrong, we set the model's _error
property and return false
.
Another operation our model should support is the creation of user records. Add the following code to the create
method:
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;
In this method, we make sure there isn't already a user associated with the given email
or mobile
. If that's true, we return the corresponding user. If it isn't, we delegate the task to create a user to the _insert
method of the Base
class.
Before we return the user record, we cast the _id
to a string. Why is that? The object that's returned to us defines the _id
field as a MongoId object. The client application, however, doesn't need this object.
The User
model also needs to support updating user records. Add the following code to the update
method:
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 ) ) ? $data->email : "" ), array( 'mobile' => ( isset( $data->mobile ) ) ? $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 );
As in the Base
class, the update
method accepts both arrays and objects as the data for the user. This make the method much more flexible.
Before we update the user record, we make sure that the user's email
and mobile
aren't already in use by another user. If that's the case, we set the error to EXISTING_USER
and return false
. Otherwise, we delegate the update operation to the Base
class.
5. BaseController
Class
Just like the application's models inherit from a Base
class, the controllers also inherit from a common parent class, other than Laravel's Controller
class. This BaseController
class won't be anywhere near the complexity of the Base
model though.
The class will only be used to handle a few simple tasks. To create the class, we use Laravel's artisan command. From the command line, navigate to the root of your application and execute the following command:
php artisan make:controller BaseController --plain
This will create a file named BaseController.php inside the application's Controllers folder in the app/Http folder. Since we are using the --plain
flag, the controller will not have any methods, which is what we want.
This controller won't be using the Request
class so you can go ahead and remove the following line:
use Illuminate\Http\Request;
Because we need access to the Session
model, add the following line to the declaration of the BaseController
class:
use App\Http\Models\Session as SessionModel;
We're now ready to implement the methods of the BaseController
class. Start by adding the following methods inside the class declaration:
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 ); } }
The _check_session
method is used to verify the session token, which is passed as the first argument. Some tasks in the application require the user to be logged in. For instance, when updating a user record, the user corresponding with the active session needs to match the _id
of the record that needs to be updated.
The implementation is pretty straightforward. We fetch the session for the session token and, if the id of the user that corresponds with the session matches the id that is passed in as the second argument, we return the session. Otherwise, we return false
.
The other helper method takes care of sending a result back to the client that consumes the API. At the moment, we only support JSON. If the result to return is an object and has a status parameter, we set it using Laravel's response
helper method. Otherwise, we simply return the result.
6. SessionController
Class
The next controller we'll implement is the one that handles requests for the Sessions
resource. From the command line, navigate to the root of your application and execute the following command:
php artisan make:controller SessionController --plain
This will create a new file named SessionController.php inside the application's Controllers folder in the app/Http folder. Before we implement this class, we need to take care of a few things.
The SessionController
class currently inherits from Laravel's Controller
class. We need to set this to use our BaseController
class. This means we need to replace
use App\Http\Controllers\Controller;
with
use App\Http\Controllers\BaseController;
We also need to change the extends
clause of the class. Instead of extending from Controller
, make sure that your class is extending the BaseController
class. We also need to include the models used in the controller. Below the last declaration, add the following lines:
use App\Http\Models\Session as SessionModel; use App\Http\Models\User as UserModel;
Normally, we would just use the SessionModel
, but you'll see why we are also using the UserModel
in just a moment. As for the controller class itself, add the following code:
private $_model = null; public function __construct() { $this->_model = new SessionModel(); } public function create( Request $request ) {} public function destroy( $token ) {}
We set the controller's model
object in the constructor and declare a couple of methods, which are the actions supported by the Sessions
resource.
Step 1: Session Removal
For the removal of a user session, we simply use the session token, which is given as a parameter in the resource URL. We will declare this later in the application routes. Inside the destroy
method, add the following code:
$result = new \stdClass(); if ( !$this->_model->remove( $token ) ) { $result->error = "ERROR_REMOVING_SESSION"; $result->status = 403; } return $this->_response( $result );
The method uses the SessionModel
's remove
method and returns the result using the _response
method of the BaseController
class. If removing the session is successful, we return an empty object. If an error occurred, we return an error with a 403
status code.
Step 2: Session Creation
The method for creating a session is a little more complicated. Note that in the method declaration we are using Laravel's Request
object. We use this object to access the POST parameters of the request. Inside the create
method, add the following code:
$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 );
We haven't created the session object yet, because there's something we need to discuss first. The application is going to be using Facebook Login only. From the Facebook SDK, we obtain the user's information when performing a login operation. In the API's Session
resource POST handler, we need to support two things:
- starting a session for a user
- creating the user when it doesn't exists and then starting the session
This is also the reason for including the UserModel
in the controller. In the above empty else
clause, add the following code:
$UserModel = new UserModel(); $where = ( !empty( $email ) ) ? array( 'email' => $email ) : array( 'mobile' => $mobile ); $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 ); }
We first check for an existing user with the passed in email
or mobile
. If the user exists, we verify that the given Facebook ID matches the Facebook ID for the user record. If that's the case, we create the session object. If it isn't, the method returns a INVALID_CREDENTIALS
error with a 403
status code.
Starting a session is now complete. Note that this isn't extra secure. However, for the purposes of this tutorial, it will work just fine.
For the case when there is no user associated with the passed in email
or mobile
, we want to create a new record. In the above empty if
clause, add the following code:
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 ); }
We first retrieve the rest of the required parameters from the request and then check if the location
parameter is given as a JSON object or an encoded JSON object (string). The method is expecting this parameter to be in the following format:
{ "lat" : 37.427208696456866, "lon" : -122.17097282409668 }
We then transform this location into a MongoDB 2dSphere location. To execute geospatial queries, this field needs to have the following format:
{ "type" : "Point" "coordinates" : [ -122.17097282409668, 37.427208696456866 ] }
We could ask the client to send the location in this format. However, it is better that we don't burden the client with reformatting the user location since this is specific to our implementation.
After setting the location object, we check that the user required parameters exist and, if that's the case, we create a new user object using the create
method of the UserModel
class.
That's it. Even though we could start a session by sending only the email
and fbId
parameters or the mobile
and fbId
parameters, if the rest of the user information is given, our handler will take care of creating a new user when necessary and starting a session.
7. UserController
Class
The last controller that the application needs is the one in charge of handling the Users
resource. Once again, we use Laravel's artisan command. From the command line, navigate to the root of your application and execute the following command:
php artisan make:controller UserController --plain
This will create a UserController.php file inside the application's Controllers folder in the app/Http folder. As with the SessionController
class, make sure that the UserController
class inherits from BaseController
and that it includes the UserModel
class. Inside the class declaration, add the following code:
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 ) {}
As with the SessionController
class, we initialize the model object and declare the methods that will be supported by the Users
resource. Let's begin with the ones for GET operations. In the get
method, add the following code:
$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 );
To retrieve a record from the system, we require that the user has an active session. It doesn't have to match the id of the retrieved user. If the user doesn't have a valid session, we return a PERMISSION_DENIED
error with a 403
status code. Otherwise we return the user record as a JSON object.
Next, for a list of users, we need to implement the retrieve
method. Add the following code to the retrieve
method:
$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 );
We start by fetching the request parameters, the user's session token and distance parameters in particular. This method, however, does not requires an active session. If a session is valid, we pass the user id to the retrieve
method of the UserModel
class.
If a distance
parameter is passed in, a geospatial query is executed. If not, a regular find
query is performed. In case of errors, we retrieve the error from the model and return it to the user with a 403
status code. Otherwise, we return an array containing the found users.
User creation will map to the Users
resource POST operation. Add the following code to the create
method:
$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 );
We first retrieve the necessary information for the user and, as in the session creation handler, we convert the user's location to the appropriate format.
After this, we check that the required information is passed in. Note that even though both the email
and mobile
fields are optional, at least one must be present.
After these checks, we invoke the create
method of the UserModel
class to insert the new user in the database. Finally, we return the new user or an error.
To remove a user, we need to implement the remove
method. Add the following code to the remove
method:
$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 );
This is one of those methods in which we want the _id
of the user to be removed to match the _id
of the user with the active session. This is the first thing we verify. If that's the case, we delegate to the model's remove
method. Otherwise, we set the error to PERMISSION_DENIED
and send the result back to the user.
Finally, let's implement the user's update operation. Inside the update
method, add the following code:
$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 );
We validate the data that's passed in and set the appropriate object for what to update. In the case of the location
parameter, we reformat it first.
Again, this method should only be accessible by users with an active session that corresponds to their own _id
. This means that we first check that's the case.
We then invoke the update
method of the UserModel
class and return the result to the client.
8. Application Router
With that last piece of code our API is complete. We have our controllers and models in place. The last thing we have to do is map incoming requests to the appropriate endpoints.
For that, we need to edit our application's routes.php file. It's located inside the app/Http folder. If you open it, you should see something like this:
Route::get( '/', 'WelcomeController@index' );
When the application receives a GET request without any resource specified, the index
method of the WelcomeController
class should handle it. However, you probably already removed the WelcomeController
at the start of this tutorial. If you try to navigate to this endpoint in a browser, you will get an error. Let's replace that last line with the following code:
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' );
We map API requests to the methods previously added to our controllers. For instance, the following call
[ DELETE ] - http://YOUR_API_URL/sessions/abc
translates to a DELETE request to the given URL. This means that in the SessionController
the delete
method will be called with a token of abc
.
Conclusion
That's it for the RESTful API using Laravel 5.0. We have support for user and session management, which is exactly what we need to implement the iOS client.
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.
Comments