Phoenix.Channel

Parsed documentation:
View on GitHub
Defines a Phoenix Channel.

Channels provide a means for bidirectional communication from clients that
integrate with the `Phoenix.PubSub` layer for soft-realtime functionality.

## Topics & Callbacks

Every time you join a channel, you need to choose which particular topic you
want to listen to. The topic is just an identifier, but by convention it is
often made of two parts: `"topic:subtopic"`. Using the `"topic:subtopic"`
approach pairs nicely with the `Phoenix.Socket.channel/2` allowing you to
match on all topics starting with a given prefix:

    channel "room:*", MyApp.RoomChannel

Any topic coming into the router with the `"room:"` prefix would dispatch
to `MyApp.RoomChannel` in the above example. Topics can also be pattern
matched in your channels' `join/3` callback to pluck out the scoped pattern:

    # handles the special `"lobby"` subtopic
    def join("room:lobby", _payload, socket) do
      {:ok, socket}
    end

    # handles any other subtopic as the room ID, for example `"room:12"`, `"room:34"`
    def join("room:" <> room_id, _payload, socket) do
      {:ok, socket}
    end

## Authorization

Clients must join a channel to send and receive PubSub events on that channel.
Your channels must implement a `join/3` callback that authorizes the socket
for the given topic. For example, you could check if the user is allowed to
join that particular room.

To authorize a socket in `join/3`, return `{:ok, socket}`.
To refuse authorization in `join/3`, return `{:error, reply}`.

## Incoming Events

After a client has successfully joined a channel, incoming events from the
client are routed through the channel's `handle_in/3` callbacks. Within these
callbacks, you can perform any action. Typically you'll either forward a
message to all listeners with `broadcast!/3`, or push a message directly down
the socket with `push/3`. Incoming callbacks must return the `socket` to
maintain ephemeral state.

Here's an example of receiving an incoming `"new_msg"` event from one client,
and broadcasting the message to all topic subscribers for this socket.

    def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
      broadcast!(socket, "new_msg", %{uid: uid, body: body})
      {:noreply, socket}
    end

You can also push a message directly down the socket:

    # client asks for their current rank, push sent directly as a new event.
    def handle_in("current_rank", socket) do
      push(socket, "current_rank", %{val: Game.get_rank(socket.assigns[:user])})
      {:noreply, socket}
    end

## Replies

In addition to pushing messages out when you receive a `handle_in` event,
you can also reply directly to a client event for request/response style
messaging. This is useful when a client must know the result of an operation
or to simply ack messages.

For example, imagine creating a resource and replying with the created record:

    def handle_in("create:post", attrs, socket) do
      changeset = Post.changeset(%Post{}, attrs)

      if changeset.valid? do
        post = Repo.insert!(changeset)
        response = MyApp.PostView.render("show.json", %{post: post})
        {:reply, {:ok, response}, socket}
      else
        response = MyApp.ChangesetView.render("errors.json", %{changeset: changeset})
        {:reply, {:error, response}, socket}
      end
    end

Alternatively, you may just want to ack the status of the operation:

    def handle_in("create:post", attrs, socket) do
      changeset = Post.changeset(%Post{}, attrs)

      if changeset.valid? do
        Repo.insert!(changeset)
        {:reply, :ok, socket}
      else
        {:reply, :error, socket}
      end
    end

## Intercepting Outgoing Events

When an event is broadcasted with `broadcast/3`, each channel subscriber can
choose to intercept the event and have their `handle_out/3` callback triggered.
This allows the event's payload to be customized on a socket by socket basis
to append extra information, or conditionally filter the message from being
delivered. If the event is not intercepted with `Phoenix.Channel.intercept/1`,
then the message is pushed directly to the client:

    intercept ["new_msg", "user_joined"]

    # for every socket subscribing to this topic, append an `is_editable`
    # value for client metadata.
    def handle_out("new_msg", msg, socket) do
      push(socket, "new_msg", Map.merge(msg,
        %{is_editable: User.can_edit_message?(socket.assigns[:user], msg)}
      ))
      {:noreply, socket}
    end

    # do not send broadcasted `"user_joined"` events if this socket's user
    # is ignoring the user who joined.
    def handle_out("user_joined", msg, socket) do
      unless User.ignoring?(socket.assigns[:user], msg.user_id) do
        push(socket, "user_joined", msg)
      end
      {:noreply, socket}
    end

## Broadcasting to an external topic

