Контроллеры
ИсточникТребование: в этом руководстве предполагается, что вы изучили вводные руководства и запустили приложение Phoenix и убедились, что оно работает.
Требование: в этом руководстве предполагается, что вы изучили руководство по жизненному циклу запроса.
Контроллеры Phoenix выступают в роли промежуточных модулей. Их функции, называемые действиями, вызываются маршрутизатором в ответ на HTTP-запросы. Действия, в свою очередь, собирают все необходимые данные и выполняют все необходимые шаги, прежде чем вызвать слой представлений для отображения шаблона или вернуть ответ в формате JSON.
Контроллеры Phoenix также построены на основе пакета Plug и сами являются плагами. В контроллерах есть функции, позволяющие выполнять в действии почти всё, что нам может понадобиться. Если мы обнаружим, что контроллеры Phoenix не предоставляют нужную функциональность, возможно, мы найдём её в самом Plug. Дополнительную информацию см. в руководстве по Plug или в документации Plug.
В только что созданном приложении Phoenix будет один контроллер с именем PageController, который находится в lib/hello_web/controllers/page_controller.ex и выглядит так:
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def home(conn, _params) do
render(conn, :home)
end
end
Первая строка после определения модуля вызывает макрос __using__/1 модуля HelloWeb, который импортирует несколько полезных модулей.
PageController предоставляет нам действие home для отображения приветственной страницы Phoenix, связанной с маршрутом по умолчанию, который Phoenix определяет в маршрутизаторе.
Действия
Действия контроллера — это обычные функции. Мы можем назвать их как угодно, если соблюдаем правила именования Elixir. Единственное обязательное условие — имя действия должно совпадать с маршрутом, определённым в маршрутизаторе.
Например, в lib/hello_web/router.ex мы могли бы изменить имя действия в маршруте по умолчанию, который Phoenix создаёт для нового приложения, с home:
get "/", PageController, :home
на index:
get "/", PageController, :index
если мы также изменим имя действия в PageController на index, то приветственная страница загрузится, как и прежде.
defmodule HelloWeb.PageController do
...
def index(conn, _params) do
render(conn, :home)
end
end
Хотя мы можем давать действиям любые имена, существуют соглашения об именовании действий, которых следует по возможности придерживаться. Мы разбирали их в руководстве по маршрутизации, но кратко повторим их здесь.
- index — отображает список всех элементов указанного типа ресурса
- show — отображает отдельный элемент по идентификатору
- new — отображает форму для создания нового элемента
- create — получает параметры нового элемента и сохраняет его в хранилище данных
- edit — извлекает отдельный элемент по идентификатору и отображает его в форме для редактирования
- update — получает параметры отредактированного элемента и сохраняет его в хранилище данных
- delete — получает идентификатор элемента, который нужно удалить, и удаляет его из хранилища данных
Каждое из этих действий принимает два параметра, которые Phoenix передаёт за кулисами.
Первый параметр всегда conn — структура, содержащая сведения о запросе, такие как узел, элементы пути, порт, строка запроса и многое другое. conn поступает в Phoenix через промежуточный слой Plug в Elixir. Более подробные сведения о conn можно найти в документации Plug.Conn.
Второй параметр — params. Как нетрудно догадаться, это карта, содержащая параметры, переданные в HTTP-запросе. Рекомендуется сопоставлять параметры с образцом в сигнатуре функции, чтобы упаковать данные в удобный вид и передать их на рендеринг. Мы видели это в руководстве по жизненному циклу запроса, когда добавляли параметр messenger в маршрут show в lib/hello_web/controllers/hello_controller.ex.
defmodule HelloWeb.HelloController do
...
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
end
В некоторых случаях — например, часто в действиях index — параметры нам не важны, потому что от них не зависит поведение. В таких случаях мы не используем входящие параметры, а просто добавляем к имени переменной подчёркивание, называя её _params. Это позволит компилятору не сообщать о неиспользуемой переменной, сохранив при этом правильную арность.
Рендеринг
Контроллеры могут отображать содержимое несколькими способами. Самый простой способ — вывести обычный текст с помощью функции text/2, которую предоставляет Phoenix.
Например, давайте изменим действие show из HelloController так, чтобы оно возвращало текст. Для этого можно сделать следующее.
def show(conn, %{"messenger" => messenger}) do
text(conn, "From messenger #{messenger}")
end
Теперь страница /hello/Frank в браузере должна отображать From messenger Frank как обычный текст без HTML.
json/2. Ей нужно передать данные, которые библиотека Jason может преобразовать в JSON, например карту. (Jason — одна из зависимостей Phoenix.)
def show(conn, %{"messenger" => messenger}) do
json(conn, %{id: messenger})
end
Если снова открыть в браузере /hello/Frank, мы увидим блок JSON с ключом id, которому соответствует строка "Frank".
{"id": "Frank"}
Функция json/2 полезна при создании API. Для отображения HTML также есть функция html/2, но чаще всего мы используем представления Phoenix для создания ответов. Для этого в Phoenix есть функция render/3. Она особенно важна для ответов HTML, поскольку представления Phoenix обеспечивают преимущества в производительности и безопасности.
Давайте вернём действие show к исходному варианту, который мы написали в руководстве по жизненному циклу запроса:
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
end
Чтобы функция render/3 работала правильно, контроллер и представление должны иметь одинаковое корневое имя (в данном случае Hello), а модуль HelloHTML должен содержать определение embed_templates, указывающее, где находятся его шаблоны. По умолчанию контроллер, модуль представления и шаблоны расположены вместе в одном каталоге контроллера. Другими словами, для HelloController требуется HelloHTML, а для HelloHTML необходимо наличие каталога lib/hello_web/controllers/hello_html/, в котором должен находиться шаблон show.html.heex.
render/3 также передаст в качестве assign значение, которое действие show получило из параметров для messenger.
Если при использовании render нужно передать значения в шаблон, это просто. Можно передать список ключевых слов, как мы уже делали с messenger: messenger, или использовать Plug.Conn.assign/3, которая удобно возвращает conn.
def show(conn, %{"messenger" => messenger}) do
conn
|> Plug.Conn.assign(:messenger, messenger)
|> render(:show)
end
Примечание: использование Phoenix.Controller импортирует Plug.Conn, поэтому можно смело использовать сокращённый вызов assign/3.
Передать в шаблон несколько значений так же просто: достаточно связать вызовы функции assign/3:
def show(conn, %{"messenger" => messenger}) do
conn
|> assign(:messenger, messenger)
|> assign(:receiver, "Dweezil")
|> render(:show)
end
Или можно передать assign напрямую в render:
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger, receiver: "Dweezil")
end
Как правило, после настройки всех assign мы вызываем слой представлений. Затем слой представлений (HelloWeb.HelloHTML) отображает show.html вместе с макетом и отправляет ответ обратно в браузер.
Новые форматы рендеринга
Отображать HTML с помощью шаблона удобно, но что делать, если нужно на лету менять формат отображения? Допустим, иногда нам нужен HTML, иногда обычный текст, а иногда JSON. Как быть?
Задача представления — не только отображать HTML-шаблоны. Представления отвечают за представление данных. Получив набор данных, представление должно вывести их в подходящем виде в заданном формате: HTML, JSON, CSV или другом. Сегодня многие веб-приложения возвращают JSON удалённым клиентам, и представления Phoenix отлично подходят для рендеринга JSON.
В качестве примера возьмём действие home модуля PageController из только что созданного приложения. По умолчанию в нём есть подходящее представление PageHTML, встроенные шаблоны из (lib/hello_web/controllers/page_html) и подходящий шаблон для отображения HTML (home.html.heex.)
def home(conn, _params) do render(conn, :home) end
Чего в нём нет, так это представления для отображения JSON. Контроллер Phoenix передаёт задачу отображения шаблонов модулю представления, причём отдельно для каждого формата. У нас уже есть представление для HTML, но нужно также указать Phoenix, как отображать JSON. По умолчанию поддерживаемые контроллерами форматы можно посмотреть в lib/hello_web.ex:
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json]
...
end
end
Таким образом, по умолчанию Phoenix будет искать модули представлений HTML и JSON в зависимости от формата запроса и имени контроллера. Мы также можем явно указать в контроллере, какие представления использовать для каждого формата. Например, поведение Phoenix по умолчанию можно задать в контроллере явно следующим образом:
plug :put_view, html: HelloWeb.PageHTML, json: HelloWeb.PageJSON
Добавим модуль представления PageJSON в lib/hello_web/controllers/page_json.ex:
defmodule HelloWeb.PageJSON do
def home(_assigns) do
%{message: "this is some JSON"}
end
end
Поскольку слой представлений Phoenix — это просто функция, вызываемая контроллером с assign соединения, мы можем определить обычную функцию home/1 и вернуть карту для сериализации в JSON.
Чтобы всё заработало, нужно выполнить ещё несколько действий. Поскольку мы хотим отображать HTML и JSON из одного контроллера, нужно сообщить маршрутизатору, что он должен принимать формат json. Для этого добавим json в список допустимых форматов в конвейере :browser. Откроем lib/hello_web/router.ex и изменим plug :accepts, добавив json наряду с html, как показано ниже.
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html", "json"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
...
Phoenix позволяет менять формат на лету с помощью параметра строки запроса _format. Если перейти по адресу http://localhost:4000/?_format=json, мы увидим %{"message": "this is some JSON"}.
Однако на практике приложениям, которым нужно отображать оба формата, обычно требуются два отдельных конвейера для каждого из них, например pipeline :api, уже определённый в файле маршрутизатора. Подробнее см. в руководстве по JSON и API.
Непосредственная отправка ответов
Если ни один из описанных выше способов рендеринга нам не подходит, можно составить собственный ответ с помощью функций, предоставляемых Plug. Допустим, мы хотим отправить ответ со статусом «201» и совсем без тела. Для этого можно воспользоваться функцией Plug.Conn.send_resp/3.
Измените действие home контроллера PageController в lib/hello_web/controllers/page_controller.ex, чтобы оно выглядело так:
def home(conn, _params) do send_resp(conn, 201, "") end
После перезагрузки страницы http://localhost:4000 должна отобразиться полностью пустая страница. Во вкладке сети инструментов разработчика браузера должен отображаться статус ответа «201» (Created). Некоторые браузеры (Safari) загрузят ответ, поскольку тип содержимого не задан.
Чтобы указать тип содержимого, можно использовать put_resp_content_type/2 вместе с send_resp/3.
def home(conn, _params) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(201, "")
end
Используя таким образом функции Plug, мы можем сформировать именно тот ответ, который нам нужен.
Установка типа содержимого
Подобно параметру строки запроса _format, мы можем отображать любой нужный формат, изменив заголовок HTTP Content-Type и предоставив соответствующий шаблон.
Если нам нужно отобразить XML-версию действия home, мы могли бы реализовать это действие в lib/hello_web/controllers/page_controller.ex следующим образом.
def home(conn, _params) do
conn
|> put_resp_content_type("text/xml")
|> put_format(:xml)
|> render(:home, content: some_xml_content)
end
Затем нужно предоставить шаблон home.xml.eex, который создаёт XML, и представление PageXML, встраивающее этот шаблон, — и всё готово.
Примечание: в шаблоне home.xml.eex используется расширение .eex. Шаблоны .eex обрабатываются с помощью EEx.
Список допустимых MIME-типов содержимого см. в библиотеке MIME.
Установка статуса HTTP
Мы также можем задать код статуса HTTP для ответа так же, как задаём тип содержимого. В модуле Plug.Conn, импортированном во все контроллеры, есть функция put_status/2 для этой цели.
Функция Plug.Conn.put_status/2 принимает conn первым параметром, а вторым — целое число или «понятное имя», используемое как атом для устанавливаемого кода статуса. Список атомов, представляющих коды статуса, приведён в документации Plug.Conn.Status.code/1.
Изменим статус в действии home контроллера PageController.
def home(conn, _params) do conn |> put_status(202) |> render(:home) end
Указанный код статуса должен быть допустимым числом.
Перенаправление
Часто в середине запроса нам нужно перенаправить пользователя на новый URL. Например, успешное действие create обычно перенаправляет на действие show для только что созданного ресурса. Другой вариант — перенаправить на действие index, чтобы показать все элементы того же типа. Перенаправление полезно и во многих других случаях.
Какова бы ни была причина, в контроллерах Phoenix есть удобная функция redirect/2, упрощающая перенаправление. Phoenix различает перенаправление на путь внутри приложения и перенаправление на URL — как внутри приложения, так и за его пределами.
Чтобы опробовать redirect/2, создадим новый маршрут в lib/hello_web/router.ex.
defmodule HelloWeb.Router do
...
scope "/", HelloWeb do
...
get "/", PageController, :home
get "/redirect_test", PageController, :redirect_test
...
end
end
Затем изменим действие home контроллера PageController так, чтобы оно только перенаправляло на наш новый маршрут.
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def home(conn, _params) do
redirect(conn, to: ~p"/redirect_test")
end
end
Для создания пути перенаправления мы воспользовались Phoenix.VerifiedRoutes.sigil_p/2 — это предпочтительный способ ссылаться на любой путь внутри приложения. О проверенных маршрутах мы рассказывали в руководстве по маршрутизации.
Наконец, в том же файле определим действие, на которое выполняется перенаправление: оно просто отображает главную страницу, но теперь по новому адресу:
def redirect_test(conn, _params) do render(conn, :home) end
После перезагрузки приветственной страницы мы увидим, что нас перенаправило на /redirect_test, где отображается исходная приветственная страница. Работает!
При желании можно открыть инструменты разработчика, перейти на вкладку сети и снова открыть корневой маршрут. Мы увидим два основных запроса для этой страницы: GET-запрос к / со статусом 302 и GET-запрос к /redirect_test со статусом 200.
Обратите внимание: функция перенаправления принимает conn и строку, представляющую относительный путь внутри приложения. Из соображений безопасности параметр :to позволяет перенаправлять только на пути внутри приложения. Если нужно перенаправить на полный путь или внешний URL, следует использовать :external:
def home(conn, _params) do redirect(conn, external: "https://elixir-lang.org/") end
Flash-сообщения
Иногда во время выполнения действия нам нужно сообщить что-то пользователям. Возможно, при обновлении схемы произошла ошибка, а возможно, мы просто хотим поприветствовать пользователя, вернувшегося в приложение. Для этого существуют flash-сообщения.
Модуль Phoenix.Controller предоставляет функцию put_flash/3, которая устанавливает flash-сообщения в виде пар ключ-значение и помещает их в assign @flash соединения. Давайте зададим два flash-сообщения в HelloWeb.PageController и проверим, как это работает.
Для этого изменим действие home следующим образом:
defmodule HelloWeb.PageController do
...
def home(conn, _params) do
conn
|> put_flash(:error, "Let's pretend we have an error.")
|> render(:home)
end
end
Чтобы увидеть flash-сообщения, нужно получить их и отобразить в макете шаблона. Для этого можно использовать Phoenix.Flash.get/2, которой передаются данные flash и нужный ключ. Она возвращает значение для этого ключа.
Для удобства компонент flash_group уже доступен и добавлен в начало нашей приветственной страницы
<.flash_group flash={@flash} />
После перезагрузки приветственной страницы сообщение должно появиться в правом верхнем углу.
Flash-функциональность удобна в сочетании с перенаправлениями. Возможно, вы захотите перенаправить пользователя на страницу с дополнительной информацией. Если повторно использовать действие перенаправления из предыдущего раздела, можно сделать так:
def home(conn, _params) do
conn
|> put_flash(:error, "Let's pretend we have an error.")
|> redirect(to: ~p"/redirect_test")
end
Теперь при перезагрузке приветственной страницы произойдёт перенаправление, и flash-сообщение появится снова.
Помимо put_flash/3, в модуле Phoenix.Controller есть ещё одна полезная функция. clear_flash/1 принимает только conn и удаляет все flash-сообщения, которые могли быть сохранены в сеансе.
Phoenix не ограничивает набор ключей, хранящихся во flash. Если мы придерживаемся единого подхода, всё будет работать. Однако :info и :error — распространённые ключи, которые по умолчанию обрабатываются в наших шаблонах.
Страницы ошибок
В Phoenix есть два представления — ErrorHTML и ErrorJSON, расположенные в lib/hello_web/controllers/. Они предназначены для общей обработки ошибок во входящих запросах HTML или JSON. Как и созданные в этом руководстве представления, представления ошибок могут возвращать ответы как HTML, так и JSON. Дополнительную информацию см. в руководстве «Пользовательские страницы ошибок».
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/controllers.html