Table of Contents
Open Table of Contents
Refinery CMS における Engine
Refinery CMS の機能を拡張するには、Engine と呼ばれるものを追加する。
Engine という言葉を使っているが、その実体は、RAILS_ROOT/vendor/engines
に配置されて動作する Rails アプリのミニ版であり、独自のルーティング、コントローラ、ビューなどを持たせることができる。ディレクトリ構成も同様のものとなる。
Refinery 本体も Engine で構成されており、以下の 7 つの Engine で構成されている。
- authentication
- core
- dashboard
- images
- pages
- resources
- settings
独自の機能を Engine を使って作成する
Refinery は scaffold のような Engine の足場を生成するための generator の機能を用意してくれている。 詳細は以下のサイトが参考になる。
- Getting Started with Refinery - Guides - Refinery CMS
- doc/engines.md at master from resolve’s refinerycms - GitHub
ここでは、上記のガイドにそって、イベントを追加できる機能を Engine で作成する。
これから作成しようとしているイベントの情報管理に必要な項目は以下の4点になる。
- イベントのタイトル
- イベントの日時
- 写真
- イベントのちょっとした宣伝文句
rails generate refinery_engine
コマンドを使用して、イベント用の Engine を作成する。簡単なヘルプは以下で確認できる。
$ rails generate refinery_engine
Usage:
rails generate refinery_engine NAME [field:type field:type] [options]
Runtime options:
-s, [--skip] # Skip files that already exist
-f, [--force] # Overwrite files that already exist
-p, [--pretend] # Run but do not make any changes
-q, [--quiet] # Supress status output
Description:
Generates a custom plugin for Refinery automatically. It works very similar
to the Rails scaffold generator.
A generated plugin gives you all the basic files needed to manage the model
the plugin will be for.
The first attribute should always be the one which is the title or name of
the model.
There must be at least one attribute.
Additional Supported Field Types
All field types that are supported by the Rails Scaffold generator are supported with the addition
of these Refinery specific ones:
text - text area with a visual editor
image - link to an image picker dialogue
resource - link to a resource picker dialogue
Example:
rails generate refinery_engine product title:string description:text image:image brochure:resource
rails generate model
と同じようなフィールド指定になるが、この generator は管理画面のフィールドまで自動で生成してくる。
指定したフィールドのタイプによって、管理画面では以下のフィールドが自動生成される。
text
WYSIWYG エディタで編集可能な複数行のテキストフィールドimage
画像のアップロード、既にアップロード(登録)済みの画像を選択する画面が表示されるresource
リソース(ファイル)のアップロード、既にアップロード(登録)済みのファイルを選択する画面が表示されるstring
andinteger
標準的な 1 行のインプットフィールドdatetime
,date
andtime
標準的な日付、日時の選択フィールド
実行してみる。
$ rails generate refinery_engine event title:string date:datetime photo:image blurb:text
create vendor/engines/events/app/controllers/admin/events_controller.rb
create vendor/engines/events/app/controllers/events_controller.rb
create vendor/engines/events/app/models/event.rb
create vendor/engines/events/app/views/admin/events/_form.html.erb
create vendor/engines/events/app/views/admin/events/_events.html.erb
create vendor/engines/events/app/views/admin/events/_event.html.erb
create vendor/engines/events/app/views/admin/events/_sortable_list.html.erb
create vendor/engines/events/app/views/admin/events/edit.html.erb
create vendor/engines/events/app/views/admin/events/index.html.erb
create vendor/engines/events/app/views/admin/events/new.html.erb
create vendor/engines/events/app/views/events/index.html.erb
create vendor/engines/events/app/views/events/show.html.erb
create vendor/engines/events/config/locales/en.yml
create vendor/engines/events/config/locales/lolcat.yml
create vendor/engines/events/config/locales/nb.yml
create vendor/engines/events/config/locales/nl.yml
create vendor/engines/events/config/routes.rb
create vendor/engines/events/db/migrate/create_events.rb
create vendor/engines/events/db/seeds/events.rb
create vendor/engines/events/features/manage_events.feature
create vendor/engines/events/features/step_definitions/event_steps.rb
create vendor/engines/events/features/support/paths.rb
create vendor/engines/events/lib/generators/refinerycms_events_generator.rb
create vendor/engines/events/lib/refinerycms-events.rb
create vendor/engines/events/lib/tasks/events.rake
create vendor/engines/events/readme.md
create vendor/engines/events/refinerycms-events.gemspec
create vendor/engines/events/spec/models/event_spec.rb
------------------------
Now run:
bundle install
rails generate refinerycms_events
rake db:migrate
------------------------
最後の指示に従って、コマンドを実行していく。
$ bundle install
...
$ rails generate refinerycms_events
create db/migrate/20110224062315_create_events.rb
create db/seeds/events.rb
------------------------
Now run:
rake db:migrate
------------------------
$ rake db:migrate
(in /home/taka/Dropbox/work/try-refinery)
== CreateEvents: migrating ===================================================
-- create_table(:events)
-> 0.0017s
-- add_index(:events, :id)
-> 0.0007s
== CreateEvents: migrated (0.3079s) ==========================================
Gemfile
を見てみると、現在作成した Engine が、path 指定での gem として登録されたことがわかる。
$ git diff Gemfile
diff --git a/Gemfile b/Gemfile
index c166aa8..024b813 100644
--- a/Gemfile
+++ b/Gemfile
@@ -57,3 +57,5 @@ end
gem 'refinerycms-i18n', '~> 0.9.9.9'
# END USER DEFINED
+
+gem 'refinerycms-events', '1.0', :path => 'vendor/engines'
ちなみに、上記コマンドを実行したことによる変更点は以下の通りとなっていた。
$ git st
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: Gemfile
# modified: Gemfile.lock
# modified: db/schema.rb
# modified: index/development/RefinerySetting/size
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# db/migrate/20110224062315_create_events.rb
# db/seeds/events.rb
# index/development/RefinerySetting/100_101_102.ind
# vendor/engines/
一旦ここでサーバを再起動して、http://localhost:3000/events
にアクセスしてみる。
Events のメニューが勝手に追加されている。また管理画面の方を見てみると、
こちらも Events タブが追加され、編集機能ができあがっている。試しに “Add New Event” をクリックすると、
画像もアップロードできるようになっている。大したもんだ。。
この時点でイベントの情報に関する基本的な CRUD の機能は実装済みとなっている。
RAILS_ROOT/vendor/engines/events/
配下に Rails アプリそのものが出来上がっているので、カスタマイズはそこで自由にやれる。
作成された Engine の仕組みを少しみてみる
試しに、generator で自動で作成された EventsController
を見てみる。
RAILS_ROOT/vendor/engines/events/app/controllers/events_controller.rb
:
class EventsController < ApplicationController
before_filter :find_all_events
before_filter :find_page
def index
# you can use meta fields from your model instead (e.g. browser_title)
# by swapping @page for @event in the line below:
present(@page)
end
def show
@event = Event.find(params[:id])
# you can use meta fields from your model instead (e.g. browser_title)
# by swapping @page for @event in the line below:
present(@page)
end
protected
def find_all_events
@events = Event.find(:all, :order => "position ASC")
end
def find_page
@page = Page.find_by_link_url("/events")
end
end
上記の index アクション、show アクションの中で、通常の Rails では現れないインスタンスメソッドの present
が使用されている。継承元の ApplicationController
は、Refinery の作成されたプロジェクト内では空のままだ。これはどこで定義されているのだろうか?
実体は、refinerycms-core
の gem 内で確認できる。
GEM_PATH/gems/refinerycms-core-0.9.9.3/lib/refinery/application_controller.rb
:
module Refinery
module ApplicationController
def self.included(controller)
controller.send :include, ::Refinery::ApplicationController::InstanceMethods
controller.send :include, ::Refinery::ApplicationController::ClassMethods
end
module ClassMethods
def self.included(c) # Extend controller
c.helper_method :home_page?,
:local_request?,
:just_installed?,
:from_dialog?,
:admin?,
:login?
c.protect_from_forgery # See ActionController::RequestForgeryProtection
c.send :include, Crud # basic create, read, update and delete methods
c.send :before_filter, :find_pages_for_menu,
:show_welcome_page?
c.send :after_filter, :store_current_location!,
:if => Proc.new {|c| c.send(:refinery_user?) rescue false }
if Refinery.rescue_not_found
c.send :rescue_from, ActiveRecord::RecordNotFound,
ActionController::UnknownAction,
ActionView::MissingTemplate,
:with => :error_404
end
end
end
module InstanceMethods
def admin?
...
end
def error_404(exception=nil)
...
end
def from_dialog?
...
end
def home_page?
...
end
def just_installed?
...
end
def local_request?
...
end
def login?
...
end
protected
# get all the pages to be displayed in the site menu.
def find_pages_for_menu
...
end
# use a different model for the meta information.
def present(model)
presenter = (Object.const_get("#{model.class}Presenter") rescue ::Refinery::BasePresenter)
@meta = presenter.new(model)
end
...
上記の Refinery::ApplicationController
が Mix-In されることで、コントローラから使用できるようになっている。
作成されたコントローラの before_filter
で Event の全データを引いている点と合わせて、present
メソッドでやっていることを確認したいと思ったのだが、追々確認することとして、この Refinery::ApplicationController
が ::ApplicationController
に Mix-In される流れを追ってみた。
以下のような流れだった。
RAILS_ROOT/Gemfile
では Refinery CMS に関する Gem は、
gem 'refinerycms', '= 0.9.9.3'
で読まれることになる。内容は以下の通り。
GEM_PATH/gems/refinerycms-0.9.9.3/lib/refinerycms.rb
:
%w(base core settings authentication dashboard images pages resources).each do |engine|
require "refinerycms-#{engine}"
end
Refinery の分割された各種 gem たちを require
している。
先ほどの Refinery::ApplicationController
は、GEM_PATH/gems/refinerycms-core-0.9.9.3/lib/refinerycms-core.rb
で読み込まれる。
GEM_PATH/gems/refinerycms-core-0.9.9.3/lib/refinerycms-core.rb
module Refinery
autoload :Activity, File.expand_path('../refinery/activity', __FILE__)
autoload :Application, File.expand_path('../refinery/application', __FILE__)
autoload :ApplicationController, File.expand_path('../refinery/application_controller', __FILE__)
autoload :ApplicationHelper, File.expand_path('../refinery/application_helper', __FILE__)
autoload :Plugin, File.expand_path('../refinery/plugin', __FILE__)
autoload :Plugins, File.expand_path('../refinery/plugins', __FILE__)
end
...
module Refinery
module Core
class << self
def attach_to_application!
::Rails::Application.subclasses.each do |subclass|
begin
subclass.send :include, ::Refinery::Application
rescue
$stdout.puts "Refinery CMS couldn't attach to #{subclass.name}."
$stdout.puts "Error was: #{$!.message}"
$stdout.puts $!.backtrace
end
end
...
end
...
class Engine < ::Rails::Engine
config.autoload_paths += %W( #{config.root}/lib )
# Attach ourselves to the Rails application.
config.before_configuration do
::Refinery::Core.attach_to_application!
end
さて、どこで Mix-In されるのだろうか?
Rails::Engine
を継承した Refinery::Core::Engine
クラスを見ると、Rails::Engine::Configuration
クラスの before_configuration
メソッドにて Refinery::Core.attach_to_application!
の呼び出しを行っているブロックが登録されている。名前からして、Rails の起動プロセスの中で実行されるのだろう。その実行時に、subclass.send :include, ::Refinery::Application
が実行されている。Refinery::Application
がインクルードされた時のフック処理に仕掛けがありそうだ。
GEM_PATH/gems/refinerycms-core-0.9.9.3/lib/refinery/application.rb
:
module Refinery
module Application
class << self
def refine!
::ApplicationHelper.send :include, ::Refinery::ApplicationHelper
[::ApplicationController, ::Admin::BaseController].each do |c|
c.send :include, ::Refinery::ApplicationController
c.send :helper, :application
end
::Admin::BaseController.send :include, ::Refinery::Admin::BaseController
end
...
def included(base)
self.instance_eval %(
def self.method_missing(method_sym, *arguments, &block)
#{base}.send(method_sym)
end
)
# JavaScript files you want as :defaults (application.js is always included).
base.config.action_view.javascript_expansions[:defaults] = %w()
# Configure the default encoding used in templates for Ruby 1.9.
base.config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
base.config.filter_parameters += [:password, :password_confirmation]
# Specify a cache store to use
base.config.cache_store = :memory_store
# Include the refinery controllers and helpers dynamically
base.config.to_prepare do
::Refinery::Application.refine!
end
...
いた。Refinery::Application.refine!
で、::ApplicationController
に Refinery::ApplicationController
を Mix-In している。Refinery::Application.refine!
は、Refinery::Application
がインクルードされる時に動作するフックメソッドを見てみると、Rails::Application::Configuration
クラスの to_prepare
メソッドにてその呼び出しがブロックとして登録されている。これも名前からして Rails の起動時のプロセスで処理されるのだろう。
Rails3 をまだちゃんと把握できておらず、起動プロセスの流れが曖昧なので、完全に読み切れてはいないが、この手の CMS でカスタマイズを行う場合、全体的な構成と仕組みをある程度頭に入れておくと触り易くなる。 折を見ながらまた覗いてみよう。