You'll hear it lots, fat models/skinny controllers but I was working on a project recently where my controller code was putting on a bit of weight!
The requirement was that when a post was saved with a url, a gem I had installed called LinkThumbnailer would visit that url to find the first image on the page and return the image url.
I implemented this within the create and update actions in my controller. The problem was that it involved quite a few steps such as checking if it was a valid url and handling exceptions. My controller code at this point was expanding and it didn't feel like this was the correct place for the code. I had a similar issue with placing the code in the model layer because all of the code for generating the image url didn't have interaction with table data and would therefore be unnecessarily bloating the model layer.
My solution after some reading was to create an app/services
folder with a class GenerateImageUrlFromPostUrl
that was a plain old Ruby object (PORO).
Now my update action for example looked at follows:
def update
@post.image_url = generate_image_url_from_post_url(post_params[:url])
if @post.update(post_params)
flash[:notice] = "You successfully updated your post!"
redirect_to post_path(@post)
else
render :edit
end
end
with a private method:
def generate_image_url_from_post_url(url)
GenerateImageUrlFromPostUrl.new(url).return_url_string
end
That's all that was needed in the controller which made it a lot cleaner with the bulk of the work done in my GenerateImageUrlFromPostUrl
class within the services folder:
class GenerateImageUrlFromPostUrl
attr_reader :post_url
def initialize(post_url)
@post_url = post_url
end
def return_url_string
image_url_object = open_graph_protocol_object(post_url)
begin
if image_url_object.images.any? && valid_image_url(image_url_object)
image_url_object.images.first.src.to_s
end
rescue =>e
Rails.logger.warn("Couldn't parse OG data for post #{post_url}")
end
end
private
def open_graph_protocol_object(url)
LinkThumbnailer.generate(url_with_protocol(url), attributes: [:images], http_timeout: 2, image_limit: 1, image_stats: false)
end
#prepend http:// to url if it is missing
def url_with_protocol(url)
url.starts_with?('http://') ? url : "http://#{url}"
end
#return false if url starts with a / character
#I want to ignore these urls as it results in a localhost lookup
#
#example: /images/sample.jpg would be false
def valid_image_url(url)
!url.images.first.src.to_s.starts_with?('/')
end
end
I actually want to take this one step further now and hand it off as a background job so there is no delay in saving the post, with the image url fetching being handled in the background. I think this will be covered in the third Tealeaf Academy course so I will implement it at that time.
That's all there is to it, now I'm back to having a skinny controller with an added bonus that it should be nice and easy to test!