In some cases, you will want to broadcast messages without the context of
a `socket`. This could be for broadcasting from within your channel to an
external topic, or broadcasting from elsewhere in your application like a
controller or another process. Such can be done via your endpoint:

    # within channel
    def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
      ...
      broadcast_from!(socket, "new_msg", %{uid: uid, body: body})
      MyApp.Endpoint.broadcast_from!(self(), "room:superadmin",
        "new_msg", %{uid: uid, body: body})
      {:noreply, socket}
    end

    # within controller
    def create(conn, params) do
      ...
      MyApp.Endpoint.broadcast!("room:" <> rid, "new_msg", %{uid: uid, body: body})
      MyApp.Endpoint.broadcast!("room:superadmin", "new_msg", %{uid: uid, body: body})
      redirect(conn, to: "/")
    end

## Terminate

On termination, the channel callback `terminate/2` will be invoked with
the error reason and the socket.

If we are terminating because the client left, the reason will be
`{:shutdown, :left}`. Similarly, if we are terminating because the
client connection was closed, the reason will be `{:shutdown, :closed}`.

If any of the callbacks return a `:stop` tuple, it will also
trigger terminate with the reason given in the tuple.

`terminate/2`, however, won't be invoked in case of errors nor in
case of exits. This is the same behaviour as you find in Elixir
abstractions like `GenServer` and others. Typically speaking, if you
want to clean something up, it is better to monitor your channel
process and do the clean up from another process.  Similar to GenServer,
it would also be possible `:trap_exit` to guarantee that `terminate/2`
is invoked. This practice is not encouraged though.

## Exit reasons when stopping a channel

When the channel callbacks return a `:stop` tuple, such as:

    {:stop, :shutdown, socket}
    {:stop, {:error, :enoent}, socket}

the second argument is the exit reason, which follows the same behaviour as
standard `GenServer` exits.

You have three options to choose from when shutting down a channel:

  * `:normal` - in such cases, the exit won't be logged, there is no restart
    in transient mode, and linked processes do not exit

  * `:shutdown` or `{:shutdown, term}` - in such cases, the exit won't be
    logged, there is no restart in transient mode, and linked processes exit
    with the same reason unless they're trapping exits

  * any other term - in such cases, the exit will be logged, there are
    restarts in transient mode, and linked processes exit with the same reason
    unless they're trapping exits

## Subscribing to external topics

Sometimes you may need to programmatically subscribe a socket to external
topics in addition to the the internal `socket.topic`. For example,
imagine you have a bidding system where a remote client dynamically sets
preferences on products they want to receive bidding notifications on.
Instead of requiring a unique channel process and topic per
preference, a more efficient and simple approach would be to subscribe a
single channel to relevant notifications via your endpoint. For example:

    defmodule MyApp.Endpoint.NotificationChannel do
      use Phoenix.Channel

      def join("notification:" <> user_id, %{"ids" => ids}, socket) do
        topics = for product_id <- ids, do: "product:#{product_id}"

        {:ok, socket
              |> assign(:topics, [])
              |> put_new_topics(topics)}
      end

      def handle_in("watch", %{"product_id" => id}, socket) do
        {:reply, :ok, put_new_topics(socket, ["product:#{id}"])}
      end

      def handle_in("unwatch", %{"product_id" => id}, socket) do
        {:reply, :ok, MyApp.Endpoint.unsubscribe("product:#{id}")}
      end

      defp put_new_topics(socket, topics) do
        Enum.reduce(topics, socket, fn topic, acc ->
          topics = acc.assigns.topics
          if topic in topics do
            acc
          else
            :ok = MyApp.Endpoint.subscribe(topic)
            assign(acc, :topics, [topic | topics])
          end
        end)
      end
    end

Note: the caller must be responsible for preventing duplicate subscriptions.
After calling `subscribe/1` from your endpoint, the same flow applies to
handling regular Elixir messages within your channel. Most often, you'll
simply relay the `%Phoenix.Socket.Broadcast{}` event and payload:

    alias Phoenix.Socket.Broadcast
    def handle_info(%Broadcast{topic: _, event: event, payload: payload}, socket) do
      push(socket, event, payload)
      {:noreply, socket}
    end

## Logging

By default, channel `"join"` and `"handle_in"` events are logged, using
the level `:info` and `:debug`, respectively. Logs can be customized per
event type or disabled by setting the `:log_join` and `:log_handle_in`
options when using `Phoenix.Channel`. For example, the following
configuration logs join events as `:info`, but disables logging for
incoming events:

    use Phoenix.Channel, log_join: :info, log_handle_in: false
No suggestions.
Please help! Open an issue on GitHub if this assessment is incorrect.