Phoenix Web Development
上QQ阅读APP看书,第一时间看更新

Understanding the controller's structure

We have a lot of work that we need to do to be able to start putting this all together and it would be very easy to get lost in the weeds, so we're going to focus down a little bit on the work that we need to do and start very small. In fact, we're not even going to build out our database schema or anything yet; instead, we'll prototype with simple data structures and, over time, build on top of those to turn them into something more usable and production-ready! We'll start by creating a brand new file in our project.

If you take a look at the /lib/vocial_web/ controllers you'll see that we start off with a page_controller.ex file first. This is the default controller that gets created for every Phoenix project created. While there is not anything significantly important or helpful in this file, it is a good basis to use to learn a controller's structure and requirements, so let's open it up and take a look:

defmodule VocialWeb.PageController do
use VocialWeb, :controller

def index(conn, _params) do
render conn, "index.html"
end
end

Remember that at the end of the day, Phoenix applications are still just Elixir applications and subject to a lot of the same design patterns and rules that govern any other Elixir application. Following in that same pattern, we see that our controllers start off with a module declaration. In this case, we see that we're namespacing our PageController module inside VocialWeb.

Remember that when creating a new Phoenix application in v1.3, we essentially have two separate applications that work together to service requests; [ApplicationName] and [ApplicationNameWeb]. ApplicationName in our case would be Vocial and thus our web side of the application would be VocialWeb! Based on that, we can see that what we're really doing here is declaring a module called PageController that lives under our VocialWeb namespace.

Next, we see our use statement. This is a Phoenix-specific macro that provides a bit of convenience to avoid us having to type in the bunch of extra imports and aliases that we need for pretty much every controller we'll write. It's important to understand exactly what this statement does, though, so we're going to look for the declaration of our VocialWeb module, which we can find at /lib/vocial_web.ex. This will also be our first little introduction to Elixir metaprogramming, which, while we won't be spending a huge amount of time learning about it, will still be worth stepping through, as it will help us understand the general flow and structure of our Phoenix application. So, if we open up /lib/vocial_web.ex, we'll see our defmodule VocialWeb do statement at the top. Next, if we look for the controller function, we'll see the following block of code:

 def controller do
quote do
use Phoenix.Controller, namespace: VocialWeb
import Plug.Conn
import VocialWeb.Router.Helpers
import VocialWeb.Gettext
end
end

So, we see that any time we type in use VocialWeb, : controller we'll get whatever the Phoenix.Controller macro provides (which is quite a bit of functionality), as well as imports for Plug.Conn (which is what helps our controller interact with plugs and connections), VocialWeb.Router.Helpers (which gives us the code to be able to reference URLs and paths for anything defined in our routers through specially-named helpers), and VocialWeb.Gettext, which provides out-of-the-box support for internationalization. But how do we get from that, using the VocialWeb: controller statement, to the code you seen before?

If you scroll to the very bottom of the file, you'll see the following chunk of code:

@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end

So, we define a macro as part of the VocialWeb module, this one being a special internal macro called __using__ that takes in a single atom as its only argument. That macro then invokes the apply function, which takes in a module as its first argument, the atom argument, and then passes in a blank array (always). Also, note the guard clause on that function, so this will only be invoked when passing in an atom; anything else will result in an error. (Specifically, an error message about no function clause matching in VocialWeb.__using__/1. If you want to try this experiment yourself, open up page_controller.ex and change the use statement at the top's argument from :controller to controller and then reload your browser window. You'll see the error message right up at the top.) apply is a special Elixir built-in function that takes in a module and an atom that corresponds with the function name it will attempt to evaluate. The final argument is a list of arguments to pass along to the function that gets evaluated. We also see that the module that is referenced is __MODULE__, which is just a fancy Elixir way of saying the current module that I'm in. Based on that, we can see that what happens is when we say use VocialWeb, :controller, the macro evaluates that as "call the function controller in the current module with no arguments passed to it." This then calls our controller function (which as we can see takes no arguments) in the VocialWeb module, and through the quote function, evaluates the code inside that function and injects it into our controller. To verify this, you could replace that use VocialWeb, :controller line with the following lines:

 use Phoenix.Controller, namespace: VocialWeb
import Plug.Conn
import VocialWeb.Router.Helpers
import VocialWeb.Gettext

Reload your browser! You'll notice that instead of getting an error message like the last experiment, this time we see the exact same thing as if the use statement were still at the top of our controller!

Next, in our PageController, we see a function called index that takes a conn argument as the first argument and an underlined params as the second argument. Remember that in Elixir, we denote any arguments in functions where we don't actually care what the value is, just that the arity matches with an underline. So what we're saying here is, "I care about the conn argument, but not the params argument" (which makes sense in this context; we're not watching for any special query parameters or anything, so we’ll throw those right out). We always have to care about the conn argument, however, since our rule is that to send back something to the browser, we have to accept and return out a connection structure.

Finally, our function makes a final call to render, passing in the connection and passing in a name of a template to render out. By default, it is assumed that the view that the controller will be calling out to has a similar name.

Side note: In your IEx terminal session, you can actually type in the following to access some documentation about this function. This function specifically lives in Phoenix.Controller, so we can learn more about the function by typing in h Phoenix.Controller.render in our IEx terminal!

So in our case, our controller is named PageController so the assumption is that our view is PageView. If we open up lib/vocial_web/views/page_view.ex, we should see the following:

defmodule VocialWeb.PageView do
use VocialWeb, :view
end

Now, you may be concerned that there is nothing here; certainly nothing corresponding to our index.html argument for our render function. This is one of the few implicit vs. explicit things in Phoenix; any calls to *.html will instead look for that matching template in the appropriate template directory and do not require any special functions inside of our view. This means our controller makes the render call for index.html, which passes through the view as it has nothing special to do. Whatever is passed in, and the connection, are passed to lib/vocial_web/templates/page/index.html.eex (/templates/(controller name)/(file.html).eex generally being the accepted patterns for these). If you open up that file you’ll see the same HTML that is getting displayed when you open up the root route / in your browser!