Conheça Phoenix: uma estrutura semelhante ao Rails para aplicativos Web modernos no Elixir
Publicados: 2022-03-11O framework Phoenix vem crescendo em popularidade em um ritmo acelerado, oferecendo a produtividade de frameworks como Ruby on Rails, além de ser um dos frameworks mais rápidos disponíveis. Isso quebra o mito de que você precisa sacrificar o desempenho para aumentar a produtividade.
Então, o que exatamente é Phoenix?
Phoenix é um framework web construído com a linguagem de programação Elixir. Elixir, construído na Erlang VM, é usado para construir sistemas distribuídos de baixa latência, tolerantes a falhas, que são qualidades cada vez mais necessárias de aplicativos web modernos. Você pode aprender mais sobre o Elixir nesta postagem do blog ou no guia oficial.
Se você é um desenvolvedor Ruby on Rails, definitivamente deveria se interessar pelo Phoenix por causa dos ganhos de desempenho que ele promete. Desenvolvedores de outros frameworks também podem acompanhar para ver como o Phoenix aborda o desenvolvimento web.
Neste artigo vamos aprender algumas das coisas em Phoenix que você deve ter em mente se estiver vindo do mundo do Ruby on Rails.
Ao contrário do Ruby, Elixir é uma linguagem de programação funcional, que provavelmente é a maior diferença com a qual você pode ter que lidar. No entanto, existem algumas diferenças importantes entre essas duas plataformas que qualquer pessoa que esteja aprendendo Phoenix deve estar ciente para maximizar sua produtividade.
As convenções de nomenclatura são mais simples.
Este é pequeno, mas é fácil errar se você estiver vindo do Ruby on Rails.
Em Phoenix, a convenção é escrever tudo na forma singular. Então você teria um “UserController” em vez de um “UsersController” como você faria em Ruby on Rails. Isso se aplica a todos os lugares, exceto ao nomear tabelas de banco de dados, onde a convenção é nomear a tabela no plural.
Isso pode não parecer grande coisa depois de aprender quando e onde usar a forma plural em Ruby on Rails, mas foi um pouco confuso no começo, quando eu estava aprendendo a usar Ruby on Rails, e tenho certeza que confundiu muitos outros também. Phoenix lhe dá uma coisa a menos para se preocupar.
O roteamento é mais fácil de gerenciar.
Phoenix e Ruby on Rails são muito semelhantes quando se trata de roteamento. A principal diferença está em como você pode controlar a maneira como uma solicitação é processada.
Em Ruby on Rails (e outros aplicativos Rack) isso é feito através de middleware, enquanto em Phoenix, isso é feito com o que chamamos de “plugs”.
Plugues são o que você usa para processar uma conexão.
Por exemplo, o middleware Rails::Rack::Logger
registra um pedido no Rails, que no Phoenix seria feito com o plug Plug.Logger
. Basicamente tudo o que você pode fazer no middleware Rack pode ser feito usando plugs.
Aqui está um exemplo de um roteador em Phoenix:
defmodule HelloWorld.Router do use HelloWorld.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end pipeline :api do plug :accepts, ["json"] end scope "/", HelloWorld do pipe_through :browser get "/", HelloController, :index end scope "/api", HelloWorld do pipe_through :api end end
Primeiro, vamos olhar para os pipelines.
São grupos de plugues pelos quais a solicitação passará. Pense nisso como uma pilha de middleware. Eles podem ser usados, por exemplo, para verificar se a solicitação espera HTML, buscar a sessão e garantir que a solicitação seja segura. Isso tudo acontece antes de chegar ao controlador.
Como você pode especificar vários pipelines, pode escolher quais plugues são necessários para rotas específicas. Em nosso roteador, por exemplo, temos um pipeline diferente para solicitações de páginas (HTML) e API (JSON).
Nem sempre faz sentido usar exatamente o mesmo pipeline para diferentes tipos de solicitações. Phoenix nos dá essa flexibilidade.
Visualizações e modelos são duas coisas diferentes.
As visualizações no Phoenix não são iguais às visualizações no Ruby on Rails.
As visualizações no Phoenix são responsáveis por renderizar modelos e fornecer funções que facilitam o uso de dados brutos para os modelos. Uma visão em Phoenix se assemelha mais a um auxiliar em Ruby on Rails, com a adição de renderização do modelo.
Vamos escrever um exemplo que exiba “Hello, World!” no navegador.
Ruby nos trilhos:
app/controllers/hello_controller.rb:
class HelloController < ApplicationController def index end end
app/views/hello/index.html.erb:
<h1>Hello, World!</h1>
Fénix:
web/controllers/hello_controller.ex:
defmodule HelloWorld.HelloController do use HelloWorld.Web, :controller def index(conn, _params) do render conn, "index.html" end end
web/views/hello_view.ex:
defmodule HelloWorld.HelloView do use HelloWorld.Web, :view end
web/templates/hello/index.html.eex:
<h1>Hello, World!</h1>
Esses exemplos exibem “Hello, World!” no navegador, mas têm algumas diferenças importantes.
Primeiro, você precisa declarar explicitamente o modelo que deseja renderizar no Phoenix, diferente do Ruby on Rails.
Em seguida, tivemos que incluir uma visão no Phoenix que fica entre o controlador e o modelo.
Agora, você pode estar se perguntando por que precisamos de uma visão se ela estiver vazia? A chave aqui é que, sob o capô, o Phoenix compila o modelo em uma função aproximadamente igual ao código abaixo:
defmodule HelloWorld.HelloView do use HelloWorld.Web, :view def render("index.html", _assigns) do raw("<h1>Hello, World!</h1>") end end
Você pode excluir o modelo e usar a nova função de renderização e obterá o mesmo resultado. Isso é útil quando você quer apenas retornar texto ou mesmo JSON.
Modelos são apenas isso: modelos de dados.
No Phoenix, os modelos lidam principalmente com a validação de dados, seu esquema, seus relacionamentos com outros modelos e como eles são apresentados.
Especificar o esquema no modelo pode parecer estranho a princípio, mas permite criar facilmente campos “virtuais”, que são campos que não são persistidos no banco de dados. Vejamos um exemplo:
defmodule HelloPhoenix.User do use HelloPhoenix.Web, :model schema "users" do field :name, :string field :email, :string field :password, :string, virtual: true field :password_hash, :string end end
Aqui definimos quatro campos na tabela “users”: nome, email, senha e password_hash.
Não há muito interessante aqui, exceto pelo campo “senha” que é definido como “virtual”.
Isso nos permite definir e obter esse campo sem salvar as alterações no banco de dados. É útil porque teríamos lógica para converter essa senha em um hash, que salvaríamos no campo “password_hash” e depois salvaríamos no banco de dados.
Você ainda precisa criar uma migração; o esquema no modelo é necessário porque os campos não são carregados automaticamente no modelo como em Ruby on Rails.
A diferença entre Phoenix e Ruby on Rails é que o modelo não trata a persistência no banco de dados. Isso é feito por um módulo chamado “Repo” que é configurado com as informações do banco de dados, conforme exemplo abaixo:
config :my_app, Repo, adapter: Ecto.Adapters.Postgres, database: "ecto_simple", username: "postgres", password: "postgres", hostname: "localhost"
Esse código está incluído nos arquivos de configuração específicos do ambiente, como config/dev.exs
ou config/test.exs
. Isso nos permite usar o Repo para realizar operações de banco de dados, como criar e atualizar.

