Elixir dan Phoenix Framework: Panduan Lengkap untuk Developer Modern

Elixir adalah bahasa fungsional yang berjalan di BEAM VM, menawarkan concurrency luar biasa, fault tolerance, dan produktivitas tinggi. Dengan Phoenix Framework, Anda bisa membangun web application yang sangat scalable.

1. Pengenalan Elixir

Elixir dibuat oleh JosΓ© Valim pada tahun 2011. Bahasa ini berjalan di atas Erlang Virtual Machine (BEAM), yang telah terbukti selama puluhan tahun dalam membangun sistem yang sangat fault-tolerant dan concurrent.

Keunggulan utama Elixir:

  • Fault Tolerance β€” "Let it crash" philosophy dari Erlang/OTP
  • Concurrency β€” Jutaan proses lightweight yang bisa berjalan bersamaan
  • Distribution β€” Built-in support untuk distributed computing
  • Hot Code Reloading β€” Update kode tanpa restart sistem
  • Immutable Data β€” Tidak ada state yang berubah, menghindari banyak bug
  • Productivity β€” Sintaks yang indah dan ekspresif
πŸ’‘ BEAM VM

BEAM (Bogdan/BjΓΆrn's Erlang Abstract Machine) adalah virtual machine yang menjalankan Erlang dan Elixir. BEAM mampu menangani jutaan proses concurrent dengan overhead yang sangat kecil. WhatsApp menggunakan Erlang/BEAM untuk melayani 2 juta koneksi per server.

2. Instalasi dan Setup

Bash
# macOS
brew install elixir

# Ubuntu/Debian
wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
sudo dpkg -i erlang-solutions_2.0_all.deb
sudo apt-get update
sudo apt-get install elixir

# Windows
# Download installer dari https://elixir-lang.org/install.html

# Verifikasi
elixir --version
# Erlang/OTP 26, Elixir 1.16.x

# Install Hex dan rebar
mix local.hex --force
mix local.rebar --force

Membuat Proyek Pertama

Bash
# Buat proyek baru
mix new hello_elixir
cd hello_elixir

# Jalankan di interactive shell
iex -S mix

# Jalankan test
mix test

# Kompilasi
mix compile

3. Dasar-Dasar Elixir

Basic Types dan Operators

iex (Elixir Shell)
# Numbers
iex> 42
42
iex> 3.14
3.14
iex> 0xFF  # hexadecimal
255
iex> 0b1010  # binary
10

# Atoms (seperti symbols)
iex> :hello
:hello
iex> :world
:world
iex> true == :true  # boolean adalah atom
true

# Strings
iex> "Hello, Elixir!"
"Hello, Elixir!"
iex> "Interpolasi: #{1 + 1}"
"Interpolasi: 2"

# Lists
iex> [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
iex> [head | tail] = [1, 2, 3, 4, 5]
iex> head
1
iex> tail
[2, 3, 4, 5]

# Tuples
iex> {:ok, "berhasil"}
{:ok, "berhasil"}
iex> {status, message} = {:ok, "data loaded"}

# Maps
iex> %{nama: "Andi", umur: 25}
%{nama: "Andi", umur: 25}
iex> map = %{nama: "Andi", umur: 25}
iex> map.nama
"Andi"
iex> %{map | umur: 26}  # update
%{nama: "Andi", umur: 26}

# Keyword lists
iex> [name: "Andi", age: 25]
[name: "Andi", age: 25]

Modules dan Functions

lib/kalkulator.ex
defmodule Kalkulator do
  @moduledoc """
  Modul kalkulator sederhana untuk demonstrasi Elixir.
  """

  # Named functions
  def tambah(a, b) do
    a + b
  end

  # Single-line syntax
  def kurang(a, b), do: a - b
  def kali(a, b), do: a * b

  # Guard clauses
  def bagi(_a, 0), do: {:error, "Tidak bisa bagi nol"}
  def bagi(a, b), do: {:ok, a / b}

  # Pattern matching pada parameter
  def hitung({:tambah, a, b}), do: a + b
  def hitung({:kurang, a, b}), do: a - b
  def hitung({:kali, a, b}), do: a * b
  def hitung({:bagi, a, b}), do: a / b

  # Private functions
  defp validate_number(n) when is_number(n), do: true
  defp validate_number(_), do: false

  # Recursive functions
  def factorial(0), do: 1
  def factorial(n) when n > 0 do
    n * factorial(n - 1)
  end

  # Higher-order functions
  def apply_operation(a, b, operation_fn) do
    operation_fn.(a, b)
  end
end

# Penggunaan
# iex> Kalkulator.tambah(5, 3)
# 8
# iex> Kalkulator.bagi(10, 0)
# {:error, "Tidak bisa bagi nol"}
# iex> Kalkulator.factorial(5)
# 120

Anonymous Functions

anonymous.ex
# Anonymous functions
tambah = fn a, b -> a + b end
IO.puts tambah.(3, 5)  # 8

# Shorthand syntax (&)
tambah2 = &(&1 + &2)
IO.puts tambah2.(3, 5)  # 8

# Pattern matching dalam anonymous function
handle_result = fn
  {:ok, data} -> "Berhasil: #{data}"
  {:error, reason} -> "Gagal: #{reason}"
end

IO.puts handle_result.({:ok, "data loaded"})
IO.puts handle_result.({:error, "timeout"})

# Closures
buat_multiplier = fn faktor ->
  fn angka -> angka * faktor end
end

kali_dua = buat_multiplier.(2)
kali_tiga = buat_multiplier.(3)

IO.puts kali_dua.(10)   # 20
IO.puts kali_tiga.(10)  # 30

4. Pattern Matching

Pattern matching adalah fitur paling powerful di Elixir. Ini bukan hanya assignment, tapi cara untuk mengekstrak dan memeriksa data.

pattern_matching.ex
defmodule PatternDemo do
  # Basic pattern matching
  def contoh_dasar do
    # Match pada tuple
    {:ok, hasil} = {:ok, 42}
    IO.puts "Hasil: #{hasil}"

    # Match pada list
    [a, b, c] = [1, 2, 3]
    IO.puts "a=#{a}, b=#{b}, c=#{c}"

    # Head | Tail
    [head | tail] = [1, 2, 3, 4, 5]
    IO.puts "Head: #{head}"     # 1
    IO.inspect tail              # [2, 3, 4, 5]

    # Map pattern matching
    %{nama: nama, umur: umur} = %{nama: "Andi", umur: 25, kota: "Jakarta"}
    IO.puts "#{nama}, #{umur}"
  end

  # Pin operator (^) - match existing variable
  def contoh_pin do
    x = 10
    ^x = 10  # OK, karena x sudah 10
    # ^x = 20  # MatchError! karena x bukan 20
  end

  # Case expression
  def classify(value) do
    case value do
      {:ok, data} when is_binary(data) ->
        "String OK: #{data}"
      {:ok, data} when is_number(data) ->
        "Number OK: #{data}"
      {:error, reason} ->
        "Error: #{reason}"
      _ ->
        "Unknown format"
    end
  end

  # With expression (railway-oriented programming)
  def proses_data(input) do
    with {:ok, parsed} <- parse(input),
         {:ok, validated} <- validate(parsed),
         {:ok, saved} <- save(validated) do
      {:ok, saved}
    else
      {:error, :invalid_format} -> {:error, "Format tidak valid"}
      {:error, :validation_failed} -> {:error, "Validasi gagal"}
      error -> error
    end
  end

  defp parse(input) when is_binary(input), do: {:ok, String.to_integer(input)}
  defp parse(_), do: {:error, :invalid_format}

  defp validate(n) when n > 0, do: {:ok, n}
  defp validate(_), do: {:error, :validation_failed}

  defp save(data), do: {:ok, "saved_#{data}"}
end

Guards dan Advanced Pattern Matching

guards.ex
defmodule GuardsDemo do
  # Guard clauses
  def classify_number(n) when is_integer(n) and n > 0, do: :positif
  def classify_number(n) when is_integer(n) and n < 0, do: :negatif
  def classify_number(0), do: :nol
  def classify_number(_), do: :bukan_angka

  # Multiple pattern matching
  def handle_message({:chat, from, message}) do
    IO.puts "Chat dari #{from}: #{message}"
  end

  def handle_message({:join, user, room}) do
    IO.puts "#{user} bergabung ke #{room}"
  end

  def handle_message({:leave, user, room}) do
    IO.puts "#{user} meninggalkan #{room}"
  end

  def handle_message(_) do
    IO.puts "Pesan tidak dikenal"
  end

  # List pattern matching
  def sum_list([]), do: 0
  def sum_list([head | tail]) do
    head + sum_list(tail)
  end

  # Map pattern matching dengan partial match
  def greet(%{nama: nama, role: :admin}) do
    "Selamat datang, Admin #{nama}!"
  end

  def greet(%{nama: nama}) do
    "Halo, #{nama}!"
  end
end

5. Pipe Operator dan Data Transformation

Pipe operator |> adalah salah satu fitur paling dicintai di Elixir. Ini memungkinkan Anda merangkai operasi secara berurutan dengan sangat readable.

pipe.ex
defmodule PipeDemo do
  # Tanpa pipe operator (nested, sulit dibaca)
  def tanpa_pipe(data) do
    String.trim(
      String.upcase(
        String.replace(data, " ", "_")
      )
    )
  end

  # Dengan pipe operator (linear, mudah dibaca)
  def dengan_pipe(data) do
    data
    |> String.replace(" ", "_")
    |> String.upcase()
    |> String.trim()
  end

  # Real-world example: data processing pipeline
  def proses_pengguna(data_list) do
    data_list
    |> Enum.filter(fn %{umur: umur} -> umur >= 17 end)
    |> Enum.map(fn %{nama: nama, email: email} ->
      %{
        nama: String.upcase(nama),
        email: String.downcase(email),
        verified: true
      }
    end)
    |> Enum.sort_by(& &1.nama)
    |> Enum.take(10)
  end

  # Pipeline dengan error handling
  def proses_dengan_error(data) do
    data
    |> String.trim()
    |> parse_integer()
    |> validate_range(1, 100)
    |> format_output()
  end

  defp parse_integer(str) do
    case Integer.parse(str) do
      {num, ""} -> {:ok, num}
      _ -> {:error, "Bukan integer"}
    end
  end

  defp validate_range({:ok, n}, min, max) when n >= min and n <= max do
    {:ok, n}
  end
  defp validate_range({:ok, _n}, _min, _max) do
    {:error, "Di luar range"}
  end
  defp validate_range(error, _min, _max), do: error

  defp format_output({:ok, n}), do: "Valid: #{n}"
  defp format_output({:error, msg}), do: "Error: #{msg}"
end

# Enum module - best friend untuk data transformation
defmodule DataProcessing do
  def demo do
    data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    # Map - transformasi
    kuadrat = Enum.map(data, &(&1 * &1))

    # Filter
    genap = Enum.filter(data, &(rem(&1, 2) == 0))

    # Reduce
    total = Enum.reduce(data, 0, &+/2)

    # Group by
    grouped = Enum.group_by(data, &(if rem(&1, 2) == 0, do: :genap, else: :ganjil))

    # Flat map
    nested = [[1, 2], [3, 4], [5, 6]]
    flat = Enum.flat_map(nested, &(&1))

    IO.inspect kuadrat, label: "Kuadrat"
    IO.inspect genap, label: "Genap"
    IO.inspect total, label: "Total"
    IO.inspect grouped, label: "Grouped"
    IO.inspect flat, label: "Flat"
  end
end

6. GenServer

GenServer (Generic Server) adalah salah satu behaviour paling penting di Elixir/OTP. Ini digunakan untuk membangun stateful processes yang bisa menerima pesan secara asynchronous.

πŸ’‘ Apa itu GenServer?

GenServer adalah abstraction untuk membangun server process yang mengelola state, menangani synchronous dan asynchronous requests, dan bisa di-terminate dengan bersih. Ini adalah building block utama untuk sistem concurrent di Elixir.

lib/counter_server.ex
defmodule CounterServer do
  use GenServer

  # ============ Client API ============

  def start_link(initial_value \\ 0) do
    GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
  end

  def increment do
    GenServer.call(__MODULE__, :increment)
  end

  def decrement do
    GenServer.call(__MODULE__, :decrement)
  end

  def get_value do
    GenServer.call(__MODULE__, :get_value)
  end

  def reset do
    GenServer.cast(__MODULE__, :reset)
  end

  def add_async(value) do
    GenServer.cast(__MODULE__, {:add, value})
  end

  # ============ Server Callbacks ============

  @impl true
  def init(initial_value) do
    IO.puts("CounterServer dimulai dengan nilai: #{initial_value}")
    {:ok, %{value: initial_value, history: []}}
  end

  # Synchronous requests (menunggu response)
  @impl true
  def handle_call(:increment, _from, %{value: val, history: hist} = state) do
    new_val = val + 1
    {:reply, new_val, %{state | value: new_val, history: [{:inc, new_val} | hist]}}
  end

  @impl true
  def handle_call(:decrement, _from, %{value: val, history: hist} = state) do
    new_val = val - 1
    {:reply, new_val, %{state | value: new_val, history: [{:dec, new_val} | hist]}}
  end

  @impl true
  def handle_call(:get_value, _from, %{value: val} = state) do
    {:reply, val, state}
  end

  # Asynchronous requests (tidak menunggu response)
  @impl true
  def handle_cast(:reset, state) do
    {:noreply, %{state | value: 0, history: [{:reset, 0} | state.history]}}
  end

  @impl true
  def handle_cast({:add, amount}, %{value: val, history: hist} = state) do
    new_val = val + amount
    {:noreply, %{state | value: new_val, history: [{:add, new_val} | hist]}}
  end

  # Handle info (messages yang bukan call/cast)
  @impl true
  def handle_info(:tick, state) do
    IO.puts("Tick! Current value: #{state.value}")
    {:noreply, state}
  end

  # Termination callback
  @impl true
  def terminate(reason, state) do
    IO.puts("CounterServer terminated: #{inspect(reason)}")
    IO.puts("Final value: #{state.value}")
    :ok
  end
end

# Penggunaan:
# {:ok, _pid} = CounterServer.start_link(0)
# CounterServer.increment()    => 1
# CounterServer.increment()    => 2
# CounterServer.get_value()    => 2
# CounterServer.reset()        => :ok
# CounterServer.add_async(10)  => :ok

GenServer dengan Timer

lib/timer_server.ex
defmodule TimerServer do
  use GenServer

  def start_link do
    GenServer.start_link(__MODULE__, %{count: 0, interval: 5000})
  end

  @impl true
  def init(state) do
    schedule_tick()
    {:ok, state}
  end

  @impl true
  def handle_info(:tick, %{count: count, interval: interval} = state) do
    IO.puts("[Timer] Tick #{count + 1} setiap #{interval}ms")
    schedule_tick()
    {:noreply, %{state | count: count + 1}}
  end

  defp schedule_tick do
    Process.send_after(self(), :tick, 5000)
  end
end

7. Phoenix Framework

Phoenix adalah web framework untuk Elixir yang terinspirasi dari Rails. Phoenix menawarkan performa tinggi, real-time features, dan arsitektur yang sangat terstruktur.

Membuat Phoenix Project

Bash
# Install Phoenix installer
mix archive.install hex phx_new

# Buat project baru dengan database
mix phx.new belajar_phoenix --database postgres

# Masuk ke project
cd belajar_phoenix

# Setup database
mix ecto.create
mix ecto.migrate

# Jalankan server
mix phx.server

# Buka http://localhost:4000

Router dan Controller

lib/belajar_phoenix_web/router.ex
defmodule BelajarPhoenixWeb.Router do
  use BelajarPhoenixWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {BelajarPhoenixWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", BelajarPhoenixWeb do
    pipe_through :browser

    get "/", PageController, :home
    get "/tentang", PageController, :about
  end

  scope "/api", BelajarPhoenixWeb do
    pipe_through :api

    resources "/users", UserController, except: [:new, :edit]
    resources "/posts", PostController
  end

  # LiveView routes
  live "/dashboard", DashboardLive
  live "/chat", ChatLive
  live "/counter", CounterLive
end

Controller

lib/belajar_phoenix_web/controllers/user_controller.ex
defmodule BelajarPhoenixWeb.UserController do
  use BelajarPhoenixWeb, :controller

  alias BelajarPhoenix.Accounts

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, :index, users: users)
  end

  def show(conn, %{"id" => id}) do
    user = Accounts.get_user!(id)
    render(conn, :show, user: user)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
      {:ok, user} ->
        conn
        |> put_status(:created)
        |> put_resp_header("location", ~p"/api/users/#{user}")
        |> render(:show, user: user)

      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(:errors, changeset: changeset)
    end
  end

  def update(conn, %{"id" => id, "user" => user_params}) do
    user = Accounts.get_user!(id)

    case Accounts.update_user(user, user_params) do
      {:ok, user} ->
        render(conn, :show, user: user)

      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(:errors, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    user = Accounts.get_user!(id)
    {:ok, _user} = Accounts.delete_user(user)
    send_resp(conn, :no_content, "")
  end
end

Ecto (Database)

lib/belajar_phoenix/accounts/user.ex
defmodule BelajarPhoenix.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer
    field :role, Ecto.Enum, values: [:admin, :user, :moderator], default: :user
    has_many :posts, BelajarPhoenix.Content.Post

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :age, :role])
    |> validate_required([:name, :email])
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than: 0, less_than: 150)
    |> unique_constraint(:email)
  end
