Rails 3 Routes Configuration – Dynamic Segments, Constraints and Scope

I’ve been messing around with Rails for the past few months, and I just got through figuring this out so I thought I’d post this here.

Sometimes you want to have a dynamic segment in a route. For example: /:username, to allow your users to have vanity urls. You might use something like this:

  get '/:username' => 'profiles#show'

However, you obviously don’t want routes like /login or /logout to be routed to your controller (in the above case, the profiles controller). In this case we would use segment constraints. Rails allows you to use either a regular expression as shown in this rails guide on segment constraints.

The other method is to use a Constraint class:

  get '/:username' => 'profiles#show', :constraints => ProfileConstraint

Reference the ProfileConstraint class by creating a new directory/file called app/constraints/profile_constraint.rb:

class ProfileConstraint
  def self.matches?(request)
    reserved_words = ['login', 'signup', 'signout', 'welcome', 'home']
    !reserved_words.include?(request.path_parameters[:username])
  end
end

Above, Rails looks for the matches? method in ProfileConstraint sends it a request object, which we then use to decide whether to match the route or not by comparing it against an array of reserved_words.

Another useful way to add on to the above would be to use scope. This allows you to extend your route like /:username/comments:

scope '/:username', :constraints => ProfileConstraint do
  get '' => 'profiles#show'
  get '/comments' => 'profile#comments'
end

And now you’ll have routes like so:

          GET        /:username(.:format)                   {:controller=>"profiles", :action=>"show"}
following GET        /:username/comments(.:format)         {:controller=>"profiles", :action=>"comments"}

Hope someone finds this useful as I did.

Tips for improving:
- Dynamically add model/controller classnames and routes to the reserved_words array.

Posted in Uncategorized | 1 Comment

Extending CodeIgniter’s Active Record Class

If you’re here you’ve probably realized by now that CodeIgniter 2.0 doesn’t officially support extending the core Active Record class, which is a bit of a drag.

As you may know, CI 2.0 came with a new core/ folder that allows you to simply drop classes to extend the CI core with.

For example, to extend the base CI_Loader class, you would just drop in MY_Loader.php into the /application/core folder, and all is set and done. Of course, you’d make sure that the ‘subclass_prefix’ config variable is set to ‘MY_’. This is the default configuration.

$config['subclass_prefix'] = 'MY_';

Being able to just drop in subclasses in the core/ folder is pure awesome, because you don’t need to modify the core CI classes to extend its functionality, and you keep everything separate in each /application instance.

You may think you can just drop in a file called “MY_DB_active_rec.php” into the /application/core folder, but this is not the case. It will not work, as verified by Phil Sturgeon himself in this StackOverflow post.

To do so requires a teeny bit of hacking of the core CI files. If you don’t like the idea of doing so, you may want to just head home now.

Still with me? The good news is that this only requires a few lines of code change. Here we go:

Locate the piece of code in

/system/database/DB.php

, starting on line 110 (as of CI 2.0.0):

require_once(BASEPATH.'database/DB_driver'.EXT);

if ( ! isset($active_record) OR $active_record == TRUE)
{
    require_once(BASEPATH.'database/DB_active_rec'.EXT);

    if ( ! class_exists('CI_DB'))
    {
        eval('class CI_DB extends CI_DB_active_record { }');
    }
}

This is the bit of code that loads CI’s active record class. Replace it with this:

require_once(BASEPATH.'database/DB_driver'.EXT);

if ( ! isset($active_record) OR $active_record == TRUE)
{
    require_once(BASEPATH.'database/DB_active_rec'.EXT);
   
    // get the CI instance
    $CI = & get_instance();
    $prefix = $CI->config->item('subclass_prefix');
   
    if (file_exists(APPPATH.'core/'.$prefix.'DB_active_rec'.EXT))
    {
        require_once(APPPATH.'core/'.$prefix.'DB_active_rec'.EXT);
        if ( ! class_exists('CI_DB'))
        {
            eval('class CI_DB extends '.$prefix.'DB_active_record { }');
        }
    }
    else
    {
        if ( ! class_exists('CI_DB'))
        {
            eval('class CI_DB extends CI_DB_active_record { }');
        }
    }
}

What happens here is that it will check the /application/core/ folder to see if a “MY_DB_active_rec.php” file exists (where MY_ is the prefix name you specified in your config file), and if it does, it loads that instead of the CI_DB_active_record class.

Next you’ll also need to extend the CI_Loader class to override the database() method, as well as the specific database driver that you’ll be using. In my case I extended the CI_DB_mysql_driver to override the _insert() method to allow the “INSERT DELAYED” feature of MySQL.

I’ve included a zip file of the changes. The files below were meant for CodeIgniter 2.0.0. CI 2.0.1+ shouldn’t be much more different, just get into the file and look around to make the necessary adjustments.

Download extend_ci_active_record.zip

Posted in Uncategorized | 3 Comments

Dynamically combine and minify your Javascript and CSS files with CodeIgniter

Update 10/27/2011: This method is outdated. I recommend using the very awesome Carabiner for your minification/concatenation needs.