Repo.insert(%User{name: "John Smith", example: "[email protected]"}) do {:ok, user} -> # Insertion was successful {:error, changeset} -> # Insertion failed end
Este é um exemplo comum em controladores em Phoenix.
Fornecemos a um usuário um nome e um e-mail e o Repo tenta criar um novo registro no banco de dados. Podemos então, opcionalmente, lidar com a tentativa bem-sucedida ou com falha, conforme mostrado no exemplo.
Para entender melhor esse código, você precisa entender a correspondência de padrões no Elixir. O valor retornado pela função é uma tupla. A função retorna uma tupla de dois valores, um status e, em seguida, o modelo ou um changeset. Um conjunto de alterações é uma maneira de rastrear alterações e validar um modelo (os conjuntos de alterações serão discutidos na próxima seção).
A primeira tupla, de cima para baixo, que corresponder ao padrão da tupla retornada pela função que tentou inserir o usuário no banco de dados, executará sua função definida.
Poderíamos ter definido uma variável para o status em vez de um átomo (que é basicamente o que um símbolo é em Ruby), mas então combinaríamos tentativas bem-sucedidas e fracassadas e usaríamos a mesma função para ambas as situações. Especificando com qual átomo queremos corresponder, definimos uma função especificamente para esse status.
Ruby on Rails tem a funcionalidade de persistência embutida no modelo através do ActiveRecord. Isso adiciona mais responsabilidade ao modelo e às vezes pode tornar o teste de um modelo mais complexo como resultado. Em Phoenix, isso foi separado de uma maneira que faz sentido e evita o inchaço de todos os modelos com lógica de persistência.
Os conjuntos de alterações permitem regras claras de validação e transformação.
Em Ruby on Rails, validar e transformar dados pode ser a fonte de bugs difíceis de encontrar. Isso ocorre porque não é imediatamente óbvio quando os dados estão sendo transformados por retornos de chamada como “before_create” e validações.
No Phoenix, você faz explicitamente essas validações e transformações usando changesets. Este é um dos meus recursos favoritos em Phoenix.
Vamos dar uma olhada em um changeset adicionando um ao modelo anterior:
defmodule HelloPhoenix.User do use HelloPhoenix.Web, :model schema "users" do field :name, :string field :email, :string field :password, :string, virtual: true field :password_hash, :string end def changeset(struct, params \\ %{}) do struct |> cast(params, [:name, :email, :password]) |> validate_required([:email, :password]) end end
Aqui o changeset faz duas coisas.
Primeiro, ele chama a função “cast” que é uma whitelist de campos permitidos, semelhante a “strong_parameters” em Ruby on Rails, e então valida se os campos “email” e “password” estão incluídos, tornando o campo “name” opcional. Dessa forma, apenas os campos que você permitir poderão ser modificados pelos usuários.
O bom dessa abordagem é que não estamos limitados a um conjunto de alterações. Podemos criar vários conjuntos de alterações. Um changeset para registro e outro para atualização do usuário é comum. Talvez queiramos apenas exigir o campo de senha ao registrar, mas não ao atualizar o usuário.
Compare esta abordagem com o que é comumente feito em Ruby on Rails, você teria que especificar que a validação deve ser executada apenas em “create”. Isso às vezes torna difícil no Rails descobrir o que seu código está fazendo quando você tem um modelo complexo.
A funcionalidade de importação é simples, mas flexível.
Grande parte da funcionalidade de uma biblioteca chamada “Ecto” é importada para os modelos. Os modelos geralmente têm esta linha perto do topo:
use HelloPhoenix.Web, :model
O módulo “HelloPhoenix.Web” está localizado em “web/web.ex”. No módulo, deve haver uma função chamada “modelo” da seguinte forma:
def model do quote do use Ecto.Schema import Ecto import Ecto.Changeset import Ecto.Query end end
Aqui você pode ver quais módulos estamos importando da Ecto. Você pode remover ou adicionar quaisquer outros módulos que desejar aqui e eles serão importados para todos os modelos.
Existem também funções semelhantes como “view” e “controller” que servem ao mesmo propósito para as views e controllers, respectivamente.
As palavras-chave de quote
e use
podem parecer confusas. Para este exemplo, você pode pensar em uma citação como executando diretamente esse código no contexto do módulo que está chamando essa função. Então será o equivalente a ter escrito o código dentro de aspas no módulo.
A palavra-chave use também permite que você execute o código no contexto de onde ele é chamado. Ele essencialmente requer o módulo especificado e, em seguida, chama a macro __using__
no módulo que o executa no contexto de onde foi chamado. Você pode ler mais sobre cotação e uso no guia oficial.
Isso realmente ajuda você a entender onde certas funções estão localizadas no framework e ajuda a reduzir a sensação de que o framework está fazendo muita “mágica”.
A simultaneidade está no centro.
A simultaneidade é gratuita no Phoenix porque é uma característica principal do Elixir. Você obtém um aplicativo que pode gerar vários processos e ser executado em vários núcleos sem se preocupar com a segurança e a confiabilidade do thread.
Você pode gerar um novo processo no Elixir simplesmente:
spawn fn -> 1 + 2 end
Tudo após o spawn
e antes do end
será executado em um novo processo.
Na verdade, cada solicitação no Phoenix é tratada em seu próprio processo. Elixir usa o poder da VM Erlang para trazer simultaneidade confiável e eficiente “de graça”.
Isso também torna o Phoenix uma ótima opção para executar serviços que usam WebSockets, pois os WebSockets precisam manter uma conexão aberta entre o cliente e o servidor (o que significa que você precisa construir seu aplicativo para que ele possa lidar com possivelmente milhares de conexões simultâneas).
Esses requisitos adicionariam muita complexidade a um projeto construído em Ruby on Rails, mas o Phoenix pode atender a esses requisitos gratuitamente através do Elixir.
Se você quiser usar WebSockets em seu aplicativo Phoenix, precisará usar Canais. É o equivalente ao ActionCable
em Ruby on Rails, mas menos complexo de configurar porque você não precisa executar um servidor separado.
Phoenix torna a construção de aplicativos web modernos indolor.
Embora tenhamos focado principalmente nas diferenças, Phoenix tem algumas coisas em comum com Ruby on Rails.
Phoenix segue aproximadamente o mesmo padrão MVC que Ruby on Rails, então descobrir qual código vai para onde não deve ser difícil agora que você conhece as principais diferenças. Phoenix também possui geradores semelhantes ao Ruby on Rails para criar modelos, controladores, migrações e muito mais.
Depois de aprender Elixir, você se aproximará lentamente dos níveis de produtividade do Ruby on Rails quando estiver mais confortável com o Phoenix.
As poucas vezes em que não me sinto tão produtivo é quando encontro um problema que foi resolvido por uma gem em Ruby e não consigo encontrar uma biblioteca semelhante para o Elixir. Felizmente, essas lacunas estão sendo preenchidas lentamente pela crescente comunidade de Elixir.
As diferenças no Phoenix ajudam a resolver muito da dor que vem com o gerenciamento de projetos complexos de Ruby on Rails. Embora eles não resolvam todos os problemas, eles ajudam a empurrá-lo na direção certa. Escolher um framework lento porque você quer produtividade não é mais uma desculpa válida, o Phoenix permite que você tenha ambos.