Baking cakes with CakePHP.

  • Default views for extensions

    I’m currently revamping the API of NoseRub to use CakePHP’s Router::parseExtensions() magic, i.e. if you append an extension like “.json” to the action you request, then CakePHP automagically sets the correct content type, and uses extension-specific layouts and views to render the result (e.g. if you request /example/action.json then the layout app/views/layouts/json/default.ctp and the view app/views/example/json/action.ctp are used by default).

    That’s nice, however, in our case all JSON views are identical (the same applies for the XML views):

    // requires the JSON part from the Zend framework
    App::import('Vendor', 'json', array('file' => 'Zend'.DS.'Json.php'));
    echo Zend_Json::encode(array('data' => $data));
    

    And so following the default approach would lead to a lot of duplication and many unnecessary folders and view files. And that’s not that cool ;-)

    After some experimentation I found a better way.

    For each extension I created a folder with a default view:

    app
      views
        json
          default.ctp
        xml
          default.ctp
    

    json/default.ctp contains the code I have shown above, and xml/default.ctp would look like:

    echo $xml->serialize(array('data' => $data), array('format' => 'tags'));
    

    To tell CakePHP it should render those default views when there is not a more specific view, I added the following beforeRender() callback method to the AppController:

    // app/app_controller.php
    class AppController extends Controller {
        public function beforeRender() {
            $pathToViewFile = dirname(__FILE__).DS.'views'.DS.$this->viewPath.DS.$this->action.'.ctp';
    		
            if (!file_exists($pathToViewFile)) {
                $this->viewPath = $this->layoutPath;
                $this->action = 'default';
            }
        }
    }
    

    Happy baking!

    3 comments

  • Whenever

    Most web applications have to perform some periodic tasks. For this purpose, you usually define some cron jobs.

    With the “whenever” Ruby gem you can define those cron jobs directly in your Rails application.

    After installing the gem (with “gem install javan-whenever”) you either have to create the file “/config/schedule.rb” manually, or you can use “wheneverize” to generate this file (call “wheneverize .” from your project root). You can then add your application-specific cron jobs to this file.

    For example, let’s say you want to clean up the users table and remove all users who didn’t activate their accounts, then you would write something like:

    every 1.day, :at => '2am' do
      runner "User.cleanup_inactive_users"
    end
    

    This will execute the “cleanup_inactive_users” method of the “User” model daily at 2am (you could also use “every :friday do” to run it every friday, or “every 2.hours do” to run it every two hours, and so on) . The “runner” keyword allows you to execute Ruby code (it uses “script/runner”, hence the name). You can also use “command” to execute command line tools, or “rake” for running rake tasks.

    So far we simply described our cron job, though no cron job is active yet. To change this, we have to tell cron about our cron job with:

    whenever --update-crontab example
    

    “example” is a unique identifier for our application, and is used by “whenever” to determine which cron jobs it has to modify when you update “schedule.rb” and re-run the above command. It is probably best to put this command in the deployment script of your application to keep your cron jobs up-to-date.

    With “crontab -l” we can see the job(s) added by “whenever”:

    # Begin Whenever generated tasks for: example
    0 2 * * * /home/dho/projects/example/script/runner -e production "User.cleanup_inactive_users"
    # End Whenever generated tasks for: example
    

    With that, our cron job should be executed like any other cron job on our system.

    I hope I could give you a short overview about this quite useful gem, for more information see:
    http://github.com/javan/whenever
    ASCIIcasts “Episode 164 – Cron in Ruby”

    2 comments

  • Specifying helpers for static pages

    In a recent comment I got asked how you can reference Javascript files from static pages (i.e. from files in app/views/pages) as “you can’t add the JavaScript helper, because there isn’t a controller”.

    Well, the assumption that there is no controller is wrong. There is one, the PagesController, though by default it is “hidden” in cake/libs/controller (unless you baked your project with the bake script, then you will also find a PagesController in app/controllers).

    And this means you can specify the helpers you want to use in your static pages in the usual way: by adding them to the $helpers array of the PagesController resp. the AppController (app/app_controller.php), if you want to use the helpers in the views of more than one controller. Please do not edit the PagesController in the cake/libs/controller directory, but copy it to app/controllers and make the changes there.

    0 comments

  • Given-When-Then

    I’m currently doing my first steps with BDD (Behavior Driven Development) and Cucumber, a Ruby tool for doing BDD.

    What I like about BDD is the simple, but powerful concept of Given-When-Then for specifying scenarios. It is a concept you can also use without doing BDD to specify the requirements of your application.

    It simply defines the structure of how you write down the requirements in the form of scenarios (as a sidenote: in BDD, a scenario belongs to a feature. For example, a “Login” feature might consist of the scenarios “Login with username/password” and “Login with OpenID”):

    Given some initial context (the givens),
    When an event occurs,
    Then ensure some outcomes.
    

    (from http://dannorth.net/introducing-bdd)

    “Given” defines the preconditions, “When” defines what happens, and “Then” defines the result of the scenario. Or in other words: “Given” describes the start state, and “When” the steps necessary to reach the accepting state (= “Then”).

    A simple example of a “Publish new article” scenario might look like:

    Given I am logged in
    When I write an article with the title "My article"
    And I publish the article
    Then I should see the message "Article published"
    And the article "My article" should appear on the homepage
    

    The simplicity of this format makes it easy to discuss the requirements of your application. And thanks to tools like Cucumber you can even make such specifications executable, but that’s something for another post…

    4 comments

  • Migration from Rails 2.2 to 2.3

    Recently, I migrated from Rails 2.2 to 2.3. After installing the new Rails gem I generated a dummy project to compare the generated project structure with the structure of a Rails 2.2 project to find out what changed.

    And those are the things I had to change (though most changes seem to be optional):

    • Renaming app/controllers/application.rb to app/controllers/application_controller.rb
    • Replacing config/boot.rb, as it was slightly modified
    • Adding the new “reconnect” option for MySQL databases to config/database.yml (see the release notes)
    • Moving the settings for “config.action_controller.session” and “config.action_controller.session_storage” from config/environment.rb to the new file config/initializers/session_store.rb
    • Changing the value of RAILS_GEM_VERSION from 2.2.2 to 2.3.2 in config/environment.rb
    • Copying config/initializers/backtrace_silencers.rb to my project
    • Adding the setting “config.action_view.cache_template_loading = true” to the production and testing environments (config/environments/production.rb resp. test.rb)
    • Removing the dispatch scripts from the “public” directory, the script/process directory (and its content), plus the “request” script from script/performance

    A detailed overview of the changes/new features of Rails 2.3 can be found in the release notes.

    4 comments

© daniel hofstetter. Licensed under a Creative Commons License