Repository: phoenixframework/phoenix
Pull request #3664 (2020-02-01 23:42:50)

Add option to not define default layout and view
This can be useful for applications that want to control the defaults on their own, for example I use different naming scheme where instead of `MyAppWeb.FooController` each controllers is named as `MyAppWeb.Controllers.Foo` and the views are named `MyApp.Views.Foo`, which is suits more into overall naming scheme after file path.

josevalim: Sorry but I don't understand why we need this change. If you don't use the defaults, you have to set them yourself, so can't you just set them? Phoenix is not going to get in the way... this change does not seem to be necessary.
hauleth: @josevalim the problem is that I do not have general solution to set my own default without overriding anything set earlier in the upstream. So in other words I cannot do:

```elixir
defmodule MyAppWeb.Controllers.Foo do
  use Phoenix.Controller

  plug :put_new_layout, {MyAppWeb.View.Layout, :app}
  plug :put_new_view, MyAppWeb.View.Foo
  # …
end
```

This is just example, because in my application I am using my own template, so it is more like:

```elixir
defmodule MyAppWeb.Controller do
  defmacro __using__(opts \\ []) do
    layout_mod = layout_mod(__CALLER__.module)
    view_mod = view_mod(__CALLER__.module)
    quote do
      use Phoenix.Controller

      plug :put_new_layout, {unquote(layout_mod), :app}
      plug :put_new_view, unquote(view_mod)
    end
  end

  # …
end
```

And then in my controller it is just:

```elixir
defmodule MyAppWeb.Controllers.Foo do
  use MyAppWeb.Controller

  # …
end
```

Current workaround is to use non-public `Phoenix.Controller.Pipeline` instead of `Phoenix.Controller`, but this has obvious the problem of using non-public API:

```elixir
defmodule MyAppWeb.Controller do
  defmacro __using__(opts) do
    view = view_module(__CALLER__.module)

    quote bind_quoted: [view: view, opts: opts] do
      use Phoenix.Controller.Pipeline, opts

      import Phoenix.Controller
      import Plug.Conn

      plug :put_new_layout, {MyAppWeb.Views.Layout, :app}
      plug :put_new_view, view
    end
  end

  defp view_module(module) do
    module
    |> Module.split()
    |> replace()
  end

  defp replace(parts, ret \\ [])

  defp replace([], ret) do
    raise(
      ArgumentError,
      "Expected controller, got: #{inspect(Module.concat(Enum.reverse(ret)))}"
    )
  end

  defp replace(["Controllers" | rest], ret),
    do: Module.concat(Enum.reverse(ret, [Views | rest]))

  defp replace([hd | tl], ret), do: replace(tl, [hd | ret])
end
```

This is (almost) exact copy from the application that I am working on, so you see that I use a little bit different naming scheme from the "official" Phoenix naming scheme (I just find it much more straightforward and consistent with the rest of modules).

Without that change (or making `Phoenix.Controller.Pipeline` public) there is no "safe" way to define custom default view and layout in the same way the Phoenix does it.
josevalim: Perfect. Can you please call the option `put_default_views`? Then we can merge it. Thanks!

Patch:
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index c7f59545a..2d020706c 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -41,6 +41,9 @@ defmodule Phoenix.Controller do
     * `:log` - the level to log. When false, disables controller
       logging
 
+    * `:put_default_views` - controls whether the default view
+      and layout should be set or not
+
   ## Connection
 
   A controller by default provides many convenience functions for
@@ -167,8 +170,10 @@ defmodule Phoenix.Controller do
 
       use Phoenix.Controller.Pipeline, opts
 
-      plug :put_new_layout, {Phoenix.Controller.__layout__(__MODULE__, opts), :app}
-      plug :put_new_view, Phoenix.Controller.__view__(__MODULE__)
+      if opts[:put_default_views] do
+        plug :put_new_layout, {Phoenix.Controller.__layout__(__MODULE__, opts), :app}
+        plug :put_new_view, Phoenix.Controller.__view__(__MODULE__)
+      end
     end
   end
 


Test patch:
diff --git a/test/phoenix/controller/pipeline_test.exs b/test/phoenix/controller/pipeline_test.exs
index 9807fcb3d..206978e53 100644
--- a/test/phoenix/controller/pipeline_test.exs
+++ b/test/phoenix/controller/pipeline_test.exs
@@ -44,6 +44,12 @@ defmodule Phoenix.Controller.PipelineTest do
     end
   end
 
+  defmodule NoViewsController do
+    use Phoenix.Controller, set_views: false
+
+    def show(conn, _), do: conn
+  end
+
   defmodule FallbackFunctionController do
     use Phoenix.Controller
 
@@ -91,6 +97,7 @@ defmodule Phoenix.Controller.PipelineTest do
 
     defp put_assign(conn, _), do: assign(conn, :value_before_action, :a_value)
   end
+
   def init(opts), do: opts
   def call(conn, :not_a_conn), do: Plug.Conn.send_resp(conn, 200, "fallback")
   def call(_conn, :bad_fallback), do: :bad_fallback
@@ -127,6 +134,15 @@ defmodule Phoenix.Controller.PipelineTest do
     assert layout(conn) == false
   end
 
+  test "does not set default view/layout" do
+    conn = stack_conn()
+           |> NoViewsController.call(:show)
+           |> put_new_view(Hello)
+
+    assert view_module(conn) == Hello
+    assert layout(conn) == false
+  end
+
   test "transforms top-level function clause errors into Phoenix.ActionClauseError" do
     assert_raise Phoenix.ActionClauseError, fn ->
       MyController.call(stack_conn(), :no_match)