Intuitive interfaces, amazing user experience, rock-solid back end, agile delivery, exceptional testing, features people will love, user-centric design

Backbone.js + PHP (with Silex microframework) + MySQL

06/January / 2012

Recently at Cambridge Software, we have been investigating different JavaScript based microframeworks for building rich applications that bring a desktop experience to the Web. Backbone.js is an excellent library, very lightweight, giving you a great deal of flexibility while covering all the foundation-type work. Backbone.js is built around the MVC pattern, it gives you base classes for models, controllers and views.

The minified version of Backbone.js measures only 4KB with the only hard dependency being Underscore.js. You can safely use jQuery with it (this becomes very helpful while working on your views).

There are several examples out there on how to use Backbone.js but most of them focus on its use with the local storage or Ruby on Rails. What you can find below is our prototype of the well-known Todos application working together with PHP and MySQL backend (with a great help from Silex microframework, and minimalist database toolkit Idiorm and Paris).

Once again, it is just a prototype and by no means it is a production ready code.

First, create the following table:

CREATE TABLE `todo` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` varchar(1000) DEFAULT NULL,
  `done` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=latin1$$

Second, create the Todo class extending the base Model class defined in Paris:

namespace CambridgeSoftware;  

class Todo extends Model {
  
}

Third, implement the API (PHP + Silex + Idiorm + Paris + MySQL). Here, we implement basic CRUD for the above Todo class:

require_once __DIR__.'/vendor/silex/silex.phar';

$app = new Silex\Application();

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Silex\Provider\MonologServiceProvider;

$app['autoloader']->registerNamespace('CambridgeSoftware', __DIR__.'/libs');

$app->register(new CambridgeSoftware\ParisServiceProvider(), array(
  'paris.dsn'      => 'mysql:host=localhost;dbname=silextest',
  'paris.username' => 'root',
  'paris.password' => ''
));

// GET 	/{resource}/{id} 	Show
$app->get('/api/todos/{id}', function ($id) use ($app) {
  $todo =  $app['paris']->getModel('Todo')->find_one($id);
 
  return new Response(json_encode($todo->as_array()), 200, array('Content-Type' => 'application/json'));
});

// POST 	/{resource} 	Create
$app->post('/api/todos', function (Request $request) use ($app) {
  $data = json_decode($request->getContent());
  
  $todo =  $app['paris']->getModel('Todo')->create();
  $todo->title = $data->title;
  $todo->save();
 
  return new Response(json_encode($todo->as_array()), 200, array('Content-Type' => 'application/json'));
});

// PUT 	/{resource}/{id} 	Update
$app->put('/api/todos/{id}', function ($id, Request $request) use ($app) {
  $data = json_decode($request->getContent());
  
  $todo =  $app['paris']->getModel('Todo')->find_one($id);
  $todo->title = $data->title;
  $todo->save();
 
  return new Response('Todo updated', 200);
});

// DELETE 	/{resource}/{id} 	Destroy
$app->delete('/api/todos/{id}', function ($id) use ($app) {
  $todo =  $app['paris']->getModel('Todo')->find_one($id);
  $todo->delete(); 
 
  return new Response('Todo deleted', 200);
});

Finally, get a copy of the todo sample application from here and change the TodoList definition to use the above API as opposed to the local storage:

  window.TodoList = Backbone.Collection.extend({

    model: Todo,

    url: "api/todos",  

    done: function() {
      return this.filter(function(todo){ return todo.get('done'); });
    },

    remaining: function() {
      return this.without.apply(this, this.done());
    },

    nextOrder: function() {
      if (!this.length) return 1;
      return this.last().get('order') + 1;
    },

    comparator: function(todo) {
      return todo.get('order');
    }

  });

Job done! All your actions in the front end (adding, updating, removing todos) should now be flowing to the todo database table.

UPDATED 13/January/2012:

To secure your API, you can use the Silex's 'before' filter. This allows you to execute code before every single request. Here is an example showing how this can be combined with HybridAuth. HybridAuth makes it easy to authenticate users with a wide variety of well known social networks (Twitter, Facebook and many others).

 

    $app->before(function ( Request $request ) use ( $app ) 
    {
        // Is this page 'secure'?
        if ( !in_array($request->getPathInfo(), $app['public_routes']) ) 
        {
            // Are you not authenticated?
            if ( !$app['hybrid_auth']->isAuthenticated() )
            {
                if  ( in_array($request->getPathInfo(), $app['redirect_to_signin_routes']) ) 
                {
                    // Some pages should redirect to signin
                    return new RedirectResponse( $app['base'] . '/signin' );
                }
                else
                {
                    // Return 401 (this is for API)
                    return new Response( 'Not authenticated', 401 );            
                }
            }
        }
    });

UPDATED 16/February/2012:

For those interested, here is our implementation of the paris service provider used in the above example.

    namespace Cs\SilexExtensions;
    
    use Silex\Application;
    use Silex\ServiceProviderInterface;
    
    use Cs\Utils as u;
        
    
    require_once __DIR__.'/../../../vendor/Idiorm/idiorm.php';
    require_once __DIR__.'/../../../vendor/Paris/paris.php';
    
    
    
    class ParisExtension implements ServiceProviderInterface
    {
        public function register(Application $app)
        {
            $app['paris'] = $app->share(function () use ($app) {
                \ORM::configure($app['paris.dsn']);
                \ORM::configure('username', $app['paris.username']);
                \ORM::configure('password', $app['paris.password']);
                \ORM::configure('logging', true );
                \ORM::configure('driver_options', array(
                    \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
                ));
    
                return new ParisWrapper();
            });
        }
    }
    
    class ParisWrapper
    {
    	public function getModel($modelName)
    	{
    		return \Model::factory($modelName);
    	}
        
        public function getLastQuery()
        {
            return \ORM::get_last_query();
        }
        
        public function getQueryLog()
        {
            return \ORM::get_query_log();
        }
    }

Example usage:

 $todo =  $app['paris']->getModel('Todo')->find_one($id);

 

Share