Servicer
NOTE: Servicer is NOT a service object.
Servicer contains a collection of CRUD operations that can be overridden and tailered to the needs.
First of all, it’s always recommended to create a base servicer class Admin::ApplicationServicer as below, so that devs can have better control of developing global changes/functions:
# app/servicers/admin/application_servicer.rb
class Admin::ApplicationServicer < Wallaby::ModelServicer
end
This base servicer can be generated by Wallaby generator as well:
rails g wallaby:install
See Mapping - Servicer for the configuration if
Admin::ApplicationServiceris taken for other purpose.
Starting with:
Configuration can be set for:
- .base_class! - flagging as base class.
- .model_class - specifying the model class.
Accessing helper methods:
- #user - accessing user object.
- #authorizer - accessing authorizer which provides functions for authorization.
Customizing CRUD operations:
- permit - whitelisting params for mass assignment.
- collection - returning records from data source.
- paginate - paginating records.
- new - initializing the new resource object for form to use.
- find - finding the record.
- create - creating record.
- update - updating record.
- destroy - deleting record.
Declaration
Read more at Servicer Naming Convention
Let’s see how a servicer can be created so that Wallaby knows its existence.
Similar to the way in Rails, create a custom servicer for model Product inheriting from Admin::ApplicationServicer (the base servicer mentioned above) as below:
# app/servicers/product_servicer.rb
class ProductServicer < Admin::ApplicationServicer
end
If ProductServicer is taken, it is still possible to use another name (e.g. Admin::ProductServicer). However, the attribute model_class must be specified. See model_class for examples.
.base_class!
All servicers will be preloaded and processed by Wallaby in order to build up the mapping between servicers and models. If the servicer is considered not to be processed, it can be flagged by using base_class!:
# app/servicers/admin/special_servicer.rb
class Admin::SpecialServicer < Admin::ApplicationServicer
base_class!
end
.model_class
According to Wallaby’s Servicer Naming Convention, if a custom servicer cannot reflect the association to the correct model, for example, as Admin::ProductServicer to Product, it is required to specify the model class in the servicer as below:
# app/servicers/admin/product_servicer.rb
class Admin::ProductServicer < Admin::ApplicationServicer
self.model_class = Product
end
Helper Methods
#user
It’s the reference of wallaby_user from controller. To access user, it goes:
# app/servicers/admin/product_servicer.rb
class Admin::ProductServicer < Admin::ApplicationServicer
self.model_class = Product
def send_promotion_email
ProductMailer.with(user: user).promotion_email.deliver_later
end
end
#authorizer
It’s the instance of Model Authorizer that does authorization for the model and the action
CRUD
permit
This is the template method to whitelist parameters for mass assignment automatically. It works differently for different ORMs, for example:
ActiveRecord
NOTE: as described below, Wallaby doesn’t automatically permit nested attribute params for
has_oneassociations.# app/models/product.rb class Product < ActiveRecord::Base has_one :product_detail has_one :picture, as: :imageable has_many :order_items, class_name: Order::Item.name has_many :orders, through: :order_items belongs_to :category has_and_belongs_to_many :tags end # app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product end product_servicer.permit params, 'create' product_servicer.permit params, 'update' # both will work like => # params.require(:product).permit( # "sku", "name", "description", # "category_id", # "order_item_ids"=>[], "order_ids"=>[], "tag_ids"=>[] # )
Please keep in mind that when servicer permits parameters, its behavior is affected by authorizer’s permit_params as well.
For example, if pundit policy has defined following
permitted_attributes:# app/policies/product_policy.rb class ProductPolicy < ApplicationPolicy def permitted_attributes if user.admin? || user.owner_of?(product) [:sku, :name, :category_id] else [:description] end end endThen servicer will only allow what policy has allowed instead of the general fields.
To customize what params should be permitted, it goes:
if utilizing what Wallaby has implemented:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def permit(params, action) permitted = super action == 'create' ? permitted : permitted.slice(:name) end endor simply replacing this template method:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def permit(params, action) if action == 'create' params.require(:product).permit(:sku, :name, product_detail_attributes: [:meta_data]) else authorizer.permit_params action, model_class end end end
collection
This is the template method that queries data source and returns a collection of data. For ActiveRecord, it handles the following parameters:
params[:q]: an expression to perform search (see Colon Search for more information).params[:sort]: expression (e.g.name asc,updated_at desc) to perform sorting.params[:filter]: the filter name (e.g.red) that Wallaby should apply its scope (e.g.where color: red) to the search
Please keep in mind that the query that servicer carries out is also filtered by authorizer’s accessible_for method.
For example, if ability has defined following condition:
# app/models/ability.rb class Ability include CanCan::Ability def initialize(user) can :index, Product, active: true end endThen the scope
active: turewill be applied to query as well.
To customize how query should be carried out, it goes:
if utilizing what Wallaby has implemented:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def collection(params) super.where active: true end endor simply replacing this template method:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def collection(params) if params[:active] Product.where active: true else authorizer.accessible_for :index, Product.where(nil) end end end
paginate
This is the template method that handles pagination for a query. For ActiveRecord, kaminari is used to handle pagination with the following params:
params[:page]: page number for pagination.params[:per]: page size for pagination.
To customize how query should be paginated, it goes:
if utilizing what Wallaby has implemented:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def paginate(query, params) super.per 100 end endor simply replacing this template method:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def paginate(query, params) query.page(params[:page_number]).per(params[:page_size]) end end
new
This is the template method that builds the resource instance for creation form page.
To customize how resource instance should be initialized, it goes:
if utilizing what Wallaby has implemented:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def new(params) super.tap do |resource| resource.active = true end end endor simply replacing this template method:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def new(params) Product.new(params.merge active: true) end end
find
This is the template method that finds the record from data source.
To customize how resource should be found, it goes:
if utilizing what Wallaby has implemented:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def find(id, params) super.tap do |resource| resource.active = true end end endor simply replacing this template method:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def find(id, params) Product.friendly.find id end end
create
This is the template method that create the resource and save to data source.
Please keep in mind that the attributes will be filtered by authorizer’s attributes_for method.
For example, if ability has defined following condition:
# app/models/ability.rb class Ability include CanCan::Ability def initialize(user) can :index, Product, active: true end endThen the attribute
active: turewill be applied to resource before saving.
To customize how resource should be created, it goes:
if utilizing what Wallaby has implemented:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def create(resource, params) resource.active = true super end endor simply replacing this template method:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def create(resource, params) ensure_attributes_for authorizer, :create, resource resource.save if valid? resource resource end end
update
This is the template method that update the resource and save to data source.
Please keep in mind that the attributes will be filtered by authorizer’s attributes_for method.
For example, if ability has defined following condition:
# app/models/ability.rb class Ability include CanCan::Ability def initialize(user) can :index, Product, active: true end endThen the attribute
active: turewill be applied to resource before saving.
To customize how resource should be updated, it goes:
if utilizing what Wallaby has implemented:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def update(resource, params) resource.active = true super end endor simply replacing this template method:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def update(resource, params) ensure_attributes_for authorizer, :update, resource resource.save if valid? resource resource end end
destroy
This is the template method that destroy the resource from data source.
To customize how resource should be destroyed, it goes:
if utilizing what Wallaby has implemented:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def destroy(resource, params) super.tap do audit_log :destroy, resource end end endor simply replacing this template method:
# app/servicers/admin/product_servicer.rb class Admin::ProductServicer < Admin::ApplicationServicer self.model_class = Product def destroy(resource, params) unregister resource resource.destroy end end