Adding Phoenix to an OTP Elixir App

Posted on 05 Sep 2017 by Eric Oestrich

For my side project, ex_venture, I wanted to add a web client that allowed players to connect not just via normal telnet. This meant I needed to add Phoenix. I was excited to try this out because the phoenix devs keep saying how you should just think of it as a layer for your app, not the app itself. Since I had an app already this was the perfect trial.

Brief note: ex_venture is a MUD engine, the protocol up until now was only telnet.

Adding Phoenix

Adding phoenix was a pretty simple affair. It took about 2 hours to get it up and running in a simple form. Here is the commit that adds it entirely. I mostly copied from a new phoenix project and took what I wanted.

The fun part was the TelnetChannel that talked with my session module similar to the normal telnet socket.

Phoenix <-> OTP communication

Another interesting part of this addition was I could add a web admin panel to the game. With this I wanted live updates to the game. If I added a new room to a zone, then I should see it reflected immediately in the game. I achieved this by creating bounded contexts that talk to the data layer then push the update into the OTP layer.

Samples

Updating a zone

This shows off updating a zone and pushing the change live into the game. The controller knows nothing about the OTP. Web.Zone is a layer between Phoenix and the game.

Web.Admin.ZoneController
alias Web.Zone

def update(conn, %{"id" => id, "zone" => params}) do
  case Zone.update(id, params) do
    {:ok, zone} -> conn |> redirect(to: zone_path(conn, :show, zone.id))
    {:error, changeset} ->
      zone = Zone.get(id)
      conn |> render("edit.html", zone: zone, changeset: changeset)
  end
end
Web.Zone
alias Data.Zone

def update(id, params) do
  zone = id |> get()
  changeset = zone |> Zone.changeset(params)
  case changeset |> Repo.update do
    {:ok, zone} ->
      Game.Zone.update(zone.id, zone)
      {:ok, zone}
    anything -> anything
  end
end
Game.Zone
# pid expands to the elixir Registry
def update(id, zone) do
  GenServer.cast(pid(id), {:update, zone})
end

def handle_cast({:update, zone}, state) do
  {:noreply, Map.put(state, :zone, zone)}
end

Adding a new room to a zone

This shows off how the Room admin will spawn a new room in a zone after being created. The controller knows nothing about OTP. Web.Room is a layer between Phoenix and the game.

Web.Admin.RoomController
alias Web.Room

def create(conn, %{"zone_id" => zone_id, "room" => params}) do
  zone = Zone.get(zone_id)
  case Room.create(zone, params) do
    {:ok, room} -> conn |> redirect(to: room_path(conn, :show, room.id))
    {:error, changeset} -> conn |> render("new.html", zone: zone, changeset: changeset)
  end
end
Web.Room
alias Data.Room

def create(zone, params) do
  changeset = zone |> Ecto.build_assoc(:rooms) |> Room.changeset(params)
  case changeset |> Repo.insert() do
    {:ok, room} ->
      Game.Zone.spawn_room(zone.id, room)
      {:ok, room}
    anything -> anything
  end
end
Game.Zone

When the Room.Supervisor comes online it lets the zone know it's PID to spawn new rooms in.

def spawn_room(id, room) do
  GenServer.cast(pid(id), {:spawn_room, room})
end

def handle_cast({:spawn_room, room}, state = %{room_supervisor_pid: room_supervisor_pid}) do
  Room.Supervisor.start_child(room_supervisor_pid, room)
  {:noreply, state}
end
Game.Room.Supervisor

The Room.Supervisor starts the new room in the supervision tree.

def start_child(pid, room) do
  child_spec = worker(Room, [room], id: room.id, restart: :permanent)
  Supervisor.start_child(pid, child_spec)
end

Conclusion

Adding Phoenix to an regular OTP app was incredibly simple and the Phoenix team did what they set out to. I hope you explore the rest of the app to find more examples of Phoenix <-> OTP communication.

comments powered by Disqus
Eric Oestrich
I am:
All posts
Creative Commons License
This site's content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License unless otherwise specified. Code on this site is licensed under the MIT License unless otherwise specified.