Phoenix Framework всегда был классным. Но он никогда не был таким классным, как с новым релизом 1.3 (который сейчас находится в стадии RC2).


Произошло много значительных изменений. Крис МакКорд написал полный путеводитель по изменениям. Так же доступна его речь с LonestarElixir, где он подробно рассказывает про ключевые моменты. Вдохновленный его трудами, в своей статье я постараюсь рассказать вам про самые важные изменения в проекте Phoenix.


Давайте начнем!


Перевод выполнен самим автором оригинальной статьи Никитой Соболевым.


Существующие проблемы


Phoenix – новый фреймворк. И, естественно, у него есть некоторые проблемы. Основная команда работала очень старательно, чтобы решить некоторые из самых важных. Итак, каковы эти проблемы?


Директория web — чистая магия


При работе над проектом с использованием Phoenix у вас есть два места для исходного кода: lib/ и web/. Концепция такова:


  • Поместите всю свою бизнес-логику и утилиты внутрь lib/.
  • Поместите всё, что связано с вашим веб-интерфейсом (контроллеры, представления, шаблоны) внутрь веб-каталога web/.

Но понятно ли это разработчикам? Я так не думаю.


Откуда появился этот веб-каталог? Это особенность Phoenix? Или другие фреймворки тоже используют его? Должен ли я использовать lib/ с Phoenix-проектами или он зарезервирован для некоторой глубинной магии? Все эти вопросы появились у меня после моей первой встречи с Phoenix.


До версии 1.2 только директория web/ автоматически перезагружалась. Итак, зачем мне создавать какие-либо файлы внутри lib/ и перезапускать сервер, когда я могу поместить их где-то внутри web/ для быстрой перезагрузки?


Это приводит нас к еще более важным вопросам: относятся ли мои файлы-модели (назовем их моделями в этом конкретном контексте) к web-части приложения или к основной логике? Можно ли разделить логику на разные домены или приложения (например, как в Django)?


Эти вопросы остаются без ответа.


Бизнес-логика в контроллерах


Более того, код шаблона, который идет в Phoenix, предполагает другой способ. Можно получить следующий код в новом проекте:


defmodule Example.UserController do
  use Example.Web, :controller

  # ...

  def update(conn, %{"id" => id, "user" => user_params}) do
    user = Repo.get!(User, id)
    changeset = User.changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, user} ->
        render(conn, Example.UserView, "show.json", user: user)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(Example.ChangesetView, "error.json", changeset: changeset)
    end
  end
end

Что должен делать разработчик, когда пользователю после успешного обновления должно быть отправлено электронное письмо? Контроллер так и просится, чтобы его расширили. Просто поставьте еще одну строку кода перед render/4, что может пойти не так? Но. Только что Phoenix сам подтолкнул нас к неправильному использованию своей кодовой базы: мы пишем бизнес логику в контроллере!


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


Схемы не являются моделями


В какой-то момент без особых причин схемы Ecto стали называться «моделями». В чем разница между «моделью» и «схемой»? Схема — это всего лишь способ определить структуру — структуру базы данных в данном конкретном случае. Модели как концепция намного сложнее схем. Модели должны обеспечивать способ управления данными и выполнять различные действия, как модели в Django или Rails. Elixir как функциональный язык не подходит для концепции «модели», поэтому они были упразднены в проекте Ecto.


Файлы внутри models/ не были организованы. По мере своего роста ваше приложение становится хаотичным. Как эти файлы связаны между собой? В каком контексте мы используем их? Это было трудно понять.


Кроме того, директория models/ рассматривалась как еще одно место для размещения вашей бизнес-логики, что нормально для других языков и фреймворков. Существует уже знакомая концепция «fat models». Но такая концепция, опять же, не подходит для Phoenix по уже названным причинам.


Решения


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


Требования


В этом руководстве предполагается, что у вас есть elixir-1.4, и он работает. Нет? Значит, установите его!


Установка


Для начала вам нужно будет установить новую версию Phoenix:


mix archive.install
https://github.com/phoenixframework/archives/raw/master/phx_new.ez

Создание нового проекта


По завершению установки надо проверить, всё ли на месте. mix help вернет вам что-то вроде этого:


mix phoenix.new       # Creates a new Phoenix v1.1.4 application
mix phx.new           # Creates a new Phoenix v1.3.0-rc.1 application using the experimental generators

Вот тут и проявляется первое изменение: новые генераторы. Старые генераторы назывались phoenix, а новые — просто phx. Теперь нужно меньше печатать. И, что более важно, новое сообщение разработчикам: эти генераторы новые, они будут делать что-то новое для вашего проекта.


Затем нужно создать структуру нового проекта, запустив:


mix phx.new medium_phx_example --no-html --no-brunch