If you still want to stick to this method, I would recommend implementing some sort of caching for your minified/concatenated files. Carabiner does this for your automatically, however!

——–
As any good UI developer will tell you, one of the best practices for speeding up your website is to minimize HTTP requests. Since most of the user’s loading time is tied up in things like loading scripts, images, flash, stylesheets, etc., reducing the number of these requests can drastically help improve loading time. It also helps to Gzip the components as well, which we’ll also cover in this tutorial.

This first tutorial will cover how to concatenate and minify your javascript and CSS files into a single HTTP request, using the CodeIgniter framework. For sites that load lots of scripts like jQuery plugins, this can be very beneficial for performance. Additionally, minification of scripts and stylesheets can help reduce the size of files, saving you bandwidth and shortening loading times for user.

So, let’s get started!

NOTE: I am assuming in this tutorial that you are already using an .htaccess file to eliminate index.php from your URIs.

Create an assets directory

You’ll want to manage your static assets (js, css, images, etc) by placing them in a directory called /assets in your web root. Here’s what your CI folder structure should look like:

/assets
  /css
  /images
  /js
/system
  /application
  ...
index.php

You may have your application/ folder elsewhere, but for this tutorial we’ll have it in the default location, inside the /system folder.

You’ll also need the below two libraries, which are essential to this tutorial:

Let’s grab copies of the following two libraries:

JSMin-PHP, Ryan Grove’s PHP port of Douglas Crockford’s JSMin.
Joe Scylla’s CssMin

Name these two files JSMin.php and CssMin.php, respectively, and place them in your application/libraries folder.

Create an asset_url() helper function

Having a helper function to access your assets URL can be helpful in the event you decide to rename or change the location of your application. It works like the base_url() function, only it will link to your assets rather than your base URL (obviously). In config.php, add the following code:

/*
|--------------------------------------------------------------------------
| Assets Path
|--------------------------------------------------------------------------
|
| URL Path to static assets library
|
*/

$config['asset_path'] = 'assets/';
$config['css_path'] = 'assets/css/';
$config['js_path'] = 'assets/js/';

We’ll be using css_path and js_path a little later in this tutorial, so keep those in mind.

Next, add this function in /system/helpers/path_helper.php

/**
 * Get asset URL
 *
 * @access  public
 * @return  string
 */

if (!function_exists('asset_url'))
{  
    function asset_url()
    {
        //get an instance of CI so we can access our configuration
        $CI =& get_instance();  
        //return the full asset path
        return base_url() . $CI->config->item('asset_path');
    }
}

Add the following to your config/autoload.php file, to helpers:

$autoload['helper'] = array('path');

Now you’re set! You can now call asset_url() anywhere to access your asset URL path. So instead of writing:

<img src="/assets/images/photo.jpg" alt="A Photo" />

You would write instead:

<img src="<?php echo asset_url(); ?> images/photo.jpg" alt="A Photo" />

Now that that’s out of the way, let’s get to the good stuff:

Create asset loader helper functions

In order to reduce our HTTP requests for our scripts to a single call, we’ll need a script tag that does the call for all files in one <script>tag. In your application/helpers folder, create a new file called asset_helper.php and insert the following code:

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

/**
 * Load JS
 * Creates the <script> tag that links all requested js files
 * @access  public
 * @param   array
 * @return  string
 */

if (!function_exists('load_js'))
{
    function load_js(array $files, $version = '')
    {
        //check if base url has a trailing slash, if not append it
        $base_url = substr(base_url(), -1) == '/' ? base_url() : base_url() . '/';
        foreach ($files as $index => $file)
        {
            //replace all backslash occurrences with tilde (~) to avoid URI confusion (gets converted back later in controller)
            $files[$index] = str_replace('/', '~', $file);
        }
        return '<script type="text/javascript" src="' . $base_url . 'loader/js/' . implode('|', $files) . '/' . $version . '"></script>';
    }
}

/**
 * Load CSS
 * Creates the <link> tag that links all requested css files
 * @access  public
 * @param   array
 * @return  string
 */

if (!function_exists('load_css'))
{
    function load_css(array $files, $version = '')
    {
        //check if base url has a trailing slash, if not append it
        $base_url = substr(base_url(), -1) == '/' ? base_url() : base_url() . '/';
        foreach ($files as $index => $file)
        {
            //replace all backslash occurrences with tilde (~) to avoid URI confusion (gets converted back later in controller)
            $files[$index] = str_replace('/', '~', $file);
        }
        return '<link type="text/css" rel="stylesheet" href="' . $base_url . 'loader/css/' . implode('|', $files) . '/' . $version . '" />';
    }
}


/* End of file asset_helper.php */
/* Location: ./system/application/helpers/asset_helper.php */

These helpers will be used on pages that you need to load specific JS or CSS files. Usage would be:

$files_to_load = array('file1', 'file2', 'file3'); //filenames WITHOUT the .js extension!
echo load_js($files_to_load);

You might want to also add this helper to your autoload.php file as well, just like the path helper above. Just add it to the array.

Create a loader controller

