📜 ⬆️ ⬇️

Managing complexity in ruby ​​on rails projects. Part 2

In the previous part, I talked about the presentation. Now let's talk about the controllers.
In this part I will tell about:

The controller provides communication between the user and the system:

The controller contains only user interaction logic:

Business logic should be kept separate. Your application can also interact with the user via the command line using rake commands. Rake commands, in fact, the same controllers and logic should be divided between them.

REST


I will not delve into the theory of REST, but will tell things related to rails.

Very often I see that controllers are perceived as a set of actions, i.e. for any user action add a new non-standard action.
')
resources :projects do member do get :create_act get :create_article_waybill get :print_packing_slips get :print_distributions end collection do get :print_packing_slips get :print_distributions end end resources :buildings do [:act, :waybill].each do |item| post :"create_#{item}" delete :"remove_#{item}" end end 

Sometimes it happens that programmers do not understand the purpose and difference of GET and POST methods. More about this is written in the article "15 trivial facts about proper operation with the HTTP protocol . "

Take for example work with sessions. According to the technical task, the user can:

To implement this functionality, we create a single session resource, respectively, with the following actions: new, create, destroy, update. Thus, we have one controller, which is responsible only for the session.

Consider an example more difficult. There is an entity project and controller that implements crud operations.
The project can be active or completed. At the completion of the project, you must specify the date of the actual completion and the reason for the delay. Accordingly, we need 2 actions: to display the form and process the data from the form. The first obvious and wrong solution is to add 2 new methods to ProjectsController. The correct solution is to create a nested resource “project completion”.

 resources :projects do scope module: :projects do resource :finish # GET /projects/1/finish/new # POST /projects/1/finish end end 

In this controller, we will add a status check: can we even complete the project?

 class Web::Projects::FinishesController < Web::Projects::ApplicationController before_action :check_availability def new end def create end private def check_availability redirect_to resource_project unless resource_project.can_finish? end end 

Similarly, you can deal with step-by-step forms: each step is a separate embedded resource.

The ideal case is when only standard actions are used. It is clear that there are exceptions, but this happens very rarely.

Responders


Gem respongers helps to remove repetitive logic from controllers.


 class Web::ApplicationController < ApplicationController self.responder = WebResponder #  ActionController::Responder respond_to :html end class Web::UsersController < Web::ApplicationController def update @user = User.find params[:id] @user.update user_params respond_with @user end end 

Controller Hierarchy


A detailed description is in the article by Kirill Mokevnin .
Something similar I saw in an English-language blog, but I will not give a link. The purpose of this technique is to organize controllers.

First, the application renders only html. Then ajax appears, the same html, only without layout.
Then api and the second version of api appear, leaving the first version for backward compatibility. Api uses a token for authentication in the header, not a cookie. Then rss tapes appear, for guests and registered, and rss clients do not know how to work with cookies. In the link to the rss feed you need to include the user token. After you need to use the js framework, and write json api for this with authentication through the current session. Then a section of the site appears with a separate layout and authentication. We also have logically nested entities with nested url.

How this is solved.
All controllers are decomposed by namespaces: web, ajax, api / v1, api / v2, feed, web_api, promo.
And for nested resources, nested routes and nested controllers are used.

Code example:

 Rails.application.routes.draw do scope module: :web do resources :tasks do scope module: :tasks do resources :comments end end end namespace :api do namespace :v1, defaults: { format: :json } do resources :some_resources end end end class Web::ApplicationController < ApplicationController include UserAuthentication #    web  include Breadcrumbs #   ,     api? self.responder = WebResponder respond_to :html #    html add_breadcrumb {{ url: root_path }} #      rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "You are not authorized to perform this action." redirect_to(request.referrer || root_path) end end #    ,    task class Web::Tasks::ApplicationController < Web::ApplicationController #     view helper_method :resource_task add_breadcrumb {{ url: tasks_path }} add_breadcrumb {{ title: resource_task, url: task_path(resource_task) }} private #        def resource_task @resource_task ||= Task.find params[:task_id] end end #   class Web::Tasks::CommentsController < Web::Tasks::ApplicationController add_breadcrumb def new @comment = resource_task.comments.build authorize @comment add_breadcrumb end def create @comment = resource_task.comments.build authorize @comment add_breadcrumb attrs = comment_params.merge(user: current_user) @comment.update attrs CommentNotificationService.on_create(@comment) respond_with @comment, location: resource_task end end 

It is clear that deep nesting is bad. But this applies only to resources, not to namespaces. Those. It is allowed to have such nesting: Api :: V1 :: Users :: PostsController # create, POST / api / v1 / users / 1 / posts. Nesting of resources should be limited to only 2 levels: the parent resource and the nested resource. Also, those actions that do not depend on the base resource can be brought one level up. In the case of users and posts: / api / v1 / users / 1 / posts and / api / v1 / posts / 1

It can be argued that the class inheritance hierarchy is the best choice for this task. If someone has ideas on how to organize controllers differently, then offer your options in the comments.

Bread crumbs


I tried several libraries to form breadcrumbs, but none came up. As a result, I made my own implementation, which uses a hierarchical organization of controllers.

 class Web::ApplicationController < ApplicationController include Breadcrumbs #      ,     #    ,      # {{}}  ,   add_breadcrumb {{ url: root_path }} end class Web::TasksController < Web::ApplicationController #     add_breadcrumb {{ url: tasks_path }} def show @task = Task.find params[:id] #      add_breadcrumb model: @task respond_with @task end end class Web::Tasks::ApplicationController < Web::ApplicationController #     add_breadcrumb {{ url: tasks_path }} add_breadcrumb {{ title: resource_task, url: task_path(resource_task) }} def resource_task; end #  end class Web::Tasks::CommentsController < Web::Tasks::ApplicationController # ..   url,      add_breadcrumb def new @comment = resource_task.comments.build authorize @comment add_breadcrumb #   "  " end end # ru.yml ru: breadcrumbs: defaults: show: "%{model}" new:    edit: ": %{model}" web: application: scope:  tasks: scope:  application: scope:  comments: scope:  

Implementation
 # app/helpers/application_helper.rb # ,   def render_breadcrumbs return if breadcrumbs.blank? || breadcrumbs.one? items = breadcrumbs.map do |breadcrumb| title, url = breadcrumb.values_at :title, :url item_class = [] item_class << :active if breadcrumb == breadcrumbs.last content_tag :li, class: item_class do if url link_to title, url else title end end end content_tag :ul, class: :breadcrumb do items.join.html_safe end end # app/controllers/concerns/breadcrumbs.rb module Breadcrumbs extend ActiveSupport::Concern included do helper_method :breadcrumbs end class_methods do def add_breadcrumb(&block) controller_class = self before_action do options = block ? instance_exec(&block) : {} title = options.fetch(:title) { controller_class.breadcrumbs_i18n_title :scope, options } breadcrumbs << { title: title, url: options[:url] } end end def breadcrumbs_i18n_scope [:breadcrumbs] | name.underscore.gsub('_controller', '').split('/') end def breadcrumbs_i18n_title(key, locals = {}) default_key = "breadcrumbs.defaults.#{key}" if I18n.exists? default_key default = I18n.t default_key end I18n.t key, locals.merge(scope: breadcrumbs_i18n_scope, default: default) end end def breadcrumbs @_breadcrumbs ||= [] end #     def add_breadcrumb(locals = {}) key = case action_name when 'update' then 'edit' when 'create' then 'new' else action_name end title = self.class.breadcrumbs_i18n_title key, locals breadcrumbs << { title: title } end end 



In this part, I showed how to organize the code for the controllers. In the next part I will tell you about working with object-forms.

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


All Articles