Realtime backbone.js apps with fuelphp and pusher

GFX9.COM share Realtime backbone.js apps with fuelphp and pusher, you can download now.



Today we're going to create a small realtime web application with FuelPHP on the server, Backbone.js on the client and using the Pusher API for the realtime aspects. The application we'll be building is a modern take on my classic "Shoutbox" PHP demo application I originally wrote six years ago here on Nettuts+, and covered again not long after in Ruby on Rails.

This time, it's a Backbone.js take on the application and we're bringing into the year 2012 with live syncing between users without refreshing the page!


An Overview

We'll be using FuelPHP to provide a Messages API to our application. We'll have a REST API endpoint at /api/messages which will serve up the latest messages ("Shouts") on a GET request, and will create a new message on a POST request.

Our front-end will use Backbone.js to provide a seamless UI and render the messages.

When the 'Submit' button is clicked, Backbone will create a new 'Message' model, causing the list of messages to re-render. The new message will be POSTed to the API where it will be added to the database.

Then, after a successful database save, we will use Pusher to send the new message out to everyone else using the application where Backbone will then add it to their collection of messages causing the list to be updated.

This will serve as an introduction to the realtime nature which can set your web application apart from others.

I recently implemented a flow similar to this for InventoryBase, a new web application I'm leading the development of. We use Pusher to provide our app with a Live Activity Feed and to push down new Tasks and Properties when they're added by Clerks:


Let's Get Started

Download and extract FuelPHP into a directory on your web server, I've named mine fuelbbpusher because I'm so original ;)

Also create a new MySQL database, for example named fuelbbpusher with a table 'messages' with the following syntax:

CREATE TABLE `messages` ( 
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 
  `name` varchar(255) DEFAULT NULL, 
  `text` varchar(255) DEFAULT NULL, 
  `created_at` int(11) DEFAULT NULL, 
  `updated_at` int(11) DEFAULT NULL, 
  PRIMARY KEY (`id`) 
);
INSERT INTO `messages` (`id`, `name`, `text`, `created_at`, `updated_at`) 
VALUES 
    (1, 'Dan Harper', 'Hello, World! :)', 1336336484, 1336336484), 
    (2, 'The Cat', 'Meow.', 1336343404, 1336343404), 
    (3, 'Bob Jones', 'Hey there ;)', 1336343429, 1336343429);

If you're new to FuelPHP, know that everything related to your PHP app lives in /fuel/app. Your CSS, JavaScript, images and the index.php 'launchpad' all sit inside /public. So if you're running the app inside a directory on your localhost you'll need to point your browser to the public directory.


If you're using a VirtualHost, set the base path to the public directory.

Alternatively, move everything inside public up a level and adjust all mentions of realpath(__DIR__.'/../fuel/ inside index.php to realpath(__DIR__.'/fuel/.

Now, set your DB settings inside fuel/app/config/development/db.php. For example:

 array( 
        'connection'  => array( 
            'dsn'        => 'mysql:host=localhost;dbname=fuelbbpusher', 
            'username'   => 'root', 
            'password'   => '', 
        ), 
    ), 
);

And at the bottom of fuel/app/config/config.php un-comment 'orm' inside the always_load => packages array so we can use Fuel's ORM package to ease in database queries.

Finally, add a new file to fuel/app/config/ named rest.php with the following content to change the default REST format from XML to JSON:

     'json', 
    );

Load up your web browser and you should see Fuel's Welcome page:


CSS, JavaScript Assets

Inside /public/assets/js create the following JavaScript files with the referenced code inside:

  • jquery.js => jQuery 1.7.2
  • underscore.js => Underscore 1.3.3
  • backbone.js => Backbone 0.9.2
  • handlebars.js => Handlebars 1.0.beta.6
  • pusher.js => Pusher 1.12.1
  • app.js (this is where our custom app code will live)

And inside /public/assets/css create the following CSS files with the referenced code inside:

  • bootstrap.css => Twitter Bootstrap 2.0.3
  • main.css =>
body { 
    background-color: #323f66; 
    color: #fff; 
} 
 
h1, h2, legend { 
    color: #47d3ff; 
    text-shadow: 0 4px 5px #383456; 
    font-family: 'Fredoka One', sans-serif; 
    font-weight: normal; 
    text-align: center; 
} 
 
hgroup { 
    margin: 20px 0; 
} 
 
