Streamlining Rails controllers with custom filters
As Rails applications grow, managing subscription-based access can overload controllers with complex logic. This article explores how to use custom filter classes to extract subscription-related checks from controllers, resulting in cleaner, more maintainable code.
The Problem: Subscription Checks in Controllers
Consider a typical scenario where we need to check a user's subscription status before granting access to certain features:
class PremiumContentController < ApplicationController
before_action :check_subscription
def check_subscription
user = current_user
unless user&.active_subscription? && !user.subscription_expired?
flash[:error] = generate_subscription_error(user)
redirect_to upgrade_path
end
end
def generate_subscription_error(user)
return "You must be logged in" if user.nil?
return "Your subscription has expired" if user.subscription_expired?
return "You need an active subscription" unless user.active_subscription?
end
# ... other controller actions for premium content
end
This approach works, but mixes subscription logic with controller responsibilities, resulting in code that is harder to maintain.
Introducing custom subscription filters
Let's refactor this using a custom filter class to encapsulate the subscription-related logic:
# app/controllers/premium_content_controller.rb
class PremiumContentController < ApplicationController
before_action SubscriptionFilter
end
# app/filters/subscription_filter.rb
class SubscriptionFilter
def self.before(controller)
user = controller.send(:current_user)
unless user&.active_subscription? && !user.subscription_expired?
controller.flash[:error] = subscription_error_message(user)
controller.redirect_to controller.upgrade_path
end
end
private
def self.subscription_error_message(user)
return "You must be logged in" if user.nil?
return "Your subscription has expired" if user.subscription_expired?
return "You need an active subscription" unless user.active_subscription?
end
end
Let's analyse the key components of our custom subscription filter:
- Class definition: We define
SubscriptionFilter
as a standalone class.self.before
method: This class method is called by Rails before controller actions. It takes the controller instance as an argument, allowing access to controller methods and properties. - User call: We use
controller.send(:current_user)
to access thecurrent_user
method, which is typically private in controllers. - Subscription validation: The filter checks that the user has an active, non-expired subscription.
- Error handling: If conditions aren't met, we set a flash error message and redirect the user to an upgrade path.
- Private helper method:
subscription_error_message
generates appropriate error messages based on the user's subscription status.
Benefits of custom subscription filters
Doing filters via this approach we could achieve:
- Focused controllers: Controllers stay lean and focused on their primary responsibilities.
- Reusability: The subscription filter can be applied to multiple controllers that require subscription checks.
- Easier testing: Subscription logic can be unit-tested independent of controllers.
Flexibility: Subscription rules can be changed in one place without touching multiple controllers.
Implementation Tips
Try to store custom filter classes in app/filters/
for organization and use descriptive names for your filter classes (e.g. TrialPeriodFilter
, PremiumTierFilter
). When possible, consider creating a base
filter class for common subscription-related functionality(in that example case).
You might think that we could use helpers or modules for that - and yes, that will work, but having separate filters for controllers I think is much more elegant.
Expand the concept
You can create several subscription-related filters for different scenarios:
class TrialPeriodFilter
def self.before(controller)
user = controller.send(:current_user)
if user&.trial_expired?
controller.flash[:notice] = "Your trial has ended. Please upgrade to continue."
controller.redirect_to controller.pricing_path
end
end
end
class PremiumContentController < ApplicationController
before_action SubscriptionFilter
before_action TrialPeriodFilter, only: [:exclusive_content]
end
This approach allows granular control over different subscription states and access to content.
Summary
Custom subscription filters in Rails provide a powerful way to manage subscription-based access control while keeping controllers clean and focused. By adopting this approach, you can create more maintainable, modular and testable Rails applications, especially when dealing with complex subscription logic.
Happy filtering!