📜 ⬆️ ⬇️

Controller architecture: simple tips for every day

The fact that controllers should be “thin” is known to everyone, but as the functionality grows, it becomes more and more difficult to maintain cleanliness of controllers. We want to offer a few recommendations on how to keep your controllers as clean as possible without compromising the quality of the code.

1. Use inherited_resources


All controllers are built on the basis of inherited_resources, which allows us to avoid the banal CRUD code. Only two lines of the declaration of the controller inherited from InheritedResources :: Base, and he knows how to perform all the basic operations (create / display / update / delete) with the resource! Everything is good, but problems often arise:

To solve the first problem, you can override the corresponding controller method (index) and make the necessary changes to the sample, but there is a much more elegant method, which is described in the inherited_resources documentation, but for some reason few people use it.

2. Use the has_scope extension


This gem allows you to insert into the selection of a resource / collection of resources any scopes described in the model.
How it works? For example, we need to display all blog posts, i.e. filter blog posts:

class PostsController < InheritedResources::Base
has_scope :by_blog, :only => :index
end

class Post < ActiveRecord::Base
belongs_to :blog
scope :by_blog, lambda{|blog_id| where (:blog_id => blog_id)}
end

* This source code was highlighted with Source Code Highlighter .

Now when you request / posts? By_blog = 1, the collection of resources will be automatically filtered by blog with id = 1. But it doesn’t look very nice, so let's write the following in routs:
')
get "blogs/:blog_id(/page/:page)(.:format)" => "posts#index" , :constraints => { :page => /\d+/ }, :defaults => { :page => 1 }
resources :posts


* This source code was highlighted with Source Code Highlighter .

And the same result can be obtained from the URL / blogs / 1.

If you need to constantly sort collections, then you can use the default parameter - then the scop will always be used with the specified default value:

class PostsController < InheritedResources::Base
has_scope :ordered, : default => 'created_at DESC'
end

class Post < ActiveRecord::Base
scope :ordered, lambda{|field| order(field)} #
end


* This source code was highlighted with Source Code Highlighter .

Similarly, you can eliminate the problem of N + 1 queries:

class PostsController < InheritedResources::Base
has_scope :eager_loading, : default => 'true' , :only => :index
end

class Post < ActiveRecord::Base
scope :eager_loading, preload(:blog, :user, :tags)
scope :eager_loading2, includes(:blog, :user)
end


* This source code was highlighted with Source Code Highlighter .
If you need to specify several scopes at once, check some additional conditions or simply do not want to produce scops in the model, you can specify them directly when setting the has_scope parameters in the block:

class PostsController > InheritedResources::Base
has_scope :blog do |controller, scope, value |
value != "all" ? scope. where (:blog_id => value ) : scope
end
end


* This source code was highlighted with Source Code Highlighter .

More information about the has_scope can be on the project page . The effect of it as from HAML is to spend a little time getting used to the features, but then a lot of time is saved.

3. For pagination use kaminari / will_paginate


These gems are very popular and only lazy did not use them. How do they integrate with inherited_resources? With kaminari there are no problems at all - it can be applied in the controller as a usual download: page just like in the previous examples. But with will_paginate - you have to tinker a bit, because The paginate method it provides is not a scoped model. But here there is a quite elegant solution - it is necessary to override the collection method in the controller as follows:

class PostsController < InheritedResources::Base
protected
def collection
@posts ||= end_of_association_chain.paginate(:page => params [:page])
end
end


* This source code was highlighted with Source Code Highlighter .

Since this trick will be used very often, it can be brought into the module and connected as needed to other controllers, but more on that next time.

4. For authentication and authorization, use devise and cancan respectively.


When using this bundle, you can completely dispense with separate controllers for the control panel. They allow you to fully make the logic of sharing access to resources from the controller, leaving only the declarations there and deserve a separate article.

5. If possible, avoid overlapping standard controller methods.


All additional checks, normalization of parameters and other actions for standard operations can be made in before_filter:

class PostsController < InheritedResources::Base
before_filter lambda{ resource.user = current_user }, :only => :create
before_filter lambda { resource.thumb = nil if params [:thumb_delete] }, :only => :update
end


* This source code was highlighted with Source Code Highlighter .


6. For forming RSS feeds, uploading AJAX collections, etc. no need to produce separate methods


It is enough to request the collection in the required format: /posts.rss and /posts.json, while in the controller it is enough to register:
class PostsController < InheritedResources::Base
respond_to :html
respond_to :rss, :json, :only => :index
end


* This source code was highlighted with Source Code Highlighter .

In addition, for the formation of RSS you need to register in the template posts / index.rss.builder:
xml.instruct! :xml, :version => "1.0"
xml.rss :version => "2.0" do
xml.channel do
xml.title ""
xml.description ""
xml.link collection_url(:format => :rss)

for resource in collection
xml.item do
xml.title resource.title
xml.description "#{resource.annotation}\n#{link_to ' ...', resource_url(resource)}"
xml.pubDate resource.published_at.to_s(:rfc822)
xml.link resource_url(resource)
xml.guid resource_url(resource)
end
end
end
end


* This source code was highlighted with Source Code Highlighter .


You can handle the JSON collection with the following jQuery code:
$( '#blog_id' ).live( 'change' , function () {
$.ajax({
url: '/posts.json' ,
dataType: 'json' ,
data: { blog_id: $( this ).val() },
success: function (json) {
var options = '' ;
for ( var i = 0; i < json.length; i++) {
options += '<option value="' + json[i].id + '">' + json[i].title + '</option>' ;
}
$( '#destination' ).html(options);
}
});
});


* This source code was highlighted with Source Code Highlighter .

Thus, it is possible to achieve the minimum amount of code in the controllers while maintaining flexible functionality, REST approach and code transparency. All these principles are implemented in real projects and have shown themselves well in practice. Some statistics: after 10 months of development, the largest controller in the project SmartSourcing takes 86 lines, the smallest - 2 lines, but most controllers - no more than 20 LOC.

Source: https://habr.com/ru/post/123200/


All Articles