end

defmodule BelajarPhoenix.Accounts do
  import Ecto.Query
  alias BelajarPhoenix.Repo
  alias BelajarPhoenix.Accounts.User

  def list_users do
    Repo.all(User)
  end

  def get_user!(id), do: Repo.get!(User, id)

  def create_user(attrs) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end

  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end

  def delete_user(%User{} = user) do
    Repo.delete(user)
  end

  def list_users_with_posts do
    User
    |> preload(:posts)
    |> Repo.all()
  end
end

8. Phoenix LiveView

Phoenix LiveView memungkinkan Anda membangun real-time, interactive web applications tanpa menulis JavaScript. Semua state management dan rendering terjadi di server.

lib/belajar_phoenix_web/live/counter_live.ex
defmodule BelajarPhoenixWeb.CounterLive do
  use BelajarPhoenixWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0, step: 1)}
  end

  @impl true
  def handle_event("increment", _params, socket) do
    new_count = socket.assigns.count + socket.assigns.step
    {:noreply, assign(socket, count: new_count)}
  end

  @impl true
  def handle_event("decrement", _params, socket) do
    new_count = socket.assigns.count - socket.assigns.step
    {:noreply, assign(socket, count: new_count)}
  end

  @impl true
  def handle_event("reset", _params, socket) do
    {:noreply, assign(socket, count: 0)}
  end

  @impl true
  def handle_event("set_step", %{"step" => step}, socket) do
    case Integer.parse(step) do
      {n, ""} when n > 0 ->
        {:noreply, assign(socket, step: n)}
      _ ->
        {:noreply, socket}
    end
  end

  @impl true
  def render(assigns) do
    ~H"""
    

πŸ”’ Counter: <%= @count %>

Step: <%= @step %>

""" end end

LiveView dengan Form

lib/belajar_phoenix_web/live/todo_live.ex
defmodule BelajarPhoenixWeb.TodoLive do
  use BelajarPhoenixWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     assign(socket,
       todos: [],
       new_todo: "",
       filter: :all
     )}
  end

  @impl true
  def handle_event("add_todo", %{"todo" => %{"text" => text}}, socket) do
    if String.trim(text) != "" do
      new_todo = %{
        id: System.unique_integer([:positive]),
        text: String.trim(text),
        completed: false,
        created_at: DateTime.utc_now()
      }
      {:noreply, assign(socket, todos: socket.assigns.todos ++ [new_todo], new_todo: "")}
    else
      {:noreply, socket}
    end
  end

  @impl true
  def handle_event("toggle_todo", %{"id" => id}, socket) do
    todos =
      Enum.map(socket.assigns.todos, fn todo ->
        if to_string(todo.id) == id do
          %{todo | completed: !todo.completed}
        else
          todo
        end
      end)
    {:noreply, assign(socket, todos: todos)}
  end

  @impl true
  def handle_event("delete_todo", %{"id" => id}, socket) do
    todos = Enum.reject(socket.assigns.todos, &(to_string(&1.id) == id))
    {:noreply, assign(socket, todos: todos)}
  end

  @impl true
  def handle_event("set_filter", %{"filter" => filter}, socket) do
    filter_atom = String.to_existing_atom(filter)
    {:noreply, assign(socket, filter: filter_atom)}
  end

  defp filtered_todos(todos, :all), do: todos
  defp filtered_todos(todos, :active), do: Enum.reject(todos, & &1.completed)
  defp filtered_todos(todos, :completed), do: Enum.filter(todos, & &1.completed)

  @impl true
  def render(assigns) do
    ~H"""
    

πŸ“ Todo List

    <%= for todo <- filtered_todos(@todos, @filter) do %>
  • <%= todo.text %>
  • <% end %>

<%= length(Enum.reject(@todos, & &1.completed)) %> item tersisa

""" end end

9. Channels dan Real-time

Phoenix Channels memungkinkan komunikasi real-time antara client dan server menggunakan WebSocket.

lib/belajar_phoenix_web/channels/chat_channel.ex
defmodule BelajarPhoenixWeb.ChatChannel do
  use BelajarPhoenixWeb, :channel

  @impl true
  def join("chat:" <> room_name, _params, socket) do
    send(self(), {:after_join, room_name})
    {:ok, %{room: room_name}, assign(socket, room: room_name)}
  end

  @impl true
  def handle_info({:after_join, room_name}, socket) do
    broadcast!(socket, "user:joined", %{
      user: socket.assigns.user_name,
      room: room_name,
      timestamp: DateTime.utc_now()
    })
    {:noreply, socket}
  end

  @impl true
  def handle_in("new_message", %{"body" => body}, socket) do
    message = %{
      id: System.unique_integer([:positive]),
      user: socket.assigns.user_name,
      body: body,
      room: socket.assigns.room,
      timestamp: DateTime.utc_now()
    }

    broadcast!(socket, "new_message", message)
    {:reply, :ok, socket}
  end

  @impl true
  def handle_in("typing", _params, socket) do
    broadcast_from!(socket, "user:typing", %{
      user: socket.assigns.user_name
    })
    {:noreply, socket}
  end

  @impl true
  def terminate(_reason, socket) do
    broadcast!(socket, "user:left", %{
      user: socket.assigns.user_name,
      room: socket.assigns.room
    })
    :ok
  end
