Phoenix First Look

Published October 24, 2022 · 1680 words · 8 minute read

Below is a set of personal notes written while taking a first look at the Elixir language and Phoenix 1.6.14 framework.

First Impressions

I’m placing this section first since the rest is mostly reference material to look back on. My first impressions of this framework, having completed some light reading and a tutorial, are very good.

If the promises pan out, implementing new projects in Elixir promises speed, safety, concurrency, and to dramatically reduce the complexity of deploying simple applications.

A microblog that would have previously required separate backend, frontend, and perhaps websocket-sync repositories can be crushed with a single codebase and far more automagic. I don’t think there is a developer on earth who wouldn’t be tempted by the proposition of more optional magic.

Phoenix goes far past simply querying the server or listening on a websocket for new objects, reducing the load on both the client and server by sending extremely minimal updates when needed.

Past here are mostly personal notes. Stay posted for more neat articles when I actually build and deploy things.

Elixir Basics

See Elixir School: Basics

  • Types work intuitively, like in other languages

  • Function and operator names have two components: a name and the arity, for example: ++/2

  • Lists are implemented as linked lists, and prepending is faster than appending

  • The head and tail of a list can be grabbed with hd and tl functions

  • Maps are the key-value store, allow keys of any type, are defined with %{}

  • Hugo highlights Elixir syntax with elixir, exs, or ex

Installation and Bootstrapping Phoenix

Install Elixir.

Follow this install guide.

# Install or update hex to latest version
$ mix local.hex

# Install Phoenix app generator
$ mix archive.install hex phx_new

# Create a new Phoenix project 'hello'
$ mix phx.new hello

Spin up a postgres docker container.

$ docker pull postgres
$ docker run --name phoenix-hello -p 5432:5432 -e POSTGRES_PASSWORD=<***> -d postgres  

To destroy the container and try again use this guide .

$ docker ps
CONTAINER ID   IMAGE      COMMAND                  CREATED          STATUS          PORTS                    NAMES
c7bc50139b5e   postgres   "docker-entrypoint.s…"   38 seconds ago   Up 36 seconds   0.0.0.0:5432->5432/tcp   phoenix-hello

Then follow this up and running guide to bootstrap a project and point it towards your development database.

Phoenix Project Structure

Read this article on Phoenix directory structure with good explanations for the tree below.

├── _build
├── assets
├── config
├── deps
├── lib
│   ├── hello
│   ├── hello.ex
│   ├── hello_web
│   └── hello_web.ex
├── priv
└── test

Tearing Apart a First Tutorial

Build a real-time Twitter clone in 15 minutes with LiveView and Phoenix 1.5

In your work directory, bootstrap a new project with liveview dependencies.

mix phx.new voidscream --live
cd voidscream

Generating a Liveview

Next, run a generator to create the ’tweet’ migration file, templates, liveviews, and so on.

Context from the Phoenix documentation:

The first argument is the context module. The context is an Elixir module that serves as an API boundary for the given resource. A context often holds many related resources. Therefore, if the context already exists, it will be augmented with functions for the given resource.

The second argument is the schema module. The schema is responsible for mapping the database fields into an Elixir struct.

The remaining arguments are the schema module plural name (used as the schema table name), and an optional list of attributes as their respective names and types. See mix help phx.gen.schema for more information on attributes.

# (All on one line, broken here for clarity)
mix phx.gen.live Timeline Post posts
  username body likes_count:integer reposts_count:integer 
  reported_count:integer social_value:integer

The output is as follows:

* creating lib/voidscream_web/live/post_live/show.ex
* creating lib/voidscream_web/live/post_live/index.ex
* creating lib/voidscream_web/live/post_live/form_component.ex
* creating lib/voidscream_web/live/post_live/form_component.html.heex
* creating lib/voidscream_web/live/post_live/index.html.heex
* creating lib/voidscream_web/live/post_live/show.html.heex
* creating test/voidscream_web/live/post_live_test.exs
* creating lib/voidscream_web/live/live_helpers.ex
* creating lib/voidscream/timeline/post.ex
* creating priv/repo/migrations/20221117234818_create_posts.exs
* creating lib/voidscream/timeline.ex
* injecting lib/voidscream/timeline.ex
* creating test/voidscream/timeline_test.exs
* injecting test/voidscream/timeline_test.exs
* creating test/support/fixtures/timeline_fixtures.ex
* injecting test/support/fixtures/timeline_fixtures.ex
* injecting lib/voidscream_web.ex

Add the live routes to your browser scope in lib/voidscream_web/router.ex:

    live "/posts", PostLive.Index, :index
    live "/posts/new", PostLive.Index, :new
    live "/posts/:id/edit", PostLive.Index, :edit

    live "/posts/:id", PostLive.Show, :show
    live "/posts/:id/show/edit", PostLive.Show, :edit


Remember to update your repository by running migrations:

    $ mix ecto.migrate

The “router” is on line 17 in lib/voidscream_web/router.ex - add the routes under these two lines within this… function? Not sure what this scope thing is yet. Docs are here .

  scope "/", VoidscreamWeb do
    pipe_through :browser
    #    <== Add the suggested lines here.

At this point, as the tutor suggests, you can run the server with mix phx.server, but not before spinning up a simple docker environment as suggested in the previous section and entering the password in config/dev.exs, followed by mix ecto.create to instantiate the database.

V@2:32 Next we cut down the fields in the form template in form_component.html.heex all the way down to one textarea and the submit button:

<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="post-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

    <%= textarea f, :body %>
    <%= error_tag f, :body %>

    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>

