FightinJoe : Aaron Wheeler

Distributed MVC

Friday, 18 April 2008

Rails literally interprets the MVC structure in the /app directory with the models, views, and controllers directories. These directories are essential to how Rails runs, as it eschews configuration for the context that these directories and their files provide.

Merb follows suit; all default Merb apps respect the MVC structure Rails pioneered. But Merb allows the defaults to be overwritten, allowing for open interpretation of MVC’s physical structure.

merb-gen app supports the default, flat, and very-flat app generation. And in the init.rb file, a custom directory structure can be setup; see the inline docs on the bootloader, line 144, for more details.

But what if static customization of the MVC structure isn’t enough? What if you need dynamic customization?

Customizing custom

Encapsulated functionality is a perfect example of why the MVC file structure needs might need to be customized. What if you want the same functionality in multiple applications? Authentication, for example? There is a great generator that will solve this problem. But what if you want a self-contained gem you can use in any application with only one ‘dependency’ line?

The Rails answer to this is to use Engines – a plugin that extends Rails to make it easy for plugins to access and extend Rails’ MVC setup.

So where are Engines for Merb? They’re included in merb-core for free!

Engines basically gives the ability to register new models, controllers, views, and routes. How is this done in Merb?

Merb Controllers

When Merb needs a controller, it doesn’t look in a direction – it looks at Merb::Controller, the class from which all controllers must be descendants. And the reason why they must be descendants? Because inheritance is how Merb tracks the controllers.

On line 25 of merb_controller.rb there is this neat little trick:

1
          2
          3
          4
          5def inherited(klass)
            _subclasses << klass.to_s
            self._template_root = Merb.dir_for(:view) unless self._template_root
            super
          end
          

When Merb is started, all of the controllers are loaded, and as each one is loaded, it’s class name is saved in the class accessor _subclasses. Then, when a request is dispatched (line 51), the requested controller is looked up in the same array.

So how does one add a dynamic controller to Merb? By loading a class that inherits from Merb::Controller. This is as simple as require ‘myController’.

Merb Views

Merb views are handled in a unique way – when Merb is started, all views are compiled into methods on their respective controller. So adding new views takes more than just specifying a new file to look for.

Merb::Controller uses the instance method _template_location to resolve which views to load. Override this method in a controller and view path can be specified. For example, this is how a flat Merb app specifies a custom view folder:

1
          2
          3def _template_location(action, type = nil, controller = controller_name)
            controller == "layout" ? "layout.#{action}.#{type}" : "#{action}.#{type}"
          end
          

The only ‘gotcha’ here is that _template_location is scoped to the view directory. To get around this, one only needs to engineer a way to back out of the directory:

1
          2
          3
          4
          5
          6def _template_location(action, type = nil, controller = controller_name)
            undo   = Merb.load_paths[:view].first.gsub(%r{[^/]+}, '..')
            prefix = File.dirname(__FILE__)
            file   = controller == "layout" ? "layout.#{action}.#{type}" : "#{action}.#{type}"
            File.join( '.', undo, prefix, 'views', file )
          end
          

The above code changes the view directory to be relative to the current file, not the pre-specified view root.

Merb models

Models in Merb are treated like any regular library. Just as with controllers, if a model has been loaded, Merb will have access to it.

Merb routes

Merb routes can be extended at any time with Merb::Router.append and Merb::Router.prepend:

1
          2
          3
          4
          5Merb::Router.prepend do |r|
            r.match('/').to(:controller => 'blogs', :action =>'index')
            r.resources :axes
          #  r.default_routes
          end
          

Make sure not to call Merb::Router.prepare as it will destroy all previously created routes.

Putting it all together

Knowing how to load all of the pieces, it’s an easy step to encapsulate everything together into a Merb gem. Using merb-gen plugin myGem to create the framework for packing together your funcationality, toss your files into the lib directory and set the gem to properly include your files and set your roots when included.

For an example of how it all fits together, take a look at MeX, a Gem that provides plug-and-play exception logging to any Merb app.

Comments

Comments are closed