end

10. PubSub dan Distributed Systems

Phoenix PubSub memungkinkan komunikasi antar process dan antar node dalam cluster.

lib/belajar_phoenix/notification_service.ex
defmodule BelajarPhoenix.NotificationService do
  use GenServer

  # Client API
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def subscribe(topic) do
    Phoenix.PubSub.subscribe(BelajarPhoenix.PubSub, topic)
  end

  def broadcast(topic, message) do
    Phoenix.PubSub.broadcast(BelajarPhoenix.PubSub, topic, {:notification, message})
  end

  # Server Callbacks
  @impl true
  def init(_opts) do
    Phoenix.PubSub.subscribe(BelajarPhoenix.PubSub, "notifications")
    {:ok, %{notifications: []}}
  end

  @impl true
  def handle_info({:notification, message}, state) do
    IO.puts("[Notification] #{message}")
    new_notifications = [message | state.notifications]
    {:noreply, %{state | notifications: new_notifications}}
  end
end

# Usage:
# NotificationService.subscribe("user:123")
# NotificationService.broadcast("user:123", "Anda punya pesan baru!")

11. ETS (Erlang Term Storage) dan Caching

ETS adalah in-memory storage yang sangat cepat, tersedia karena Elixir berjalan di BEAM VM.

lib/cache.ex
defmodule BelajarPhoenix.Cache do
  use GenServer

  @table :app_cache

  # Client API
  def start_link(_opts) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def get(key) do
    case :ets.lookup(@table, key) do
      [{^key, value, expiry}] ->
        if System.system_time(:millisecond) < expiry do
          {:ok, value}
        else
          :ets.delete(@table, key)
          :miss
        end
      [] ->
        :miss
    end
  end

  def put(key, value, ttl_seconds \\ 3600) do
    expiry = System.system_time(:millisecond) + (ttl_seconds * 1000)
    :ets.insert(@table, {key, value, expiry})
    :ok
  end

  def delete(key) do
    :ets.delete(@table, key)
    :ok
  end

  def flush do
    :ets.delete_all_objects(@table)
    :ok
  end

  # Server Callbacks
  @impl true
  def init(_) do
    table = :ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
    schedule_cleanup()
    {:ok, %{table: table}}
  end

  @impl true
  def handle_info(:cleanup, state) do
    now = System.system_time(:millisecond)
    :ets.select_delete(@table, [{{:_, :_, :"$1"}, [{:<, :"$1", now}], [true]}])
    schedule_cleanup()
    {:noreply, state}
  end

  defp schedule_cleanup do
    Process.send_after(self(), :cleanup, 60_000) # Cleanup setiap 1 menit
  end