h1 { 
    font-size: 42px; 
    margin-bottom: 0; 
} 
 
hgroup p { 
    color: white; 
    font-size: 11px; 
    margin: 5px 0 0; 
    text-align: center; 
} 
 
hgroup p a,   
hgroup p a:hover {   
    color: white; 
} 
 
form { 
    margin: 0; 
} 
 
legend { 
    border: none; 
} 
 
h2, legend { 
    font-size: 22px; 
    line-height: 1.4em; 
    padding-bottom: 5px; 
} 
 
#form-shell, #list-shell { 
    background: #3f4c73; 
    border: 1px solid #2d395c; 
    border-radius: 3px; 
    margin: 0 0 20px; 
    padding: 20px; 
} 
 
input, textarea { 
    background: #313d60; 
    border: 1px solid #2d395c; 
    color: #fff; 
    width: 100% !important; 
} 
 
li { 
    margin-bottom: 10px; 
} 
 
.info { 
    text-align: center; 
}

Site Controller

We're going to need a controller to serve up the app. We'll call this controller 'Site'.

Inside the fuel/app/classes/controller/ directory, create site.php with the following content:


Nice and simple (If you don't have prior experience with Fuel, checkout the Getting Started tutorial). For the URL site/index (or just site/) we serve up the view file at fuel/app/views/site/index.php (which we'll create now).

Create a new file at fuel/app/views/site/ named index.php with the following HTML content:

 
 
 
     
    Live 
 
     
 
 
 
 
    

Shoutbox

Dan Harper for Nettuts+

The form will be here.

Message list will be here.

Nice and simple. A HTML5 document, with our assets included with Fuel's Asset Class and some Twitter Bootstrap-specific markup. Included is two "shell" divs which we will use to drop in the relevant segments of the application's Backbone Views.

It's a Default

We'll want to set our default route (eg. what's loaded when you go to the site root) to be our Site controller. You do this inside /fuel/app/config/routes.php. Set the __root__ value to site/index.

At this point, browsing to your app's directory in your browser should present you with:


Backboning

Let's get started with the core of our Backbone application. Inside /public/assets/js/app.js drop in the following code:

$(function() { 
 
    var app = window.app = {}; 
 
    // Router 
    app.Router = Backbone.Router.extend({ 
        routes: { 
            '*path': 'home' 
        }, 
 
        home: function() { 
            $('#list-shell').empty().append('Backbone has landed!'); 
        } 
    }); 
 
    // APP HERE 
 
    app.router = new app.Router(); 
    Backbone.history.start({ pushState: true }); 
    Backbone.emulateJSON = true; 
 
});

First off, we've created an app object to namespace our application to prevent us from flooding the global namespace with too many application-related variables. We've kept one reference of window.app to ease in debugging.

Following that we've got the core of our Router. We've specified that a route matching *path should run the home function in the Router. *path will match every route.

Inside the home route, we've simply swapped out the content of #list-shell with a message to demonstrate that Backbone is working successfully.

At the bottom, we've created a new instance of the Router. Take note of how we're using titlecase variable names to denote a class (eg. app.Router), and lowercase versions of that name for an instance of that class (eg. app.router). This is a common naming convention in JavaScript.

Next, we kick Backbone into full gear by starting the history module. We're using HTML5 pushState instead of hash navigation - however for this application, neither would be used; but it's a good default to use anyway.

And finally, we're telling Backbone to "emulate JSON". This means that when Backbone sends data to our server, instead of sending it as raw JSON it will fake it by passing the "stringified" data as part of a URL-encoded form. This is common with PHP frameworks like Fuel which would otherwise require a bit of hacking to include native JSON support.

Refresh your application and you should find that where the text "Message list will be here." is in the HTML source, we instead have "Backbone has landed!"

Rendering a Collection of Messages

Let's add a bit of meat into the app by implementing our Message model, Messages collection and a ListView to display the collection.

In place of the // APP HERE comment, start with the following:

// Model 
app.Message = Backbone.Model.extend({ }); 
 
app.message = new app.Message; 
 
 
// Collection 
app.Messages = Backbone.Collection.extend({ 
    model: app.Message 
}); 
 
app.messages = new app.Messages([ 
    { 
        id: 1, 
        name: 'Dan Harper', 
        text: 'Hello Nettuts+' 
    }, 
    { 
        id: 2, 
        name: 'Fred Jones', 
        text: 'Yadda yadda' 
    } 
]);

