Module: Engines::RailsExtensions::Dependencies

Included in:
ActiveSupport::Dependencies
Defined in:
vendor/plugins/engines/lib/engines/rails_extensions/dependencies.rb

Overview

One of the magic features that that engines plugin provides is the ability to override selected methods in controllers and helpers from your application. This is achieved by trapping requests to load those files, and then mixing in code from plugins (in the order the plugins were loaded) before finally loading any versions from the main app directory.

The behaviour of this extension is output to the log file for help when debugging.

Example

A plugin contains the following controller in plugin/app/controllers/my_controller.rb:

  class MyController < ApplicationController
    def index
      @name = "HAL 9000"
    end
    def list
      @robots = Robot.find(:all)
    end
  end

In one application that uses this plugin, we decide that the name used in the index action should be "Robbie", not "HAL 9000". To override this single method, we create the corresponding controller in our application (RAILS_ROOT/app/controllers/my_controller.rb), and redefine the method:

  class MyController < ApplicationController
    def index
      @name = "Robbie"
    end
  end

The list method remains as it was defined in the plugin controller.

The same basic principle applies to helpers, and also views and partials (although view overriding is performed in Engines::RailsExtensions::Templates; see that module for more information).

What about models?

Unfortunately, it’s not possible to provide this kind of magic for models. The only reason why it’s possible for controllers and helpers is because they can be recognised by their filenames ("whatever_controller", "jazz_helper"), whereas models appear the same as any other typical Ruby library ("node", "user", "image", etc.).

If mixing were allowed in models, it would mean code mixing for every file that was loaded via require_or_load, and this could result in problems where, for example, a Node model might start to include functionality from another file called "node" somewhere else in the $LOAD_PATH.

One way to overcome this is to provide model functionality as a module in a plugin, which developers can then include into their own model implementations.

Another option is to provide an abstract model (see the ActiveRecord::Base documentation) and have developers subclass this model in their own application if they must.


The Engines::RailsExtensions::Dependencies module includes a method to override Dependencies.require_or_load, which is called to load code needed by Rails as it encounters constants that aren’t defined.

This method is enhanced with the code-mixing features described above.

Class Method Summary

Instance Method Summary

Class Method Details

+ (Object) included(base)

:nodoc:



71
72
73
# File 'vendor/plugins/engines/lib/engines/rails_extensions/dependencies.rb', line 71

def self.included(base) #:nodoc:
  base.class_eval { alias_method_chain :require_or_load, :engine_additions }
end

Instance Method Details

- (Object) require_or_load_with_engine_additions(file_name, const_path = nil)

Attempt to load the given file from any plugins, as well as the application. This performs the ‘code mixing’ magic, allowing application controllers and helpers to override single methods from those in plugins. If the file can be found in any plugins, it will be loaded first from those locations. Finally, the application version is loaded, using Ruby’s behaviour to replace existing methods with their new definitions.

If Engines.disable_code_mixing == true, the first controller/helper on the $LOAD_PATH will be used (plugins’ app directories are always lower on the $LOAD_PATH than the main app directory).

If Engines.disable_application_code_loading == true, controllers will not be loaded from the main app directory if they are present in any plugins.

Returns true if the file could be loaded (from anywhere); false otherwise - mirroring the behaviour of require_or_load from Rails (which mirrors that of Ruby’s own require, I believe).



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'vendor/plugins/engines/lib/engines/rails_extensions/dependencies.rb', line 93

def require_or_load_with_engine_additions(file_name, const_path=nil)
  return require_or_load_without_engine_additions(file_name, const_path) if Engines.disable_code_mixing

  file_loaded = false

  # try and load the plugin code first
  # can't use model, as there's nothing in the name to indicate that the file is a 'model' file
  # rather than a library or anything else.
  Engines.code_mixing_file_types.each do |file_type| 
    # if we recognise this type
    # (this regexp splits out the module/filename from any instances of app/#{type}, so that
    #  modules are still respected.)
    if file_name =~ /^(.*app\/#{file_type}s\/)+(.*_#{file_type})(\.rb)?$/
      base_name = $2
      # ... go through the plugins from first started to last, so that
      # code with a high precedence (started later) will override lower precedence
      # implementations
      Engines.plugins.each do |plugin|
        plugin_file_name = File.expand_path(File.join(plugin.directory, 'app', "#{file_type}s", base_name))
        if File.file?("#{plugin_file_name}.rb")
          file_loaded = true if require_or_load_without_engine_additions(plugin_file_name, const_path)
        end
      end
  
      # finally, load any application-specific controller classes using the 'proper'
      # rails load mechanism, EXCEPT when we're testing engines and could load this file
      # from an engine
      unless Engines.disable_application_code_loading
        # Ensure we are only loading from the /app directory at this point
        app_file_name = File.join(RAILS_ROOT, 'app', "#{file_type}s", "#{base_name}")
        if File.file?("#{app_file_name}.rb")
          file_loaded = true if require_or_load_without_engine_additions(app_file_name, const_path)
        end
      end        
    end 
  end

  # if we managed to load a file, return true. If not, default to the original method.
  # Note that this relies on the RHS of a boolean || not to be evaluated if the LHS is true.
  file_loaded || require_or_load_without_engine_additions(file_name, const_path)
end