Rails EngineでAction Cableを使う時にハマったこと

  • 1
    いいね
  • 0
    コメント

【2017-04-17 現在】
Rails Engine内でAction Cableを使おうとした時に、一緒にセッションも使おうとしたら、かなりハマってしまったのでメモ。

まずAction Cableでセッションを使いたい。

deviseのcurrent_userが使いたかったのです。こちらの記事を参考にしました。Rails Engine側のプロジェクトに作成しました。

app/channels/application_channel/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected
    def find_verified_user
      User.find(session['warden.user.user.key'][0][0])
    rescue
      reject_unauthorized_connection
    end

    def session
      @session ||= cookies.encrypted[Rails.application.config.session_options[:key]]
    end
  end
end

しかし問題発生

あとは、Rails Engine側でチャネル作って、その中でcurrent_userを読めばいけるでしょと思ったのですが、undefined method or valiableのエラーが出てしまいました。
色々確かめて行くうちにRails Engine側のapp/channels/application_channel/connection.rbではなく、メインのプロジェクトの方のapp/channels/application_channel/connection.rbが読み込まれていることがわかりました。
ちなみにチャネルを作ると

app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
  end

  def unsubscribed
  end
end

のようなクラスが作成されApplicationCable::Channelを継承するのですが、なぜかこちらもメインのプロジェクト側のapp/channels/application_channel/channel.rbしか呼び出されないようです。

原因と思われるコード

Action Cableのソースのここではないでしょうか?
self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }の部分でApplicationCable::Connectionを呼んでいるのですが、うまく動いていないのだと思います。

lib/action_cable/engine.rb(抜粋)
    initializer "action_cable.set_configs" do |app|
      options = app.config.action_cable
      options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?

      app.paths.add "config/cable", with: "config/cable.yml"

      ActiveSupport.on_load(:action_cable) do
        if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
          self.cable = Rails.application.config_for(config_path).with_indifferent_access
        end

        previous_connection_class = connection_class
        self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call } #ここ

        options.each { |k, v| send("#{k}=", v) }
      end
    end

対策

とりあえず名前空間を分けます。

app/channels/application_channelapp/channels/(engine_project)/application_channelに移しました。合わせてmoduleで囲んでおきましょう。既に作成されたチャネルのスーパークラスをEngineProject::ApplicationCable::Channelに変更します(必要があれば)。

ConnectionClassの変更

これだけでは、読み込まれるクラスが変わらないのでRails Engineの設定をいじります。config.action_cable.connection_class = -> { 'EngineProject::ApplicationCable::Connection'.safe_constantize }とすることでconnection_classを上書きできるようです。

lib/engine_project/engine.rb
module EngineProject
  class Engine < ::Rails::Engine
    isolate_namespace EngineProject
    config.before_initialize do
      config.action_cable.connection_class = -> { 'EngineProject::ApplicationCable::Connection'.safe_constantize } # これを追加
    end
  end
end

これで無事に動きました!もしかしたらもっといい方法があるのかもしれません。