Powered By Blogger

viernes, 30 de agosto de 2013

Building a CodeIgniter Web Application From Scratch – Part 1

In this series we’re going to build a web billboard application from scratch, we’re going to use CodeIgniter to handle the back-end service and BackboneJS for the web client. In the first two parts of the series we’ll create the back-end service and then the client application in the last two.


Application Description

The application we are creating will be a simple billboard, where users can register, post tasks, and offer a reward for its completion. Other users can see the existing tasks, assign the task to themselves, and get the offered reward.

The tasks will have basic data like a title, description and reward (as required parameters) and an optional due date and notes. The user profile will simply consist of the user’s name, email and website. So let’s get started.


Database Setup

First off, for the app data we’re going to use MongoDB as the database server. MongoDB is a document oriented database and the leading NoSQL database out there. It’s really scalable and fast, which makes it great to manage huge amounts of data.

MongoDB is a document oriented database and the leading NoSQL database.

In order to use MongoDB in this application, I’m going to use a MongoDB CodeIgniter driver that I wrote some time ago, it’s just a wrapper of the MongoDB PHP driver to mimic the framework’s SQL ActiveRecord. You can find the source files for this driver in my public repository. For this driver to work properly, make sure that you have the PHP’s MongoDB driver installed, if you don’t, follow these steps to get it working.

Please note that explaining the drivers in CodeIgniter and such is out of the scope of this tutorial, refer to the documentation if you have any doubts. You just need to move the "mongo_db.php" in the "config" folder to the "config" folder of your application and the "Mongo_db" folder in the "libraries" folder to the "libraries" folder in your application.

Database Configuration

The only file we need to edit at this point is the "mongo_db.php" file under the "config" folder, since my mongo installation has all the default parameters, I’m just going to edit line 40 and give it the name of the database that I want to use:

  $config['mongo_db'] = 'billboard';  

That’s it for the database, one of the many advantages of MongoDB is that the documents have no predefined structure, so it works without us needing to set anything up before using it, our database doesn’t even have to exist, MongoDB will create it on the fly when we need it.


Global Configuration

Other than your regular configuration options, that should include the base_url and the index_page if any, we need to set the string and date helpers to autoload. I’m not going to walk you through this, since we have much more to cover, when in doubt refer to the documentation.

Other than the helpers, we need to set up the encryption class since we’re going to use it for our app.


URL Handling

This is going to be a RESTful service and we need a way to take the requests coming to the server and handle them accordingly. We could use an existing library (which is great by the way) but for the purposes of this demonstration, I’m going to create the functionality I need using CodeIgniter’s core features.

Handling RESTful Requests

In particular, we’re going to use the ability to extend the core classes. We will start with the Controller, for the main part of this extension we’re using the "_remap" method in the base controller so all the controllers of our app can use it. Start by creating a MY_Controller.php file inside the "core" folder in the "application" folder, we create this just like any other CodeIgniter controller, as follows:

  <?php  if( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );    class MY_Controller extends CI_Controller {    }  

Now in this controller we’re going to use the CodeIgniter _remap method to preprocess every request made to the server. Inside the class we just created, add the following method:

  public function _remap( $param ) {      $request = $_SERVER['REQUEST_METHOD'];        switch( strtoupper( $request ) ) {          case 'GET':              $method = 'read';              break;          case 'POST':              $method = 'save';              break;          case 'PUT':              $method = 'update';              break;          case 'DELETE':              $method = 'remove';              break;          case 'OPTIONS':              $method = '_options';              break;      }        $this->$method( $id );  }  

A couple of things to note here, first off, there are some REST verbs that we are ignoring (like PATCH), since I’m demonstrating building a REST app, I don’t want to add things that may make this more complex than it needs to be. Secondly, we’re not taking into account the case where a controller doesn’t implement a particular method, which is very likely that this could happen. Now, we could add a default method to handle such requests, but so that we don’t add too much complexity, let’s leave it like this. Third, we’re receiving a param variable in the method declaration, let’s address that, and then I’ll explain the OPTIONS request. Above the switch statement, add the following code:

  if ( preg_match( "/^(?=.*[a-zA-Z])(?=.*[0-9])/", $param ) ) {      $id = $param;  } else {      $id = null;  }  

