A universal way to detect environment in Phoenix?

Values can affect behaviour though and that’s what people are trying to suggest here.

Say you have a UI where you show a secret key. In dev you want to show the whole secret, while in prod (and other envs) only the last few characters are supposed to be shown. This is different behaviour.

defmodule MyUI do
  defp format_secret(<<_start::binary-size(12), rest::binary-size(4)>> = secret) do
    case Mix.env() do
      :dev -> secret
      _ -> "…" <> rest
    end
  end
end

Instead of depending on the environment to select behaviour directly you can bundle up the behaviour (e.g. in functions or modules) and let the environment select the behaviour via configuration.

Step 1: Bundle up behaviour

defmodule SecretFormatter do
  @callback format(binary) :: binary
end

defmodule SecretFormatter.SecretFull do
  @behaviour SecretFormatter

  @impl true
  def format(secret), do: secret
end

defmodule SecretFormatter.SecretTail do
  @behaviour SecretFormatter

  @impl true
  def format(<<_start::binary-size(12), rest::binary-size(4)>>), do: "…" <> rest
end

defmodule MyUI do
  defp format_secret(secret) do
    case Mix.env() do
      :dev -> SecretFormatter.SecretFull.format(secret)
      _ -> SecretFormatter.SecretTail.format(secret)
    end
  end
end

Step 2: Use config to select the behaviour

# config.exs
config :my_app, MyUI, secret_formatter: SecretFormatter.SecretTail

# dev.exs
config :my_app, MyUI, secret_formatter: SecretFormatter.SecretFull
defmodule MyUI do
  defp format_secret(<<_start::binary-size(12), rest::binary-size(4)>> = secret) do
    formatter = Application.fetch_env!(:my_app, __MODULE__) |> Keyword.fetch!(:secret_formatter)
    formatter.format(secret)
end

This is quite a bit longer as you can see, but this is also the most elaborate setup with gives you a lot of compiler help, the ability to mock things using Mox in tests and such. You could also just name the two different ways to format things and configure the name for each.

# config.exs
config :my_app, MyUI, secret_format: :tail

# dev.exs
config :my_app, MyUI, secret_format: :full
defmodule MyUI do
  defp format_secret(secret) do
    case Application.fetch_env!(:my_app, __MODULE__) |> Keyword.fetch!(:secret_format) do
      :full -> secret
      :tail -> "…" <> rest
    end
  end
end

The important bit is that you put the different code paths (currently per env) into something independently callable and make the decision which of those to call a configuration concern instead of compile time or runtime decisions in the codebase itself.

The second example might also show that Mix.env is also just a value you use to make decisions. It’s just way less flexible and less visible than using configuration. Configuration allows you to map env => behaviour in one common place instead of scattered all over the codebase.

10 Likes