Прежде чем мы увидим какие-либо результаты этой команды, давайте обсудим параметры. --no-html удаляет некоторые компоненты для работы с html, поэтому phx.gen.html больше не будет работать. Но мы строим json API, и нам не нужен html. Аналогично --no-brunch означает: не создавайте brunch-файл для работы со статикой.


Изменения


Веб-директория


Глядя на ваши новые файлы, вы можете задаться вопросом: где находится веб-директория? Ну, вот и второе изменение. И довольно большое. Теперь ваша веб-директория находится внутри lib/. Она была особенной, многие люди неправильно поняли его главную цель, которая состояла в содержании веб-интерфейса для вашего приложения. Это не место для вашей бизнес-логики. Теперь все ясно. Поместите всё внутрь lib/. И оставьте только свои контроллеры, шаблоны и представления внутри новой web-директории. Вот как это выглядит:


lib
L-- medium_phx_example
    +-- application.ex
    +-- repo.ex
    L-- web
        +-- channels
        ¦   L-- user_socket.ex
        +-- controllers
        +-- endpoint.ex
        +-- gettext.ex
        +-- router.ex
        +-- views
        ¦   +-- error_helpers.ex
        ¦   L-- error_view.ex
        L-- web.ex

Где medium_phx_example — имя текущего приложения. Приложений может быть много. Итак, теперь весь код живет в одной и той же директории.


Третье изменение откроется вскоре после просмотра файла web.ex:


defmodule MediumPhxExample.Web do  
  def controller do
    quote do
      use Phoenix.Controller, namespace: MediumPhxExample.Web
      import Plug.Conn
      # Before 1.3 it was just:
      # import MediumPhxExample.Router.Helpers
      import MediumPhxExample.Web.Router.Helpers
      import MediumPhxExample.Web.Gettext
    end
  end

  # Some extra code:
  # ...

end

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


Создание схемы


Это четвертое и моё любимое изменение. Раньше у нас была директория web/models/, которая использовалась для хранения схем. Теперь концепция моделей полностью мертва. Внедрена новая философия:


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

Наше приложение будет содержать только один контекст: Audio. Начнем с создания Audio контекста с двумя схемами Album и Song:


mix phx.gen.json Audio Album albums name:string release:utc_datetime
mix phx.gen.json Audio Song songs album_id:references:audio_albums name:string duration:integer

Синтаксис этого генератора также изменился. Теперь требуется, чтобы имя контекста было первым аргументом. Также обратите внимание на audio_albums, схемы теперь содержат префикс с именем контекста. И вот что происходит со структурой проекта после запуска двух генераторов:


lib
L-- medium_phx_example
    +-- application.ex
    +-- audio
    ¦   +-- album.ex
    ¦   +-- audio.ex
    ¦   L-- song.ex
    +-- repo.ex
    L-- web
        +-- channels
        ¦   L-- user_socket.ex
        +-- controllers
        ¦   +-- album_controller.ex
        ¦   +-- fallback_controller.ex
        ¦   L-- song_controller.ex
        +-- endpoint.ex
        +-- gettext.ex
        +-- router.ex
        +-- views
        ¦   +-- album_view.ex
        ¦   +-- changeset_view.ex
        ¦   +-- error_helpers.ex
        ¦   +-- error_view.ex
        ¦   L-- song_view.ex
        L-- web.ex

Каковы основные изменения в структурах по сравнению с предыдущей версией?


  1. Теперь схемы не принадлежат web/, а директория models/ вообще исчезла.
  2. Схемы теперь разделены контекстом, который определяет, как они связаны друг с другом.

И схемы прямо сейчас являются не более чем описанием таблицы. Чем и должна быть схема в первую очередь. Вот как выглядят наши схемы:


defmodule MediumPhxExample.Audio.Album do
  use Ecto.Schema

  schema "audio_albums" do
    field :name, :string
    field :release, :utc_datetime

    timestamps()
  end
end

defmodule MediumPhxExample.Audio.Song do
  use Ecto.Schema

  schema "audio_songs" do
    field :duration, :integer
    field :name, :string
    field :album_id, :id

    timestamps()
  end
end

Всё за исключением самой схемы исчезло. Нет обязательных полей, никаких функций changeset/2 или каких-либо других. Генератор теперь даже не создает belongs_to для вас. Вы сами управляете связями ваших схем.


Итак, теперь это довольно ясно: схема — не место для вашей бизнес-логики. Всё это обрабатывается контекстом, который выглядит следующим образом:


defmodule MediumPhxExample.Audio do
  @moduledoc """
  The boundary for the Audio system.
  """

  import Ecto.{Query, Changeset}, warn: false
  alias MediumPhxExample.Repo

  alias MediumPhxExample.Audio.Album

  def list_albums do
    Repo.all(Album)
  end

  def get_album!(id), do: Repo.get!(Album, id)

  def create_album(attrs \\ %{}) do
    %Album{}
    |> album_changeset(attrs)
    |> Repo.insert()
  end

  # ...

  defp album_changeset(%Album{} = album, attrs) do
    album
    |> cast(attrs, [:name, :release])
    |> validate_required([:name, :release])
  end

  alias MediumPhxExample.Audio.Song

  def list_songs do
    Repo.all(Song)
  end

  def get_song!(id), do: Repo.get!(Song, id)

  def create_song(attrs \\ %{}) do
    %Song{}
    |> song_changeset(attrs)
    |> Repo.insert()
  end

  # ...

  defp song_changeset(%Song{} = song, attrs) do
    song
    |> cast(attrs, [:name, :duration])
    |> validate_required([:name, :duration])
  end
end

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


Использование контроллера


Раньше у нас было много кода в контроллере по-умолчанию и разработчику было легко расширить шаблонный код. Здесь появляется пятое изменение. Начиная с нового выпуска, шаблонный код в контроллере был уменьшен и реорганизован:


defmodule MediumPhxExample.Web.AlbumController do
  use MediumPhxExample.Web, :controller

  alias MediumPhxExample.Audio
  alias MediumPhxExample.Audio.Album

  action_fallback MediumPhxExample.Web.FallbackController

  # ...

  def update(conn, %{"id" => id, "album" => album_params}) do
    album = Audio.get_album!(id)

    with {:ok, %Album{} = album} <- Audio.update_album(album, album_params) do
      render(conn, "show.json", album: album)
    end
  end

  # ...

end

В действии update/2 теперь есть только три осмысленные строчки кода.


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


Контроллеры даже не обрабатывают ошибки. Для работы с ошибками предназначен специальный новый fallback_controller. Эта новая концепция — шестое изменение. Оно позволяет иметь все обработчики ошибок и коды ошибок в одном месте:


defmodule MediumPhxExample.Web.FallbackController do
  @moduledoc """
  Translates controller action results into valid `Plug.Conn` responses.
  See `Phoenix.Controller.action_fallback/1` for more details.
  """
  use MediumPhxExample.Web, :controller

  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> render(MediumPhxExample.Web.ChangesetView, "error.json", changeset: changeset)
  end

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> render(MediumPhxExample.Web.ErrorView, :"404")
  end
end

Что происходит, когда результат из Audio.update_album(album, album_params) не соответствует {:ok, %Album{} = album}? В этой ситуации вызывается контроллер, определенный в action_fallback. И будет выбран правильный call/2, что в свою очередь возвращает правильный ответ. Легко и приятно. Никаких обработок исключений в контроллере.


Заключение


Внесенные изменения весьма интересны. Их много, они все сфокусированы на том, чтобы загубить старые привычки программистов, которые пришли из других языков программирования. И новые изменения стараются пополнить философию Phoenix-Way новыми практиками. Надеюсь, эта статья была полезна и побудила вас использовать Phoenix Framework по максимуму. Заходите ко мне на GitHub.


Благодарим Никиту за подготовку перевода своей собственной оригинальной статьи и с радостью публикуем материал на Хабре. Никита представляет сообщество ElixirLangMoscow, которое организует митапы по Эликсиру в Москве, а также является активным контрибьютером в опенсорс и вносит значительный вклад в наше сообщество Вунш. На сайте вас ждут 3 десятка тематических статей, еженедельная рассылка и новости из мира Эликсира. А для вопросов у нас есть чат в Телеграме с отличными участниками.

Поделиться с друзьями
-->

Комментарии (3)


  1. begemot_sun
    13.07.2017 14:36
    -1

    Вот разрабатываешь, разрабатываешь.
    А потом приходит чел и говорит, все что мы делали — неверно. Давайте делать так.
    И спрашивается и где они раньше были? А что нельзя было сразу подумать сделать?
    А вот например, значек ~> в версионинровании зависимостей.
    Ну нафига они его везде пихают по умолчанию? Ну ведь много раз обжигались, и все должны знать
    что версии зависимостей в проекте должны быть прибиты гвоздями и изменятся только человеком?
    Ну нет же, жуют эту жвачку. Ну удачи, такое впечатление что школьники.


    1. sl_bug
      13.07.2017 14:40
      +1

      ~> 1.1.0 означает что обновляться будет только минорная версия, в которой обратные совместимости не ломают, если кто-то делает ~> 1.1, то ССЗБ.


    1. jarosluv
      13.07.2017 15:06

      Зависимости и прибиты гвоздями в файле `mix.lock`.

      Такая стрелочка `~>` помогает легче обновлять библиотеку вручную, непременно с помощью ручного ввода команды на обновление.

      А изменения `1.3` не должны убивать приложения на `1.2`, как можно увидеть – изменения вынесены отдельно и являются в некотором роде экспериментальными.