We start by creating a Backbone Model named Message. For now, this doesn't require any specific attributes. It will later contain functions for validation and the server URL. Then we've instantiated a new instance of the Message as app.message.

Following the Model, we have the Collection, aptly named Messages. We've only defined one attribute so far: the model which the collection is, well, a collection of.

Following, we've instantiated the collection. We've passed in an array of dummy data for now. Backbone will instantiate each item in the array as a new model in the collection. So now if we were to look into app.messages with our Console, we'd find two models.

In order to display that data on the screen, we need to do three things:

  • Write a HTML template which the collection will be inserted into
  • Create a Backbone View to render the template and provide it with the required data
  • Initialise the View in the Router and add the rendered template into the approriate portion of our HTML.

Starting with the template, we're going to use the Handlebars templating engine to build HTML around our data.

Handlebars Template

By default, Handlebars templates exist inside

We'll be providing Handlebars with our collection in JSON format for it to loop over.

The List View

Our Backbone View for the List will compile (load) our Handlebars template (by simply using jQuery: $('#template-list').html()) and give us a render() method which provides the template with the collection, and generates the HTML. Add the following into your app.js file:

// Views 
app.ListView = Backbone.View.extend({ 
    template: Handlebars.compile($('#template-list').html()), 
 
    initialize: function() { 
        _.bindAll(this, 'render'); 
    }, 
 
    render: function() { 
        this.$el.html(this.template({ 
            collection: this.collection.toJSON() 
        })); 
        return this; 
    } 
});

We've started by using Handlebars to compile our template into memory. The initialize() function is simply ensuring the correct "this" is used inside the render() function using Underscore's bindAll method. This is one of JavaScript's many quirks, and is pretty much boilerplate code when creating a Backbone View.

Inside the render function is where the magic happens. this.$el is a new helper variable in Backbone which simply references "a cached jQuery object of the view's element" (the HTML element the view represents). We're swapping out the HTML of that element with the result of the Handlebars template.

The compiled Handlebars template is provided with our collection in JSON format (all Backbone Collections and Models have a toJSON() method). You'll see in the next step how the view knows what this.collection is.

The only thing left to do now is initialise the ListView inside our Router and drop the rendered content into #list-shell.

Routed

Swap out the contents of your home function inside the Router object with the following:

this.views = {} 
 
this.views.list = new app.ListView({ 
    collection: app.messages 
}); 
$('#list-shell').empty().append(this.views.list.render().el);

As we already know that this route is going to have two views inside it - the form and the list - we've created a views object to store them in. We've then initialised our ListView inside it, and provided the view with our collection (app.messages).

We've then added the rendered content of the List View to #list-shell. If you give your page a refresh, you should see our two messages displayed in a list.


Form View

Now we've seen the process of creating one view, let's quickly run through adding our second Form View. Initially this will consist of the same three steps as the List View:

  • Write a HTML template containing a form with two fields and a button
  • Create a Backbone View to render the template
  • Initialise the View in the Router and add the rendered template into the approriate portion of our HTML

Later, we'll hook the form up to a "submit event", which once fired will add the submitted data to a new model in our collection.

Add the following Handlebars template to your HTML source (in the , ideally):

And create a new Backbone View for it inside app.js:

app.FormView = Backbone.View.extend({ 
    template: Handlebars.compile($('#template-form').html()), 
 
    initialize: function() { 
        _.bindAll(this, 'render'); 
    }, 
 
    render: function() { 
        this.$el.html(this.template({})); 
        return this; 
    } 
});

This is very similar to the ListView. We're grabbing the template with jQuery and compiling it with Handlebars. Inside the render function we're then setting the HTML content of the View's element to the result of the template. Note that we're not providing the template any data because it doesn't require any.

Finally, add the View to your page by adding the following to the end of the home method in the Router:

this.views.form = new app.FormView({ 
    collection: app.messages 
}); 
$('#form-shell').empty().append(this.views.form.render().el);

Nice and simple. Give your page a refresh and you should see the form now in place. Remember that submitting it doesn't actually do anything just yet.


API

Now we have a basic structure to our Backbone app, let's start on the API for it. Our API will provide two endpoints:

  1. GET /api/messages - Retrieve all messages in the database. By default in JSON format.

  2. POST /api/messages - Add a message to the database.

In a full-featured app we'd also have PUT and DELETE endpoints to be fully RESTful, but since the app we're writing isn't going to have editing or deleting support, we'll leave it out.