This regular expression matches any string consisting of uppercase and lowercase letters and any numbers. This is used to check if a MongoDB _id string is being given as a parameter, again, this is not the safest way nor the most thorough check, but for the sake of simplicity, we’ll keep it as is.

OPTIONS Request

Since we’re building a web service and a client application as separate parts, it makes sense that both are going to be hosted on different domains, so we will enable CORS in the back-end and this means, among other things, that our app will respond properly to OPTIONS requests.

When a web app created with BackboneJS (and some other frameworks) tries to make an asynchronous request to a remote server, it sends an OPTIONS request before sending the actual request it’s supposed to send. Among other things, the client tells the server from where it is sending the request, what type of request it is about to send, and the content that it’s expecting. After that, it is up to the server to send the client a response where it acknowledges the request or rejects it.

Since our back-end service, no matter which controller is called, is going to receive this OPTIONS request, it makes sense to implement the method to respond to it in our base controller. Add the following method below (or above) the _remap method in our controller.

  private function _options() {      $this->output->set_header( 'Access-Control-Allow-Origin: *' );      $this->output->set_header( "Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS" );      $this->output->set_header( 'Access-Control-Allow-Headers: content-type' );      $this->output->set_content_type( 'application/json' );      $this->output->set_output( "*" );  }  

Ideally, we would only allow some domains to make requests to us, we would check the request_headers header to see if we accept it and we would check for the expected content type, by the client to see if we support it, but again, this is a not-so-complex app and we are skipping these edge cases.


Managing Output

To finish our base controller, let’s create a method that every controller will use to send its results back to the client. In the base controller class, add the following method:

  protected function _format_output( $output = null ) {      $this->output->set_header( 'Access-Control-Allow-Origin: *' );        if( isset( $output->status ) && $output->status == 'error' ) {          $this->output->set_status_header( 409, $output->desc );      }      $this->_parse_data( $output );        $this->output->set_content_type( 'application/json' );      $this->output->set_output( json_encode( $output ) );  }  

Again, in order for BackboneJS to process the server response it has to know that its host is accepted by the server, hence the Allow-Origin header, then, if the result is a faulty one, we set a status header indicating this. This status will become more clear when we create the back-end models. Next we use the parse_data helper, which will be a private method (that we will write in a moment) but let me skip that for the time being, then we set the content type as JSON and finally we encode the response as a JSON object. Again, here we could (and should) support other output formats (like XML).

Now let’s create the parse_data helper method (and I’ll explain it afterwards), add the following code to the base controller:

  private function _parse_data( &$data ) {      if ( ! is_array( $data ) && ! is_object( $data ) )          return $data;        foreach ( $data as $key => $value ) {          if ( is_object( $value ) || is_array( $value ) ) {              if( is_object( $data ) ) {                  $data->{$key} = $this->_parse_data( $value );              } else {                  $data[ $key ] = $this->_parse_data( $value );              }          }            if ( isset( $value->sec ) ) {              if( is_object( $data ) ) {                  $data->{$key} = date( 'd.m.Y', $value->sec );              } else {                  $data[ $key ] = date( 'd.m.Y', $value->sec );              }          }            if ( is_object( $value ) && isset( $value->{'$id'} ) ) {              if( is_object( $data ) ) {                  $data->{$key} = $value->__toString();              } else {                  $data[ $key ] = $value->__toString();              }          }      }        return $data;  }  

First off, note that we only parse the data for arrays and objects, and we’re doing it recursively. This pre-parsing has to do with the fact that MongoDB uses dates and IDs as objects, but our clients don’t need this information. Now for the case of IDs, we just need its string value, hence the toString method call, then the value has an ‘$id’ property. Afterwards we are converting the dates to a day.month.year format, this is being done for convenience in the design of the client application, again, not the most flexible approach but it works for this example.


Handling Input

Since we’re sending JSON back to the client application, it is only logical that we accept data in JSON format as well. CodeIgniter doesn’t support this by default like Laravel does, as a matter of fact, CodeIgniter doesn’t even support put and delete params. This is mainly because the framework is not intended for a RESTful service, however the effort that it takes to adapt it is minimal compared to the benefits, at least from my point of view.