end

# Usage:
# Cache.put("user:123", %{name: "Andi"}, 3600)
# Cache.get("user:123")  => {:ok, %{name: "Andi"}}

🧠 Kuis: Elixir dan Phoenix

Uji pemahaman Anda tentang Elixir dan Phoenix:

1. Apa fungsi utama pipe operator |> di Elixir?

  • Meneruskan hasil ekspresi ke function berikutnya
  • Membuat subprocess baru
  • Mengkompilasi kode secara parallel
  • Membuat koneksi ke database

2. Apa itu GenServer dalam Elixir/OTP?

  • Database server built-in
  • Generic server process untuk mengelola state
  • Web server untuk Phoenix
  • File system manager

3. Apa keunggulan utama Phoenix LiveView?

  • Rendering di sisi client
  • Menggunakan WebSocket secara manual
  • Real-time UI tanpa menulis JavaScript
  • Hanya bisa digunakan untuk static pages

4. Apa yang dilakukan oleh pin operator (^) di Elixir?

  • Membuat pointer ke variabel
  • Match nilai existing variable, bukan rebinding
  • Menghapus variabel dari memory
  • Mengcopy nilai variabel

5. Apa itu ETS di Elixir?

  • External Testing System
  • Elixir Type System
  • Error Tracking Service
  • In-memory key-value storage dari Erlang

πŸ“š Sumber Belajar Lanjutan