Phoenix framework что это

Обновлено: 04.07.2024


От переводчика: «Elixir и Phoenix — прекрасный пример того, куда движется современная веб-разработка. Уже сейчас эти инструменты предоставляют качественный доступ к технологиям реального времени для веб-приложений. Сайты с повышенной интерактивностью, многопользовательские браузерные игры, микросервисы — те направления, в которых данные технологии сослужат хорошую службу. Далее представлен перевод серии из 11 статей, подробно описывающих аспекты разработки на фреймворке Феникс казалось бы такой тривиальной вещи, как блоговый движок. Но не спешите кукситься, будет действительно интересно, особенно если статьи побудят вас обратить внимание на Эликсир либо стать его последователями».

На данный момент наше приложение основано на:

Установка Phoenix

Лучшая инструкция по установке Phoenix находится на его официальном сайте.

Шаг 1. Добавление постов

Начнём с запуска mix-задачи для создания нового проекта под названием «pxblog». Для этого выполним команду `mix phoenix.new [project] [command]`. Отвечаем утвердительно на все вопросы, так как нам подойдут настройки по умолчанию.


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

Если у вас не создана база данных Postgres или приложение не настроено на работу с ней, команда `mix ecto.create` выкинет ошибку. Для её исправления откройте файл config/dev.exs и просто измените имя пользователя и пароль для роли, которая имеет права на создание базы данных:


Когда всё заработает, давайте запустим сервер и убедимся, что всё хорошо.


Теперь можем ещё раз выполнить команду и убедиться, что наша миграция прошла успешно.

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

Шаг 1Б. Написание тестов для постов

Одной из прелестей работы со скаффолдами является то, что с самого начала мы получаем набор базовых тестов. Нам даже не нужно особо их изменять до тех пор, пока мы не начнём вносить серьёзные изменения в код самого приложения. Сейчас давайте разберём какие тесты были созданы, чтобы лучше понять как писать собственные.

Во-первых, откроем файл test/models/post_test.exs и взглянем на содержимое:


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

Очевидно, что нам нужно определить тестовый модуль в пространстве имён нашего приложения.
Далее, мы говорим этому модулю использовать функции и DSL, представленные в наборе макросов ModelCase.
Теперь убеждаемся, что тест может обращаться к модели напрямую.
Настраиваем основные действительные атрибуты, которые позволят успешно создать ревизию (changeset). Это просто переменная уровня модуля, которую мы можем дёргать каждый раз, когда хотим создать валидную модель.
Как и выше, но создаём набор недействительных атрибутов.
Теперь, мы непосредственно создаём наш тест функцией test, дав ему строковое имя. Внутри тела нашей функции сначала мы создаём из модели Post ревизию (передавая пустую структуру и список действительных параметров). Затем с помощью функции assert проверяем валидность ревизии. Это как раз то, для чего нам нужна переменная @valid_attrs.

Наконец, мы проверяем создание ревизии с недействительными параметрами, и вместо «утверждения», что ревизия валидная, выполняем обратную операцию refute.

Это очень хороший пример того, как написать тест модели. Теперь давайте посмотрим на тест контроллера. Взглянув на него мы должны увидеть примерно следующее:


Здесь используется Pxblog.ConnCase для получения специального DSL уровня контроллера. Остальные строки должны быть уже знакомы.

Посмотрим на первый тест:


Следующий тест по сути такой же, только уже проверяется действие new. Всё просто.


А здесь мы делаем что-то новенькое. Во-первых, отправляем POST-запрос со списком действительных параметров по адресу post_path. Мы ожидаем получить перенаправление на список постов (действие :index). Функция redirected_to принимает в качестве аргумента объект соединения, так как нам нужно знать, куда произошло перенаправление.

Наконец, мы утверждаем, что объект, представленный этими действительными параметрами был успешно добавлен в базу данных. Эта проверка осуществляется через запрос к репозиторию Ecto Repo на поиск модели Post, соответствующей нашим параметрам @valid_attrs.


Теперь снова попробуем создать пост, но уже с недействительным списком параметров @invalid_attrs, и проверим, что снова отобразится форма создания поста.


Чтобы протестировать действие show, нам нужно создать модель Post, с которой и будем работать. Затем вызовем функцию get с хелпером post_path, и убедимся, что возвращается соответствующий ресурс.

Также мы можем попытаться запросить путь к ресурсу, которого не существует, следующим образом:


Здесь используется другой шаблон записи теста, который на самом деле довольно прост для понимания. Мы описываем ожидание того, что запрос несуществующего ресурса приведёт к ошибке 404. Туда же передаём анонимную функцию, содержащую код, который при выполнении должен вернуть эту самую ошибку. Всё просто!

Остальные тесты лишь повторяют вышеперечисленное для оставшихся путей. А вот на удалении остановимся подробнее:

Шаг 2. Добавляем пользователей