So we will start by supporting the JSON data that BackboneJS sends. Create a new file inside the "core" folder, this time it is going to be named "MY_Input.php" and it will have the following basic structure:

  <?php  if( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );    class MY_Input extends CI_Input {    }  

Now every time we use $this->input in our application we’ll be referring to this class, we will create some new methods and override a few existing ones. First off, we are going to add the support for JSON data, add the following method to the new class.

  public function json() {      if ( !self::$request_params ) {          $payload    = file_get_contents( 'php://input' );            if ( is_array( $payload ) ) {              self::$request_params   = $payload;          } else if ( ( substr( $payload, 0, 1 ) == "{" ) && ( substr( $payload, ( strlen( $payload ) - 1 ), 1 ) == "}" ) ) {              self::$request_params   = json_decode( $payload );          } else {              parse_str( $payload, self::$request_params );          }      }        return (object) self::$request_params;  }  

$request_params is a static variable used to store the request string/data sent by the client. It is static in order to make it object independent so that we can access it from any controller at any given time. The data is obtained from the php://input stream rather than the $_POST global. This is done in order to obtain the data sent in via PUT and DELETE requests as well. Finally, the obtained payload is inspected to check if it’s an array, a JSON encoded object, or a query string, and it’s processed accordingly. The result is then returned as an object.

For this method to work, we need to create the static $request_params variable, add its declaration to the top of the class.

  private static $request_params  = null;  

Handling Regular Requests

Next, we need to override the post method of the regular input class to use the new JSON payload instead of the $_POST global, add the following method to the new Input class.

  public function post( $index = NULL, $xss_clean = FALSE ) {      $request_vars   = ( array ) $this->json();      if ( $index === NULL && !empty( $request_vars ) ) {          $post       = array();          foreach( array_keys( $request_vars ) as $key ) {              $post[$key]  = $this->_fetch_from_array( $request_vars, $key, $xss_clean );          }          return $post;      }      return $this->_fetch_from_array( $request_vars, $index, $xss_clean );  }  

This is almost the same as the post method from the original CI_Input class, with the difference being that it uses our new JSON method instead of the $_POST global to retrieve the post data. Now let’s do the same for the the PUT method.

  public function put( $index = NULL, $xss_clean = FALSE ) {      $request_vars   = ( array ) $this->json();      if ( $index === NULL && !empty( $request_vars ) ) {          $put = array();          foreach( array_keys( $request_vars ) as $key ) {              $put[$key]   = $this->_fetch_from_array( $request_vars, $key, $xss_clean );          }          return $put;      }      return $this->_fetch_from_array( $request_vars, $index, $xss_clean );  }  

And then we also need the DELETE method:

  public function delete( $index = NULL, $xss_clean = FALSE ) {      $request_vars   = ( array ) $this->json();      if ( $index === NULL && !empty( $request_vars ) ) {          $delete = array();          foreach( array_keys( $request_vars ) as $key ) {              $delete[$key]   = $this->_fetch_from_array( $request_vars, $key, $xss_clean );          }          return $delete;      }      return $this->_fetch_from_array( $request_vars, $index, $xss_clean );  }  

Now technically, there’s really no need for these additional methods, since the post method can handle the params in the PUT and DELETE requests, but semantically it’s better (in my opinion).

This is all we need for our custom Input class. Again we’re ignoring edge cases here, like multipart requests, even though it’s not very hard to handle those and still maintain the functionality obtained here, but, for the sake of simplicity we’ll keep it just the way it is.


Base Model

To end the extension of the core classes, let’s create a base model that every model in the app will extend upon, this is just to avoid repetition of common tasks for every model. Like any other core class extension, here’s our barebones base model:

  <?php  if( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );    class MY_Model extends CI_Model {    }  

This base model will only serve the purpose of setting and retrieving errors. Add the following method to set a model error:

  protected function _set_error( $desc, $data = null ) {      $this->_error           = new stdClass();      $this->_error->status   = 'error';      $this->_error->desc     = $desc;      if ( isset( $data ) ) {          $this->_error->data = $data;      }  }  

