This service object allows users without a session to create a comment, and register OR login at the same time.
If a user is signed out he must enter an email and a password along with the comment.
The service checks if a user exists using the email, then attempts to authenticate the user. Otherwise, it creates a new user.
Three code smells:
- User errors are added to the comment. There is no user object in the view, just a params hash with that name.
- If user validation fails, comment validation does not trigger, meaning if the comment was also blank they need another round trip to find out. Probably not significant though.
- User data is not repopulated on validation failure.
Question: I'm proud of writing a nifty object like that, but it stills feels a little 'wrong-ish' if there is such a word. Is there a better way to handle this type of scenario?
class CommentService
def self.by(params, user)
if user
new params, user
else
new params, find_or_new(params)
end
end
def self.find_or_new(params)
User.find_by(email: params[:user][:email]) || User.new
end
attr_reader :params, :comment, :user
def initialize(params, user)
@params, @user = params, user
@comment = Comment.new(comment_params)
end
def save
if user.new_record?
self.user.attributes = user_params
user.save ? persist : add_user_errors
else
if params[:user].present?
user.authenticate(params[:user][:password]) ? persist : add_auth_error
else
persist
end
end
end
def post
@post ||= Post.find(params[:post_id])
end
private
def add_user_errors
user.errors.each do |k,v|
comment.errors.add(k, v)
end
false
end
def add_auth_error
comment.errors[:base] << "Invalid email or password."
false
end
def user_params
params.require(:user).permit(:name, :email, :password)
end
def comment_params
params.require(:comment).permit(:text)
end
def persist
comment.author = user
comment.post = post
comment.save
end
end
Controller code:
def create
service = CommentService.by(params, current_user)
@post = service.post
if service.save
redirect_to @post
else
@comment = service.comment
render 'new'
end
end
View code:
= form_for [@post, @comment] do |f|
= f.text_area :comment
/ No user object here. Just tossing a user hash into the params.
- unless signed_in?
= fields_for :user do |uf|
= uf.text_field :email
= uf.password_field :password
= f.submit