Для создания модели User, мы пройдём практически те же шаги, что и при создании модели постов, за исключением добавления других столбцов. Для начала выполним:


Далее откроем файл web/router.ex и добавим следующую строчку в тот же скоуп, что и ранее:


Синтаксис здесь определяет стандартный ресурсовый путь, где первым аргументом идёт URL, а вторым — имя класса контроллера. Затем выполним:

К сожалению, пока это не очень полезный блог. В конце концов, хоть мы и можем создавать пользователей (к сожалению, сейчас это сможет сделать любой), мы не можем даже войти. Кроме того, дайджест пароля не использует никаких алгоритмов шифрования. Мы тупо храним тот текст, что ввёл пользователь! Совсем не круто!

Давайте приведём этот экран в вид более похожий на регистрацию.

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

Шаг 3. Сохранение хеша пароля, вместо самого пароля

В файле web/templates/user/form.html.eex удалите следующие строки:


И добавьте на их место:


После обновления страницы (должно происходить автоматически) введите данные пользователя и нажмите кнопку Submit.


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

Начнём с изменения схемы. В файле web/models/user.ex добавим пару строк:


Обратите внимание на добавление двух полей :password и :password_confirmation. Мы объявили их в качестве виртуальных полей, так как на самом деле их не существует в нашей базе данных, но они должны существовать как свойства в структуре User. Это также позволяет применять преобразования в нашей функции changeset.

Затем мы добавим :password и :password_confirmation в список обязательных полей:


Если вы сейчас попробуете запустить тесты из файла test/models/user_test.exs, то тест “changeset with valid attributes” упадёт. Это происходит потому, что мы добавили :password и :password_confirmation к обязательным параметрам, но не обновили @valid_attrs. Давайте изменим эту строчку:


Наши тесты моделей должны снова проходить! Теперь нужно починить тесты контроллеров. Внесём некоторые изменения в файл test/controllers/user_controller_test.exs. Вначале выделим валидные атрибуты для создания объекта в отдельную переменную:


Затем изменим наш тест на создание пользователя:


И тест на обновление пользователя:


После того, как наши тесты снова стали зелёными, нам нужно добавить в changeset функцию, преобразующую пароль в дайджест:

Снова запустим тесты этого файла. Они проходят, но не хватает теста на проверку работы функции hash_password. Давайте добавлять:

Для этого откройте файл mix.exs и добавьте :comeonin в список applications:


Также нам нужно изменить наши зависимости. Обратите внимание на


Теперь отключим запущенный сервер и выполним команду `mix deps.get`. Если все пройдет хорошо (а оно должно!), то командой `iex -S mix phoenix.server` вы снова сможете запустить сервер.

Наш старый метод hash_password неплох, но вообще-то нам нужно, чтобы пароль хешировался на самом деле. Так как мы добавили библиотеку Comeonin, которая предоставляет нам прекрасный модуль Bcrypt с методом hashpwsalt, который мы импортируем в нашу модель User. В файле web/models/user.ex добавьте приведённую ниже строчку сразу следом за use Pxblog.Web, :model:


Что мы сейчас сделали? Мы вытянули модуль Bcrypt из пространства имён Comeonin и импортировали метод hashpwsalt с арностью 1. А следующим кодом мы заставим функцию hash_password работать:


Предлагаю попробовать создать пользователя ещё раз! На этот раз, после регистрации мы должны увидеть шифрованный дайджест в поле password_digest!

Теперь давайте немного доработаем функцию hash_password. Во-первых, чтобы шифрование пароля не тормозило тестирование, необходимо внести изменения в настройки тестового окружения. Для этого откройте файл config/test.exs и добавьте следующую строчку в самый низ:


Это скажет библиотеке Comeonin не слишком сильно шифровать пароль во время выполнения тестов, поскольку в тестах нам важнее скорость, чем безопасность! А в продакшене (файл config/prod.exs) нам наоборот нужно усилить защиту:


Давайте напишем тест для вызова Comeonin. Мы сделаем его менее подробным, так как хотим лишь убедиться в работе шифрования. В файле test/models/user_test.exs:


