activeadmin読んだ
activeadmin/activeadminを初めて使うことになったので、どういう仕組みになっているのか調べてみた。
TL;DR
rails g active_admin:install
を実行するとlib/generators/active_admin/install_generator.rbが実行され、ActiveAdmin.routes(self)
がconfig/routes.rbに追加される。- app/admin/以下にあるResource定義ファイル内で実行される
ActiveAdmin.register
では、ActiveAdmin::Resource
インスタンスが生成され、動的にResourceごとのcontrollerが生成される。それらはすべてActiveAdmin::ResourceController
を継承している。 - config/routes.rbに追加された
ActiveAdmin.routes(self)
は内部でapp/admin/以下のファイルをロードし(このタイミングで上述のActiveAdmin.register
が実行される)、ActiveAdmin::Resource
インスタンスから動的にroutingが定義される。
長いので、以下ActiveAdmin::
をAA::
と略記する。
Generator
Active Adminをセットアップするにはまずrails g active_admin:install
を実行する。
このとき、lib/generators/active_admin/install_generator.rbに定義されたRails::Generators::Base
のサブクラスにあるpublicメソッドが上から順番に実行される。Railsはlib/generators/**/*_generator.rbにマッチするファイルに定義されたRails::Generators::Base
のサブクラスをRails Generatorとして実行することができる。
# lib/generators/active_admin/install_generator.rb module ActiveAdmin module Generators class InstallGenerator < ActiveRecord::Generators::Base # ... def setup_routes if options[:user] inject_into_file "config/routes.rb", "\n ActiveAdmin.routes(self)", after: /devise_for .*, ActiveAdmin::Devise\.config/ else route "ActiveAdmin.routes(self)" end end # ... end end end
いくつかメソッドが定義されている中でsetup_routes
を見ると、config/routes.rbにActiveAdmin.route(self)
を追記しているようだ。self
はRails.application.routes.draw do ... end
のブロック内でのself
なのでActionDispatch::Routing::Mapper
インスタンスを表している。
Register a resource
Generatorでファイルの追加と変更を行ったあとは、管理画面で管理するResourceを作成する。例えば、rails g active_admin:resource Post
を実行すると以下のようなapp/admin/post.rbが生成される。
# app/admin/post.rb ActiveAdmin.register Post do end
このブロックの中にviewやcontrollerの設定を追加していくのだけど、まずActiveAdmin.register
の定義を調べる。
# lib/active_admin.rb module ActiveAdmin class << self # ... def application @application ||= ::ActiveAdmin::Application.new end # ... delegate :register, to: :application # ... end end
ActiveSupportが拡張したメソッドdelegate
によって、ActiveAdmin.register
の処理は実際にはAA::Application#register
が行っている。
# lib/active_admin/application.rb def register(resource, options = {}, &block) ns = options.fetch(:namespace){ default_namespace } namespace(ns).register resource, options, &block end
options[:namespace]
がなければdefault_namespace
つまり:admin
がns
に入る。#namespace
はnamespaces[ns]
があればそれを返し、なければAA::Namespace
インスタンスを初期化しnamespaces
に追加した上で返す。よって、AA::Namespace#register
が処理が渡っている。
# lib/active_admin/namespace.rb def register(resource_class, options = {}, &block) config = find_or_build_resource(resource_class, options) register_resource_controller(config) parse_registration_block(config, resource_class, &block) if block_given? reset_menu! ActiveAdmin::Event.dispatch ActiveAdmin::Resource::RegisterEvent, config config end
find_or_build_resource
はAA::Resource
インスタンスを返す。#register_resource_controller
は以下のように定義されており、Resource
インスタンスから動的にAA::ResourceController
を継承するResourceごとのcontrollerを定義している。
# lib/active_admin/namespace.rb def register_resource_controller eval "class ::#{config.controller_name} < ActiveAdmin::ResourceController; end" config.controller.active_admin_config = config end
parse_registration_block
は上述の例のapp/admin/post.rbでActiveAdmin.register
に渡されていたブロックを評価する部分だと思う。ブロックの中身を独自のDSLとして評価してカスタマイズを行っていると思う。
Routing
Generatorによってconfig/routes.rbに追加されたActiveAdmin.routes
の定義を調べる。
# lib/active_admin.rb module ActiveAdmin # ... def application @application ||= ::ActiveAdmin::Application.new end # ... delegate :routes, to: :application # ... end
delegate
はActiveSupportが拡張しているメソッドで、メソッドの呼び出しをto
で指定したオブジェクトに委譲する。なので、ActiveAdmin.routes
は実際にはAA::Application#routes
を指している。
# lib/active_admin/application.rb def routes(rails_router) load! router.apply(rails_router) end
load!
はapp/admin/**/*.rbをKernel.load
する。このとき上述したapp/admin/post.rbのようなResource定義ファイルがロードされる。そして、ActiveAdmin.register
が実行され各Resourceのcontrollerが定義される。
router
はRouter
インスタンスなので、Router#apply
を調べる。
# lib/active_admin/router.rb def apply(router) define_root_routes router define_resource_routes router end
まずdefine_root_routes
は以下のように定義されている。
# lib/active_admin/router.rb def define_root_routes(router) router.instance_exec @application.namespaces.values do |namespaces| namespaces.each do |namespace| if namespace.root? root namespace.root_to_options.merge(to: namespace.root_to) else namespace namespace.name do root namespace.root_to_options.merge(to: namespace.root_to) end end end end end
このrouter
はActionDispatch::Routing::Mapper
であり、@application.namespaces.values
はAA::Namespace
インスタンスの配列だ。
ActiveAdmin.register
に特にoptionを指定しない場合、namespace.root?
はtrue
となる。namespace.root_to_options
とnamespace.root_to
がどこで定義されているのか不明。。。なんだけど、AA::Application
内にこれらが定義されており、root_to_options
は{}
でroot_to
は"dashboard#index"
となっている。どのようにしてAA::Namespace
にそれらが定義されるのか不明ではあるが、おそらくこれらの値が使われるのだと思う。よって、結局このメソッドはroot to: "dashboard#index"
としているだけだ。
# lib/active_admin/router.rb def define_resource_routes(router) router.instance_exec @application.namespaces, self do |namespaces, aa_router| resources = namespaces.value.flat_map{ |n| n.resources.values } resources.each do |config| routes = aa_router.resource_routes(config) # ... instance_exec &routes end end end
config
は先述したAA::Resource
インスタンスだ。aa_router
はAA::Router
インスタンスなのでAA::Router#resource_routes
を見る。
# lib/active_admin/router.rb def resource_routes(config) Prox.new do build_route = proc{ |verbs, *args| [*verbs].each{ |verb| send verb, *args } } build_action = proc{ |action| build_route.call(action.http_verb, action.name) } case config when ::ActiveAdmin::Resource resources config.resource_name.route_key, only: config.defined_actions do member do config.member_actions.each &build_action end collection do config.collection_actions.each &build_action post :batch_action if config.batch_action_enabled? end end when ::ActiveAdmin::Page # ... else # ... end end end
このメソッドで返されるProcオブジェクトはActionDispatch::Routing::Mapper
インスタンスのコンテキストでinstance_exec
されるため、要するにこのProcオブジェクト内の処理がそのままconfig/routes.rb内のroutingの設定となる。Resourceインスタンスの情報から動的にroutingを組み立てているようだ。
所感
軽く触ってみたけど、Resource定義ファイルに独自DSLでviewを書いていくのが非常にカスタマイズが大変だし覚えることが多そうだな、あまり筋がよくなさそうという印象を受けた。
で、調べてみた結果、Resource定義ファイルから動的にcontrollerとかroutingとかを定義していて、それらをカスタマイズするのに独自DSLを使うという構図になっていることが分かった。管理画面って、ビジネスサイドの要求によってどんどんカスタマイズが必要になるので、カスタマイズに独自のDSLを覚えなくてはいけないとか、場合によってはカスタマイズできないみたいな状況は大きな問題だと思う。だから、動的にいろいろ生成する方針は管理画面の実装には適していないのではないかと思った。なんでこれがこんなに支持されているのかよくわからない。