To start, create a new FuelPHP Controller for the API root inside /fuel/app/classes/controller/ named api.php with the following content:

response('Welcome to our API.'); 
    } 
 
}

There isn't much point to this file other than providing us with a point to expand from. Note that we're using Fuel's Rest Controller, which provides an excellent interface for writing RESTful APIs. In other Fuel Controllers, you will prefix your methods with action_ to signify this is a web-facing method. In a Rest Controller you prefix a method with either get_, post_, put_ or delete_ depending on the HTTP method the action should be used for.

The Rest Controller also enables you to display your data in a variety of different formats. You'll recall we previously set the default format to JSON in a config file, however this can be over-ruled by appending ?format=xml to an API URL in order to retrieve the data in XML format. Or try CSV. Or PHP.

Go to /api in your browser and you should see the message "This is the API.". Infact, append ?format=xml and instead you'll see:

 
    This is the API. 

GET /api/messages

Now we have a base to start from, create a new directory inside /fuel/app/classes/controller/ named api and inside that, a file messages.php with the following content:


We're extending our Controller_API_Messages class from Controller_API. For the purpose of this tutorial this provides nothing; however this allows you to specify functions inside the base API controller and use them across all your API controllers (such as access control).

Next we need a Model to access our database. FuelPHP's conventions state that a model should be in the singular, with the associated table in the database being in the plural (ie. message and messages). Create a new file at fuel/app/classes/model/ named message.php. Start with the following:


The above is pretty much standard boilerplate for the start of a FuelPHP model. We've listed the database fields for a message and included Fuel's CreatedAt and UpdatedAt observers - these automatically set the created_at and updated_at fields in the database.

Next, we need a function to get the 20 latest models from the database and return them in an array which our Messages Controller can use.

Add the following method to the Model_Message class:

public static function get_all() 
{ 
    $models = Model_Message::find() 
                ->limit(20) 
                ->get(); 
 
    $r = array(); 
    foreach ($models as $model) 
        $r[] = $model; 
 
    return $r; 
}

Nice and simple. We can now use this method in the controller. Go ahead and replace the line // return latest messages in the controller with:

return $this->response(Model_Message::get_all());

Now, if you go to /api/messages in your browser, you should get a JSON representation of the 3 records we have in the database:

[{"id":"1","name":"Dan Harper","text":"Hello, World! :)","created_at":"1336336484","updated_at":"1336336484"},{"id":"2","name":"The Cat","text":"Meow.","created_at":"1336343404","updated_at":"1336343404"},{"id":"3","name":"Bob Jones","text":"Hey there ;)","created_at":"1336343429","updated_at":"1336343429"}]

Append ?format=xml to that for an XML respresentation:

 
     
        1 
        Dan Harper 
        Hello, World! :) 
        1336336484 
        1336336484 
     
     
        2 
        The Cat 
        Meow. 
        1336343404 
        1336343404 
     
     
        3 
        Bob Jones 
        Hey there ;) 
        1336343429 
        1336343429 
     

Fetching from the API with Backbone

Now we have a working API for retrieving data, let's use it with Backbone. The first thing you need to do is define what URL Backbone should use. We define this on the Model, and on the Collection. So find our Message Backbone Model definition:

app.Message = Backbone.Model.extend({ });

And add in a url property like so:

app.Message = Backbone.Model.extend({ 
    url: 'api/messages', 
});

Do the same for the Collection, too:

app.Messages = Backbone.Collection.extend({ 
    model: app.Message, 
    url: 'api/messages', 
});

Now, where we instantiate the collection with dummy data, remove the dummy data and call fetch() on the object:

// before 
app.messages = new app.Messages([ 
    { 
        id: 1, 
        name: 'Dan Harper', 
        text: 'Hello Nettuts+' 
    }, 
    { 
        id: 2, 
        name: 'Fred Jones', 
        text: 'Yadda yadda' 
    } 
]); 
 
 
// after 
app.messages = new app.Messages(); 
app.messages.fetch();

At this point, if you were to refresh, you'll see that our List View claims there are no messages:

However if you open the Network Developer Panel in your browser, you should see a successful (status: 200) call to /api/messages, and the response is the JSON we expected:

And if you type app.messages.toJSON() into your Console, you'll see Backbone has infact loaded the JSON into models in our collection:

Backbone will not automatically re-render your views when new data is received. We need to explicitly tell it what to do. This is done by running ListView's render() method whenever the "reset" event is triggered on the collection (ie. whenever the collection's contents is replaced - which fetch() does).

In the initialize function of the ListView, add the following line:

this.collection.on('reset', this.render);

Give your browser a refresh and you should see the messages in the collection being displayed.


Adding Messages

Our "Next Big Leap" is actually being able to add messages. We'll start by writing the API endpoint to allow us to POST to api/messages with a name and some text which will be added to the database.

Inside your API Messages controller, add the following POST action:

public function post_index() 
{ 
    $data = json_decode(Input::post('model'), true); 
 
    if ( ! trim($data['name']) || ! trim($data['text'])) 
        return $this->response(array('message' => 'Name and Message should be provided.'), 400); 
 
    $params = array( 
        'name' => $data['name'], 
        'text' => $data['text'], 
    ); 
 
    $model = new Model_Message($params); 
 
    if ( ! $model->save()) 
        return $this->response(array('message' => 'Failed to save.'), 500); 
 
    return $this->response($model, 201); 
}

First off, we're decoding the JSON which will be POSTed inside the model parameter. You'll remember how we told Backbone to emulateJSON, and this is what it does. It POSTs some URL-Encoded JSON as part of the model parameter. After decoding the JSON we now have a normal array, which hopefully includes "name" and "text" keys.

We then check that "name" and "text" contain something. We're using trim() to ensure it's not just whitespace which has been submitted. If either of the fields are missing data, we return an error message with the HTTP status code 400 (Bad Request).

Next, we create a new Model_Message using the received data. Note how we're not just passing Model_Message everything inside $data like so:

    $data = json_decode(Input::post('model'), true); 
 
    $model = new Model_Message($data);

If we did, someone could pass in any other fields we don't want them to edit - a "mass-assignment vulnerability". This is a very simple concept, yet this vulnerability recently came to light in the Ruby on Rails framework.

Finally, we save the model. If the save failed, we return a "Failed to save." message with the HTTP status code 500 (Server Error). Otherwise, everything went OK, and so we return the whole model - which now includes an ID, and the Created At and Updated At times - with a HTTP status code of 201 (OK - Created).

Testing it out

Before we write the Backbone code, let's first be sure that the API is working correctly. Open up your console and enter the following code:

data = { 
    name: 'Dan Harper', 
    text: 'Sent from the API' 
}

A JavaScript object containing a name and message text. And the POST it to the server with:

$.ajax('api/messages', { 
    type: 'post', 
    contentType: 'application/x-www-form-urlencoded', 
    data: { 
        model: JSON.stringify(data) 
    } 
});

This AJAX call is exactly how Backbone will make this request. Hit return and check out the Network panel and you should see the request returned with a status code 201!

And under the Response sub-tab you should see the returned model:

{"name":"Dan Harper","text":"Sent from the API","updated_at":1337178638,"created_at":1337178638,"id":"4"}

Give your browser a refresh and your new message will be waiting there.

Tip: Try this again, but this time remove "name" or "text" from the data object being submitted. You should receive a 400 error code and our error message in JSON.


Hook up Backbone

To add a new message with Backbone we need to:

  1. Listen for the "submit" event on the #new-list-item form and intercept it
  2. Gather the inputted text for "name" and "text"
  3. create() a new model on the collection
  4. If the request was successul, clear the form's inputs
  5. If the request was unsuccessful, display an error message
  6. Instruct the ListView to listen for the "add" event on the collection and re-render itself

Add the following to the top of your FormView:

events: { 
    'submit #new-list-item': 'add' 
},

This is similar to writing $(document).on('submit', '#new-list-item', this.add);. Now start off the add function with:

add: function(e) { 
    e.preventDefault(); 
 
    var self = this 
    ,   $inputName = $('#new-name') 
    ,   $inputText = $('#new-text') 
    ,   params = { 
            name: $inputName.val() 
        ,   text: $inputText.val() 
        } 
    ; 
 
    // more here... 
}

We're simply preventing the form's default submit functions from executing and adding the "name" and "text" fields into an object we can submit.

Remember that we also need to bind "this" correctly inside the add function, so add the following to FormView's initialize function:

_.bindAll(this, 'add');

Now, create a new model in the collection with the params. Type the following to the bottom of the add function:

this.collection.create(params, { 
    wait: true, 
 
    success: function(model, response) { 
        $inputName.val(''); 
        $inputText.val(''); 
    }, 
 
    error: function(model, response) { 
        console.log('error', model, response); 
        try { 
            var json = $.parseJSON(response.responseText); 
            if ('message' in json) { 
                alert(json.message); 
                return; 
            } 
        } 
        catch(e) {} 
        alert('Unknown error occurred. Error ' + response.status + '.'); 
    } 
});

Calling create() on a collection is equivalent to instantiating a new model instance, saving it to the server and then adding it to the collection. We're passing in wait: true to instruct Backbone to wait for the server to respond before continuing (this was the default prior to Backbone 0.9 and I prefer this functionality).

Like $.ajax() you can also provide success and error functions. On success, we are clearing the form's inputs.


On an error, we are alerting the "message" contained in our error response's JSON. However, as the error could be caused by other issues than one we triggered ourselves in the API (such as a general server error, timeout etc.) we can't be sure that the response actually contains a JSON message and so we have wrapped the code in a try {} catch() {} block and otherwise alerting that an "Unknown error occurred."

Try it out in your browser. Enter a Name and Message, hit Submit and the inputs should clear. We haven't yet instructed to ListView to update when a new model is added to the collection, but if you refresh you should see it.

The final step here is to subscribe to the collection's "add" event and run the ListView's render function. Add the following to the ListView initialize:

this.collection.on('add', this.render);

Refresh your browser and try again. After submitting the form, the message will pop on top of the list!


Client-Side Validation

As of right now, if you leave either of the input fields blank (or filled with just whitespace), the API will return an error. This is fine, but ideally we want to predict any errors on the client without having to wait for a server response.

Backbone makes this very easy. You can define a "validate" method on a model which is provided with a model's data for you to test before the model is created. Add the following to the Backbone Message Model:

    validate: function (attrs) { 
        if ( ! $.trim(attrs.name)) 
            return { message: 'Name is required.' }; 
        if ( ! $.trim(attrs.text)) 
            return { message: 'Message is required.' }; 
    }

Here we're preforming a similar validation check to what we're doing on the server, and returning an object with a message if there's an error. If a model is valid, you return nothing and Backbone will go in its merry way in creating the model.

One last change you need to make is to the error callback on the collection.create inside FormView. Change the line:

var json = $.parseJSON(response.responseText);

To:

var json = $.parseJSON(response.responseText) || response;

This is so that the error message can be extracted from the client-side error object, too - which as it's already a JavaScript Object, doesn't need parsing from JSON.

Try out the app in your browser again, and if you try to submit either field as empty/just whitespace you'll get an error just for that field. Done!


Getting Real(time)

Our final hurdle is making our application realtime - instantly pushing new messages out to everyone with the app open. Personally, this concept always scared me. It sounds like a lot of work and the notion of realtime web apps is rarely associated with PHP. It's node's territory.

However, it turns out this is ridiculously easy to achieve thanks to Pusher. We include a few lines of JavaScript in our app to listen for new messages being pushed down and add them to the collection. On the server, whenever a new message is added to the database we run one line of code to send that data to Pusher and down to all the connected clients.

Firstly, you'll need to register yourself a Pusher account - their free plan includes 20 connections and 100,000 messages per day. Once you've signed up find the API Access section. You're going to need your app_id, key and secret.

Fuel Package

GitHub user Lembubintik has kindly released a FuelPHP package for the Pusher PHP API library: https://github.com/lembubintik/pusherapp. Go ahead and download it (there's a Zip download on the GitHub page if you're not fluent in Git) and ensure the directory is named pusherapp.

Drop this directry into fuel/packages/ so that your source tree looks something like this:

Copy the contents of fuel/packages/pusherapp/config/pusher.php and create a new file at fuel/app/config/pusher.php with the copied code inside. In this new file, enter your auth_key, secret and app_id.

Now, set this package to auto-load by going to fuel/app/config/config.php and inside always_load => packages add pusherapp, so it looks something like this:

'always_load'  => array( 
 
    /** 
     * These packages are loaded on Fuel's startup.  You can specify them in 
     * the following manner: 
     * 
     * array('auth'); // This will assume the packages are in PKGPATH 
     * 
     * // Use this format to specify the path to the package explicitly 
     * array( 
     *     array('auth' => PKGPATH.'auth/') 
     * ); 
     */ 
    'packages'  => array( 
        'orm', 
        'pusherapp', 
    ),

Publishing Pusher Events

We know that once a new message has been added to the database, we want to send out a Pusher event to all our connected clients. One method to do this is just to add it straight into our API Messages Controller after a successful save, like so:

public function post_index() 
{ 
    $data = json_decode(Input::post('model'), true); 
 
    if ( ! trim($data['name']) || ! trim($data['text'])) 
        return $this->response(array('message' => 'Name and Message should be provided.'), 400); 
 
    $params = array( 
        'name' => $data['name'], 
        'text' => $data['text'], 
    ); 
 
    $model = new Model_Message($params); 
 
    if ( ! $model->save()) 
        return $this->response(array('message' => 'Failed to save.'), 500); 
 
    // ==> Send Pusher Event Here... 
 
    return $this->response($model, 201); 
}

While this would work, it's not the most scalable solution. What if our app grew, and new Messages were being created elsewhere in the application? We'd have to add the Pusher code there, too. Then if we wanted to change some details about the Pusher code, we'll have to update it in multiple places, which isn't very efficient (or DRY).

Fuel provides a simple solution to this dilema through the use of Observers. These are special classes you connect to an ORM Model which fire off a function on certain events, eg. "beforeinsert", "afterinsert", "beforeupdate", "afterdelete" etc. By default in Fuel you are already using two observers all the time: CreatedAt and UpdatedAt.

You can imagine that CreatedAt binds to the "beforeinsert" event to add a created_at timestamp, and UpdatedAt binds to the "beforeupdate" event to update the updated_at timestamp.

We'll create our own observer named MessageCreated which will bind to the "afterinsert" event where it will then trigger a Pusher event.

Create a new directory at fuel/app/classes/ named observer, and inside that another directory named message with a file inside: created.php.

Inside this file, start with the following:

    

Pretty simple. Just extend from OrmObserver and name a function after the event you want to bind to. These functions are passed one argument: the Model.

Triggering a Pusher event is done like so:

$data = array('hello' => 'world!'); 
Pusherapp::forge()->trigger('CHANNEL_NAME', 'EVENT_NAME', $data);

A CHANNEL is literally that: like a radio frequency. On InventoryBase, we have private channels for each user account (user_1), and private channels for managers of an account area (management_1). For this app, we'll only have one channel: fuelbbpusher. Note that channels are nothing special, you don't need to register them with Pusher: just give them a name (they're automatically limited to your app_id, anyway).

An EVENT is the name of some sort of event which occurs in your app. On InventoryBase we have events like task_created and new_activity. For this app, new_message is perfect. (More on Channels and Events.)

The third parameter is simply data to send with the event. In our case this will be a representation of the new Message model.

Now we understand the concepts, replace // Trigger Pusher Here with the following:

Pusherapp::forge()->trigger('fuelbbpusher', 'new_message', Format::forge($obj)->to_array());

As described above, we're using the fuelbbpusher channel to publish a new_message event and we've used the Format class to convert our ORM Model object to an Array representation itself.

The final thing we need to do here is instruct Fuel to use our new Observer with our Message Model. Open fuel/app/classes/model/message.php and inside the $_observers array, add Observer_Message_Created.

Subscribing to Pusher Events

At this point whenever a new message is added to database, Pusher is sending out an event. You can see this by monitoring your Debug Console on Pusher.com and whenever a message is added you should see events being fired. All that's required now is to subscribe to those events and perform an action on them.

We already included the Pusher JavaScript library at the start of the tutorial so we can make use of it right away. At the bottom of your app.js file add in the following (obviously before the the last });):

// Pusher 
var pusher = new Pusher('YOUR_APP_ID'); 
var channel = pusher.subscribe('fuelbbpusher'); 
channel.bind('new_message', function(data) { 
    // do something with data 
});

Add in your app_id and we're now subscribed to the fuelbbpusher channel and listening for new_message events. All we need to do is do something with the data pushed down to us. That is, add it to our messages collection:

app.messages.add(data);

Thankfully, .add() on a collection does not automatically save the itself back to the server - otherwise we'd be in an endless loop of a message being added to the database, pushed out to clients who send it back to the server which adds it to the database and pushes it out to the clients who send it back to the server which… you get the point :)

Give your browser one final refresh. Open another browser window to the same page. In one window add a message, and watch how that message then appears in the other window. Welcome to the realtime web!

Download the up-to-date source files on GitHub.

>




submit to reddit


Similar content