In timeline/post.ex, modify the schema to provide defaults and provide some validations. This Voidscream.Timeline.Post module uses the Ecto library to bind a database table to an elixir database structure.

schema "posts" do
  field :body, :string
  field :social_value, :integer, default: 0
  # ... set default 0 for all the other integers
  field :username, :string, default: "Test User"

A method below defines valid changesets.

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [:username, :body, :likes_count, :reposts_count, :reported_count, :social_value])
    |> validate_required([:username, :body, :likes_count, :reposts_count, :reported_count, :social_value])
  end

V@3:20 Remove everything but :body from the cast and validate required functions, and add a length validator for the body field.

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [:body])
    |> validate_required([:body])
    |> validate_length(:body, min: 2, max: 250)
  end

…and be amazed as the websockets shuttle user input back to the server and provide an amazing developer experience. This blows the socks off React plus some API.

Syntax Coffee Break

…but what’s this weird notation with |> all about? It’s a pipe operator and as the docs put it it’s a clean alternative to nesting.

foo(bar(baz(new_function(other_function()))))

In elixir, this semi-readable python becomes:

other_function() |> new_function() |> baz() |> bar() |> foo()

The pipe operator |> passes the result of an expression as the first parameter of another expression.

The pipe takes the result on the left, and passes it to the right hand side.

Rendering with Live Components

V@4:53 By modifying live/post_live/index.html we can improve the tabular rendering by using what is essentially a web component in Phoenix.

Phoenix has improved since the video was made so the syntax here is slightly different than what is in the tutorial.

<div id="posts">
  <%= for post <- @posts do %>
    <%= live_component Voidscream.PostLive.PostComponent, id: post.id, post: post %>
  <% end %>
</div>

…Then start to write your web component. Again, the syntax is slightly updated. V@6:20

defmodule Voidscream.PostLive.PostComponent do
  use VoidscreamWeb, :live_component

  def render(assigns) do
    ~H"""
    <div id={"post-#{@post.id}"} class="post">
      <!-- Rest of post markup -->
    </div>
    """
  end
end

…it took me a while to figure out the updated HEEx syntax.

…without all the fancy styling, I just continued with some braindead markup instead of spending time trying to style my posts exactly like a twitter feed.

defmodule Voidscream.PostLive.PostComponent do
  use VoidscreamWeb, :live_component

  def render(assigns) do
    ~H"""
    <div id={"post-#{@post.id}"} class="post" style="border:1px solid black; padding: 5px; margin-top: 5px;">
        <div><b><%= @post.username %></b></div>
        <div style="padding: 5px;"><%= @post.body %></div>
        <div>
        <small>
          <%= @post.likes_count %> Likes&nbsp;
          <%= @post.reposts_count %> Reposts&nbsp;
          </small>
        </div>
        <span><%= live_redirect "Show", to: Routes.post_show_path(@socket, :show, @post) %>&nbsp;</span>
        <span><%= live_patch "Edit", to: Routes.post_index_path(@socket, :edit, @post) %>&nbsp;</span>
        <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: @post.id, data: [confirm: "Are you sure?"] %>&nbsp;</span>
    </div>
    """
  end
end

In a React/Django environment, this would take many hours to complete. I’m already impressed, and we’re only V@7:00 seven minutes into the video tutorial. Ha.

Making it Realtime

V@7:30 back in voidscream/timeline.ex we can find all the backend logic for handling requests related to the timeline module and post objects.

def create_post(attrs \\ %{}) do
  %Post{}
  |> Post.changeset(attrs)
  |> Repo.insert()
  |> broadcast(:post_created)  # Add this broadcast
end
def update_post(%Post{} = post, attrs) do
  post
  |> Post.changeset(attrs)
  |> Repo.update()
  |> broadcast(:post_updated)  # Add this broadcast
end

Now, we define the broadcast functions V@8:02

defp broadcast({:error, _reason} = error, _event), do: error

defp broadcast({:ok, post}, event) do
  Phoenix.PubSub.broadcast(Voidscream.PubSub, "posts", {event, post})
  {:ok, post}
end

Now we allow users to subscribe to our broadcasted events:

def subscribe do
  Phoenix.PubSub.subscribe(Voidscream.PubSub, "posts")
end

On line 9 in index.ex add:

@impl true
def mount(_params, _session, socket) do
  if connected?(socket), do: Timeline.subscribe()  # add this
  {:ok, assign(socket, :posts, list_posts())}
end

Lower in the file, add a handle_info function:

@impl true
def handle_info({:post_created, post}, socket) do
  {:noreply, update(socket, :posts, fn posts -> [post | posts] end)}
end

This combination of additions will cause every liveview open to this page to update and prepend the new post to the list of posts when a new post is created.

V@10:42 This will work seamlessly over any load-balanced cluster because Phoenix PubSub is distributed with no additional dependencies.

Holy cow, easy peasy!

I’m going to stop following this tutorial here to start a full course and take notes on that. This high level tutorial is awesome but not giving me the deep understanding I need to start developing Phoenix apps.

To Continue?

Does it seem like Phoenix is worth investigating further? Yes.

Luckily my current employer has excellent learning resources, so I have access to a few courses on Elixir and Phoenix. This one was last updated in December 2022 and is taught by Stephen Grider.

I’m not going to lie to you, Elixir and Phoenix are both tough, I guarantee you they are unlike any other programming language and framework you’ve seen before.

Course repo: github.com/StephenGrider/ElixirCode

Focus #1 is to get a solid understanding of Elixir basics: how to write, test, and use the toolbox.

For deployment, check https://aswinmohan.me/phoenix-on-digitalocean

Comments