In order to actually load our asset files, we’ll need a controller that handles the concatentation and minification of the scripts and files. Create a new file in your application/controllers directory called loader.php with the following code:

<?php
/**
 * loader.php
 */


load_class('CSSMin', false);
load_class('JSMin', false);

class Loader extends Controller {

    var $asset_output;
    var $type;
    var $files;
    var $asset_path;
    var $ext;
    var $content_type;
    var $modified_time;

    function Loader()
    {
        parent::Controller();
        $this->modified_time = 0;
    }

    function js($files)
    {
        $this->type = 'js';
        $this->files = $files;
        $this->asset_path = $this->config->item('js_path');
        $this->ext = '.js';
        $this->content_type = 'text/javascript';

        $this->_output();
    }

    function css($files)
    {
        $this->type = 'css';
        $this->files = $files;
        $this->asset_path = $this->config->item('css_path');
        $this->ext = '.css';
        $this->content_type = 'text/css';

        $this->_output();
    }

    function _output()
    {
        $files_array = explode("|", $this->files);
        foreach ($files_array as $key => $file)
        {
            //replace chars for folder separation, replace ~ with /
            $file = str_replace('~', '/', $file);
            if (file_exists($this->asset_path . $file . $this->ext))
            {
                $this->asset_output .= file_get_contents($this->asset_path . $file . $this->ext)."\n";
                $this->modified_time = max(filemtime($this->asset_path . $file . $this->ext), $this->modified_time);
            }
        }

        //replace occurrences of /assets/ with asset_url()
        //$this->asset_output = str_replace("/assets/", asset_url(), $this->asset_output);

        switch ($this->type)
        {
            case 'js':
                $this->asset_output = JSMin::minify($this->asset_output);
                break;
            case 'css':
                $this->asset_output = CSSMin::minify($this->asset_output);
                break;
            default:
                throw new LoaderException("Unknown file type.");
                break;
        }

        //gzip
        if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
            if (stristr($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false) {
                $this->asset_output = gzencode($this->asset_output);
                header('Content-encoding: gzip');
            } else if (stristr($_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') !== false) {
                $this->asset_output = gzdeflate($this->asset_output);
                header('Content-encoding: deflate');
            }
        }

        //headers
        header('Content-type: ' . $this->content_type);
        header('Last-modified: ' . date('r', $this->modified_time));
        header('Expires: ' . date('r', time() + 2592000));
        header('Content-length: ' . strlen($this->asset_output));

        echo $this->asset_output;
    }
}

/* End of file loader.php */
/* Location: ./system/application/controllers/loader.php */

And that’s it!

Usage examples:

Javascript:

<?php echo load_js(array('jsfile1', 'dir/jsfile2' , 'jsfile3')); ?>

CSS:

<?php echo load_css(array('cssfile1', 'dir/cssfile2', 'cssfile3')); ?>

To load files inside a directory, for example, assets/js/plugins/example.js, you would include 'plugins/example' inside your the array as shown above.

If you update your css/js, you can use the option $version parameter. Simply use an incrementing integer value of your choice and the browser will read the file as a new file rather than loading the cached file.

Happy coding!

Posted in Uncategorized | 13 Comments

Twitter Search API, tweet IDs, json_decode, & bigint

Today I realized that my Tweet parsing application was saving a bunch of floating numbers as the status ID. After some investigation I realized that Twitter changed their

id

parameter in the Search API response to use a

bigint

. Since our data was returned using JSON, PHP’s native

json_decode

automatically converts the ID to a floating point value, like

1.0034070453002E+15

. Not cool.

If you are finding you’re having this problem when using

json_decode()

in general, and you’re running a development version of PHP 5.3+, you can use the bitmask

JSON_BIGINT_AS_STRING

in your

json_decode()

call to output any bigints as a string.

Example usage:

json_decode($json, false, 512, JSON_BIGINT_AS_STRING);

Anyway Twitter has now added an

id_str

field in the search response. So if you’re using PHP and you need to store or output the status ID, you should use this value instead:

{
    from_user_id_str: "52138619"
    profile_image_url: http://a3.twimg.com/profile_images/403904047/BC_color_logo_normal.jpg
    created_at: "Sat, 13 Nov 2010 03:00:56 +0000"
    from_user: "BCSportsNews"
    id_str: "3281128077139968"
    metadata: {
        result_type: "recent"
    }
    to_user_id: null
    text: "MSOC: North Carolina has defeated Boston College, 1-0, to advance to the ACC Championship game."
    id: 3281128077139968
    from_user_id: 52138619
    geo: null
    iso_language_code: "en"
    to_user_id_str: null
    source: "<a href="http://twitter.com/">web</a>"
}
Posted in Uncategorized | Leave a comment

I know what I’m going to be for Halloween next year

A Daft Punk original to boot.

Posted in Uncategorized | Leave a comment

A very useful snippet

Ever catch yourself trying to deciper a

var_dump()

or a

print_r()

?

Just add the

&lt;pre&gt;

tag to it and all will make sense.

echo '<pre>';
print_r($someVar);
echo '</pre>';

Enjoy!

Posted in Uncategorized | Leave a comment