As you can see, this method uses an instance variable $error, so let’s add its declaration to the top of our base model class.

  protected $_error;  

Finally, to keep it structured, let’s create the getter method for this property.

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

Handling Sessions

Session Controller

For the last part of this tutorial, we will create the controller and model to handle user sessions.

The controller for our session is going to respond to any POST request made to our Session resource, since the session can’t be retrieved after creation, nor updated directly, this controller will only respond to POST and DELETE requests. Please note that sending any other request to the resource will result in a server error, we’re not dealing with edge cases here but this could be easily avoided by checking if the method that’s called exists in our MY_Controller file and setting a default method name if the resource doesn’t support the request.

Below you’ll find the structure for our Session controller:

  <?php  if ( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );    class Session extends MY_Controller {        public function __construct() {}        public function save() {}        public function remove( $id = null ) {}  }  

Note that this controller is extending the MY_Controller class instead of the regular CI_Controller class, we do this in order to use the _remap method and other functionality that we created earlier. OK, so now let’s start with the constructor.

  public function __construct() {      parent::__construct();        $this->load->model( 'session_model', 'model' );  }  

This simple constructor just calls its parent constructor (as every controller in CodeIgniter must do) and then loads the controller’s model. The code for the save method is as follows.

  public function save() {      $result = $this->model->create();      if ( !$result ) {          $result = $this->model->get_error();      }      $this->_format_output( $result );  }  

And then the code for the remove method:

  public function remove( $id = null ) {      $result = $this->model->destroy( $id );      if ( !$result ) {          $result = $this->model->get_error();      }      $this->_format_output( $result );  }  

Both methods simply delegate the task at hand to the model, which handles the actual data manipulation. In a real world application, the necessary data validation and session checking would be done in the controller, and the common tasks such as session checking should be implemented in the base controller.

Session Model

Now let’s move on to the session model. Here is its basic structure:

  <?php  if ( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );    class Session_Model extends MY_Model {        public function __construct() {}        public function create() {}        public function destroy( $id ) {}  }  

Like the controller, this model extends the MY_Model class instead of the regular CI_Model class, this is being done in order to use the common methods that we’ve created earlier. Again, let’s start with the constructor.

  public function __construct() {      $this->load->driver( 'mongo_db' );  }  

In this case, we just load the Mongo_db driver, that we discussed earlier. Now we’ll continue with the method in charge of destroying the session.

  public function destroy( $id ) {      $filters    = array( '_id' => $this->mongo_db->gen_id( $id ) );        $query     = $this->mongo_db->get_where( 'sessions', $filters );      if ( $query->num_rows() == 0 ) {          $this->_set_error( 'INVALID_CREDENTIALS' );          return false;      }        $this->mongo_db->remove( 'sessions', $filters );      return 'SESSION_TERMINATED';  }  

In this method we check if there’s a session for the given session_id, and if so we attempt to remove it, sending a success message if everything goes OK, or setting an error and returning false if something goes wrong. Note that when using the session_id we use the special method $this->mongo_db->gen_id, this is because like I mentioned earlier, IDs in MongoDB are objects, so we use the id string to create it.

Finally, let’s write the create method which will wrap up part one of this tutorial series.

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

First of all, we check that there’s a user associated with the given email. Then we decode the user’s associated salt (which I’ll explain in the second part of this series when we cover user registration) and check that the given password matches the user’s stored password.

We then remove any previous session associated with the user and create a new session object. If we were checking the session thoroughly, we would add things like the user_agent, ip_address, last_activity field and so on to this object. Finally, we send back to the client the session and user IDs for the new session.


Conclusion

This has been a rather long tutorial, we covered a lot of topics, and we have even more to cover yet. Hopefully by now you have a better understanding of RESTful or stateless services and how to create such a service with CodeIgniter, and possibly, you may have also picked up some new ideas that you can give to the framework’s core functionality.

In the next part we will finish the back-end service and in parts three and four we’ll cover the BackboneJS client application. If you have any doubts/suggestions or anything to say, please do so in the comments section below.

No hay comentarios: