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:
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:
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:
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:
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:
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:
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:
These are holders for the where, select, limit and offset of the database queries. To set these member variables, create the following setter methods:
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:
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.
Finally, the _where method will be used to filter the query results and can either be an array or a key/value pair.
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:
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:
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.
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:
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.
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.
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:
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.
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:
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:
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:
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.
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:
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:
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:
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:
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:
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:
Because we need access to the Session model, add the following line to the declaration of the BaseController class:
We're now ready to implement the methods of the BaseController class. Start by adding the following methods inside the class declaration:
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:
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
with
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:
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:
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:
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:
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:
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:
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:
We then transform this location into a MongoDB 2dSphere location. To execute geospatial queries, this field needs to have the following format:
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:
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:
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:
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:
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:
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:
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:
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:
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:
We map API requests to the methods previously added to our controllers. For instance, the following call
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.