Для улучшения покрытия тестами, давайте рассмотрим случай, когда строчка `if the password = get_change() не является истиной:


В данном случае поле password_digest должно оставаться пустым, что и происходит! Мы проделываем хорошую работу, покрывая наш код тестами!

Шаг 4. Давайте войдём!

Добавим новый контроллер SessionController и сопутствующее представление SessionView. Начнём с простого, а со временем придём к более правильной реализации.

Создайте файл web/controllers/session_controller.ex:


А также web/views/session_view.ex:


И напоследок web/templates/session/new.html.eex:


Следующую строку добавьте в скоуп "/":


Тем самым мы включаем наш новый контроллер в маршрутизатор. Единственный путь, который нам сейчас понадобится, это new, что мы явно и указываем. Опять же, нам нужно получить наиболее устойчивый фундамент наиболее простыми методами.

Добавим сюда настоящую форму. Для этого создадим файл web/templates/session/form.html.eex:


И вызовем только что созданию форму в файле web/templates/session/new.html.eex с помощью всего лишь одной строчки:


Благодаря автоматической перезагрузке кода, на странице отобразится ошибка, так как мы ещё не определили переменную @changeset, которая как можно догадаться должна быть ревизией. Раз мы работаем с объектом, у которого есть поля :name и :password, давайте их и использовать!

В файле web/controllers/session_controller.ex нам необходимо добавить алиас модели User, чтобы мы спокойно могли к ней обращаться дальше. В верхней части нашего класса, прямо под строкой use Pxblog.Web, :controller добавьте следующее:


И в функции new измените вызов рендера, как показано ниже:


Мы должны передать сюда объект соединения, шаблон который мы хотим отрендерить (без расширения eex) и список дополнительных переменных, которые будут использоваться внутри шаблона. В данном случае нам нужно указать changeset: и передать ему ревизию Ecto для User с пустой структурой пользователя.

Обновим страничку. Теперь мы должны увидеть другую ошибку, выглядящую следующим образом:


В нашей форме мы ссылаемся на путь, которого пока не существует. Мы используем хелпер session_path, передавая ему объект @conn, но затем указываем путь :create, который ещё только предстоит создать.

Половина пути пройдена. Теперь давайте реализуем возможность реального входа с использованием сессии. Для этого изменим наши пути.

В файле web/router.ex включим :create в описание SessionController:


В файле web/controllers/session_controller.ex импортируем функцию checkpw из модуля Bcrypt библиотеки Comeonin:


В этой строке говорится “Импортируй из модуля Comeonin.Bcrypt только функцию checkpw с арностью 2".

А затем подключим плаг scrub_params, который будет работать с пользовательскими данными. Добавим перед нашими функциями:


scrub_params — это специальная функция, которая очищает пользовательский ввод. В случае, когда, например, какой-либо атрибут передаётся как пустая строка, scrub_params сконвертирует её в значение nil, чтобы избежать создание в базе данных записей с пустыми строками.

Следом добавим функцию для обработки действия create. Расположим её внизу модуля SessionController. Здесь будет много кода, так что давайте разберём его по частям.

В файле web/controllers/session_controller.ex:


Первый кусочек кода Repo.get_by(User, username: user_params[“username”]) вытаскивает подходящего пользователя User из нашего репозитория Ecto Repo, если username совпадает либо возвращает nil.

Вот небольшой кусочек вывода, чтобы проверить это поведение:


Затем мы берем пользователя, и передаём его по цепочке в функцию sign_in. Мы до сих пор её не написали, так что давайте этим и займемся!

Нам также нужно написать тесты на этот контроллер. Создадим файл test/controllers/session_controller_test.exs и заполним его следующим кодом:


Начинаем со стандартного блока setup и довольно обычной проверки GET-запроса. Тест на создание выглядит более интересным:

Шаг 5. Улучшаем нашего current_user

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

Для этого в файл web/views/layout_view.ex добавим хелпер, который облегчит получение информации о текущем пользователе:


Теперь откроем файл web/templates/layout/app.html.eex и вместо ссылки Get Started добавим следующее:


Давайте снова разбирать код по шагам. Одной из первых вещей, которую нам нужно сделать — это выяснить кто текущий пользователь, предполагая, что он уже вошёл в систему. Сначала сделаем решение в лоб, а рефакторингом займёмся после. Установим пользователя из сессии прямо в нашем шаблоне. Функция get_session — это часть объекта Conn.

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

Если мы не смогли найти пользователя, то просто показываем ссылку на вход. Здесь мы снова рассматриваем сессию как ресурс, так что new предоставит правильный путь для создания новой сессии.

В файле web/router.ex добавляем к маршрутам сессии также и :delete:


Ещё нужно изменить контроллер. В файл web/controllers/session_controller.ex добавьте следующее:

Теперь мы можем войти, выйти и проверить неудачный вход. Всё идёт к лучшему! Но прежде нам нужно написать несколько тестов. Мы начнём с тестов для нашего LayoutView. Первое, что мы собираемся сделать, это прописать алиасы для модулей LayoutView и User, чтобы сократить код. Далее в блоке настройки мы создаём пользователя и добавляем его в базу данных. А затем возвращаем стандартный кортеж .

Теперь рассмотрим сами тесты. В первом из них мы создаём сессию и утверждаем, что функция LayoutView.current_user должна вернуть определённые данные. Во втором же рассмотрим обратную ситуацию. Мы явно удаляем сессию и опровергаем, что функция current_user возвращает пользователя.

Также мы добавили действие delete в SessionController, следовательно на это тоже нужно написать тест:

На этом первая часть подошла к концу.

Важное заключение от переводчика

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

Другие статьи серии

Читайте также: