Контроллеры

Введение

Контроллер в Rails выступает в качестве посредника. Он знает что запросить у модели и только, а все остальные задачи перекладываются на модель. Также он знает, какое представление надо отрендерить и отправить браузеру, но представление должно само позаботиться о том, чтобы собрать итоговый HTML код. Основная же его работа заключается в том, чтобы собрать необходимый набор переменных экземпляра и отправить его представлению.

В каких случаях используется контроллер? Пришедший в приложение HTTP запрос отправляется маршрутизатором в действие контроллера, Rails упаковывает пришедшие с запросом параметры и запускает требуемый метод контроллера. После выполнения метода, Rails перешлет представлению все переменные экземпляра, которые были определены в методе. И эти переменные будут доступны для вызова из шаблона представления, которое, в свою очередь, будет отправлено в браузер.

Все достаточно просто. Обычно контроллеры не содержат много кода, но зато способны выполнить много работы с помощью этого кода. Что, если вы хотите отобразить все посты вашего блога на странице index вашего сайта? Выполните действие #index контроллера PostsController, и он получит все нужные данные и передаст их в представление index.html.erb, которое уже само решит, в каком виде их отображать (в виде огромного списка? или симпатичных панелек?).

Действие #index нашего контроллера могло бы выглядеть так:

    PostsController < ApplicationController
      ...
      def index
        @posts = Post.all
      end
      ...
    end

В этом простом примере контроллер запрашивает модель ("Эй, дай мне все посты!"). Полученный результат в виде всех постов присваивается переменной экземпляра @posts для того, чтобы представление могло их использовать, а затем контроллер автоматически рендерит файл представления app/views/posts/index.html.erb (мы поговорим об этом через минуту).

Пункты для размышления

Постарайтесь ответить на предложенные вопросы. После выполнения задания попробуйте ответить на них ещё раз

  • Почему важно то, как вы называете ваши контроллеры, модели и представления?
  • Где находится файл представления, который рендерится по умолчанию из контроллера?
  • В чем различие между #render и #redirect_to?
  • Что происходит с переменными экземпляра в контроллере в каждом из этих случаев?
  • Что из себя представляет ссылка на определенный пост (подсказка: работает везде как #link_to и #*_path)
  • Прекращает ли метод выполнение или прерывается, когда доходит до #render или #redirect_to?
  • Что происходит при множественном рендере или перенаправлении?
  • Что такое Strong Parameters?
  • В каких случаях можно использовать хэш params напрямую, а когда следует использовать "белый список"?
  • Что такое "скалярные" значения?
  • Что делают #require и #permit?
  • Что такое #flash?
  • В чем различие между #flash и #flash.now?

Правила именования

Rails облегчает вашу жизнь при условии, что все названо вами определенным образом, отрабатывая затем согласно этим соглашениям. Например, ваш контроллер и его действие должны быть названы также, как и в файлеroutes.rb, когда вы направляли на это действие входящий запрос.

Рассмотрим также последующие действия контроллера. Как только Rails доходит до конца действия, он пересылает все переменные экземпляра этого контроллера в файл представления, названный точно также, как действие этого контроллера и находящийся в папке, названной его именем, например app/views/posts/index.html.erb. И это не случайно, это сделано чтобы облегчить вам жизнь, когда вы в дальнейшем будете искать необходимые файлы. В случае, если вы поместите файлы представлений в другое место, вам придется явно указывать подлежащие рендерингу файлы.

Рендеринг и Перенаправление (Redirecting)

Тем не менее, во многих случаях необходимо направить Rails на рендеринг другого представления, в отличие от того, что используется по умолчанию. В основном это необходимо, когда пользователя необходимо отправить на другую страницу вместо рендеринга результатов действия в контроллере.

Перенаправления обычно используются в таких действиях контроллера, где вы отправляете данные из какой-то формы, например для создания нового поста. В таких случаях нет необходимости иметь такое представление как create.html.erb, которое отобразит созданный пост. Обычно необходимо отобразить созданный пост, и нам просто нужно перенаправить пользователя на страницу его просмотра. Различие здесь в том, что приложение рассматривает перенаправление как совершенно новый HTTP запрос. Так что он проходит через модуль маршрутизации, ищет страницу просмотра этого поста и рендерит его. Также это значит, что любые переменные экземпляра из исходного действия #create теряются.

Это обычный путь при успешном создании объекта, но как быть, если произошла ошибка (например пользователь ввел слишком короткое название поста)? В этом случае вы можете отрендерить представление другого действия контроллера, и обычно это действие, которое создает ту же форму для создания объекта (в данном случае действие #new).

Хитрость тут в том, что представление получает переменные экземпляра из вашего текущего действия контроллера. Скажем, вы попробовали создать пост через #create и сохранить его в переменную @post, но сохранение прошло неудачно. Тогда вы рендерите представление для действия #new, и оно получит переменную @post, с которой вы только что работали в действии #create. Это хорошо по той причине, что форма не теряет введенные данные (что несомненно расстроило бы пользователя), а вы можете определить какое поле содержит некорректную информацию и дать пользователю возможность отправить данные повторно. Сейчас это может звучать несколько абстрактно, но скоро вы увидите этот нюанс в действии.

Взгляните на код:

    # app/controllers/posts_controller.rb
    class PostsController < ApplicationController
      ...
      # Создает (но не сохраняет) пустой пост, и форма, которую мы рендерим
      # знает, какие поля использовать и где она подтверждает данные
      # Это действие отрендерит app/views/posts/new.html.erb после
      # своего выполнения
      def new
        @post = Post.new
      end

      # Это действие запустится после того, как мы получим подтвержденную
      # форму из вышеуказанного действия NEW (помните действия REST?)
      # Здесь мы используем псевдокод просто чтобы обозначить смысл происходящего
      def create
        ... здесь код для создания новой переменной @post, основанной на данных из формы ...
        if @post.save
          ... код для создания сообщения об успешности выполнения операции ...
          redirect_to post_path(@post.id) # перейти на страницу просмотра для @post
        else
          ... код для создания сообщения об ошибке ...
          render :new
        end
      end
    end

Обратите внимание здесь на то, что в случае успешного сохранения поста в базу данных, мы перенаправляем Rails на просмотр поста. Также, вместо длинной записи redirect_to post_path(@post.id), можно просто написать redirect_to @post, так как программисты настолько часто этим пользуются, что Rails дает возможность прибегать к краткой форме. Мы будем использовать эту форму в дальнейшем.

Условие, которое отрабатывает при ошибке в действии #create рендерит ту же самую форму, которая рендерилась в действии #new. Переменная @post в этом случае будет объектом Post, который неудачно сохранился, при этом он содержит информацию об ошибках, которую мы можем вывести красным шрифтом в соответствующих полях формы.

Множественный Рендеринг/Перенаправление

Важно отметить, что render и redirect_to НЕ останавливают действие контроллера так, как это бы сделал return. Так что вы должны быть абсолютно уверены, что логика в контроллере не приведет к выполнению этих методов более одного раза. Если это случится, вы получите ошибку. Такие ошибки, впрочем, легко отловить.

Если вы напишете что-то вроде:

    def show
      @user = User.find(params[:id])
      if @user.is_male?
        render "show-boy"
      end
      render "show-girl"
    end

то в случае, если условие is_male? истинно, вы получите ошибку множественного рендеринга, потому что вы указали Rails отрендерить как "show-boy", так и "show-girl".

Хэш Params и Strong Parameters

В примере выше мы видели ... здесь код для создания новой переменной @post, основанной на данных из формы .... Как же нам получить эти данные? Модуль маршрутизации упаковывает все параметры, посланные с HTTP запросом, но как нам получить к ним доступ?

С помощью хэша params! Он работает как обычный Ruby хэш и содержит параметры запроса, хранящиеся как пары key: value. Как получить ID поста, который нам нужен? Используя params[:id]. Вы можете получить этим способом любой параметр, имеющий "скалярное значение", то есть строки, числа, булевы значения, nil... все, что "плоское".

Некоторые формы отправляют каждое поле на верхнем уровне хэша, например params[:post_title] может быть "test post" и params[:post_body] может быть "body of post" и т.д. Доступ к таким параметрам не составит труда. Вы сможете ими управлять, что мы рассмотрим в уроке, посвященном формам.

Strong Parameters

Бывает, что вы хотите слать параметры из браузера в виде хорошо упакованного хэша или помещенные в массив. Это может сильно облегчить вам жизнь тем, что вы просто отсылаете этот хэш напрямую в Post.new(здесь_ваш_хэш_атрибутов), а Post.new их в любом случае и ожидает. Мы не будем сейчас вдаваться в эти подробности (до урока о Моделях и Формах), а просто скажем, что структура отсылаемых вами из формы данных полностью зависит от того, как вы настроили форму (как вы выбрали название своих полей, используя атрибут HTML name='').

В нашем примере мы полагаем, что наш params[:post] выдает нам ожидаемый Post.new хэш атрибутов { title: "test post", body: "this post rocks", author_id: "1" }. Вам решать, как создавать форму и упаковывать ли параметры в хэш или же оставить на верхнем уровне индивидуальных атрибутов (как params[:id]). Но обычно легче подать в контроллер хорошо упакованный хэш атрибутов.

Важным различием между "скалярными" значениями параметров, такими как строки и более сложными как хэши и массивы является то, что в контроллерах Rails 4 включена некоторая защита, названная "Strong Parameters". К примеру, пользователь не может отослать нежелательные данные (как например присвоение себе признака администратора при создании пользователя). Чтобы это реализовать, вы должны явно указать для Rails список атрибутов хэша или массива, который вы хотите принимать.

Примечание: Это было реализовано в Rails 3 настройкой attr_accessible в модели для белого списка атрибутов, и возможно вы это знаете по множеству постов на Stack Overflow и по наличию этого функционала в ранее созданных приложениях.

Чтобы добавить параметры в "белый список", или использовать разрешенные атрибуты, необходимо использовать методы require и permit. Вы используете require для названия вашего массива или хэша в Params (иначе будет ошибка), а затем используете permit для указания индивидуальных атрибутов внутри хэша для использования.
Например:

    def whitelisted_post_params
      params.require(:post).permit(:title,:body,:author_id)
    end

Это занесет в белый список и вернет хэш только тех параметров, которые были указаны (например {title: "your title", body: "your body", author_id: "1"}). Если вы это не сделаете, то при попытке доступа к params[:post], вы ничего не получите. Также, если в хэше были любые дополнительные поля, они будут обрезаны и сделаны недоступными (для вашей безопасности).

Это может быть неудобно, но здесь Rails защищает вас от злоумышленников. Обычно вы будете упаковывать эти параметры в их собственный приватный метод внизу контроллера, а затем вызывать этот метод, когда необходимо получить эти параметры.

Итак, наше действие #create теперь может быть переписано так:

    # app/controllers/posts_controller.rb
    class PostsController < ApplicationController
      ...
      # Это действие запустится после того, как мы получим подтвержденную
      # форму из вышеуказанного действия NEW (помните действия REST?)
      def create
        @post = Post.new(whitelisted_post_params) # см. метод ниже
        if @post.save
          ... code to set up congratulations message ...
          redirect_to post_path(@post.id) # перейти на страницу просмотра для @post
        else
          ... code to set up error message ...
          render :new
        end
      end

      private  # такие методы-помощники лучше делать приватными

      # отдает нам только хэш, содержащий параметры, необходимые
      # для создания или изменения поста
      def whitelisted_post_params
        params.require(:post).permit(:title,:body,:author_id)
      end
    end

Флэш/Flash

Нам осталось сформировать специальные сообщения для пользователя. Rails предоставляет для этого удобный инструмент для передачи сообщений об успешном завершении и об ошибке (как например небольшое сообщение зеленого цвета в каком-либо приложении, появляющееся после того, как вы успешно залогинились), называемое "флэш". Оно работает как хэш - вы можете присвоить его ключам определенное сообщение, которое затем будет вам доступно для отображения в представлениях.

Вы можете использовать при этом любые ключи, но обычно используются следующие три: :success, :error, и :notify. Так что вышеуказанное сообщение может выглядеть как flash[:success] = "Отлично! Вы создали пост!".

Возможно вы напишете снипет в представлении для отображения флэш-сообщения, и поэтому вам понадобится использовать произвольные ключи. Но тем не менее хорошей практикой является использование стандартных ключей. Учтите, что данные хэша флэш-сообщения стираются сразу после его использования, так что не беспокойтесь, что сообщение будет выводиться снова и снова.

Последнее различие возвращает нас к различию между перенаправлением и рендерингом. Помните, перенаправление создает полностью новый HTTP запрос, при котором мы теряем все наши данные - за исключением хэша флэш-сообщения. Флэш специально создан для путешествий с этим HTTP запросом, поэтому вы получите к нему доступ даже если перенаправлены на другую страницу.

Рендеринг не заходит так далеко - он использует файл представления, являющийся частью стандартного хода событий и вы имеете доступ ко всем переменным экземпляра в этом файле. Из-за особенностей флэш-сообщения, необходимо использовать flash.now вместо flash в случае, если используется рендеринг. Это будет к примеру так - flash.now[:error] = "Исправьте ваши ошибки, пожалуйста..

Различие между flash и flash.now просто дает Rails знать, необходимо ли ему сделать флэш доступным для вас. Если например вместо требуемого flash.now вы используете flash, то это сообщение вы увидите и при переходе на другую страницу приложения, что будет говорить о некорректной работе программы.

Окончательный код контроллера для действия #create:

    # app/controllers/posts_controller.rb
    class PostsController < ApplicationController
      ...

      # Это действие запустится после того, как мы получим подтвержденную
      # форму из вышеуказанного действия NEW (помните действия REST?)
      def create
        @post = Post.new(whitelisted_post_params)
        if @post.save
          flash[:success] = "Отлично! Вы создали пост!"
          redirect_to @post # # перейти на страницу просмотра для @post
        else
          flash.now[:error] = "Исправьте ваши ошибки, пожалуйста."
          render :new
        end

        private

          def whitelisted_post_params
            params.require(:post).permit(:title,:body,:author_id)
          end
      end
    end

В итоге это действие контроллера выполняет кучу работы - забирает данные из формы, создает новый пост, пытается его сохранить, устанавливает "успешное" сообщение и перенаправляет вас на этот пост в случае успешного создания, а в противном случае сообщает вам о вашей ошибке и заново рендерит форму для ввода данных. Огромная работа всего в 10 строках кода - теперь это действие с хорошей логикой.

Ваши задания

Надеемся вы почувствовали принципы работы контроллера Rails; тем не менее, вы должны хорошо разбираться в происходящих в нем процессах и знать приемы работы с ним.

  1. Прочтите главу о контроллерах, разделы 1 - 4.5.3 и 5.2. О сессиях (раздел 5.1) мы поговорим более подробно в будущем, так что не волнуйтесь насчет них.

Дополнительные ресурсы

Этот раздел содержит полезные ссылки на дополнительные материалы. Это не обязательно, так что расценивайте их как нечто полезное, если вы хотите поглубже погрузиться в тему

Поделиться уроком: