Skip to content

Commit

Permalink
MOAR guides
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Feb 14, 2020
1 parent 50b4e75 commit 4f3a5fb
Show file tree
Hide file tree
Showing 13 changed files with 488 additions and 1,128 deletions.
8 changes: 4 additions & 4 deletions guides/contexts.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ We rewrote the `list_users/0` and `get_user!/1` to preload the credential associ
Next, let's expose our new feature to the web by adding the credentials input to our user form. Open up `lib/hello_web/templates/user/form.html.eex` and key in the new credential form group above the submit button:


```eex
```html
...
+ <div class="form-group">
+ <%= inputs_for f, :credential, fn cf -> %>
Expand All @@ -380,7 +380,7 @@ We used `Phoenix.HTML`'s `inputs_for` function to add an associations nested fie

Next, let's show the user's email address in the user show template. Add the following code to `lib/hello_web/templates/user/show.html.eex`:

```eex
```html
...
+ <li>
+ <strong>Email:</strong>
Expand Down Expand Up @@ -535,7 +535,7 @@ end

Next, add a new template in `lib/hello_web/templates/session/new.html.eex`:

```eex
```html
<h1>Sign in</h1>

<%= form_for @conn, Routes.session_path(@conn, :create), [method: :post, as: :user], fn f -> %>
Expand Down Expand Up @@ -618,7 +618,7 @@ Remember to update your repository by running migrations:

The `views` attribute on the pages will not be updated directly by the user, so let's remove it from the generated form. Open `lib/hello_web/templates/cms/page/form.html.eex` and remove this part:

```eex
```html
- <%= label f, :views %>
- <%= number_input f, :views %>
- <%= error_tag f, :views %>
Expand Down
451 changes: 139 additions & 312 deletions guides/controllers.md

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion guides/ecto.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Ecto has out of the box support for the following databases:

Newly generated Phoenix projects include Ecto with the PostgreSQL adapter by default (you can pass the `--no-ecto` flag to exclude this).

For a thorough, general guide for Ecto, check out the [Ecto getting started guide](https://hexdocs.pm/ecto/getting-started.html). For an overview of all Ecto specific mix tasks for Phoenix, see the [mix tasks guide](phoenix_mix_tasks.html#ecto-specific-mix-tasks).
For a thorough, general guide for Ecto, check out the [Ecto getting started guide](https://hexdocs.pm/ecto/getting-started.html). For an overview of all Ecto specific mix tasks for Phoenix, see the [mix tasks guide](mix_tasks.html#ecto-specific-mix-tasks).

This guide assumes that we have generated our new application with Ecto integration and that we will be using PostgreSQL. For instructions on switching to MySQL, please see the [Using MySQL](#using-mysql) section.

Expand Down Expand Up @@ -514,3 +514,9 @@ $ mix ecto.migrate
[info] create table users
[info] == Migrated in 0.2s
```

## Other options

While Phoenix uses [the Ecto project](https://hexdocs.pm/ecto) to interact with the data access layer, there are many other data access options, some built into the Erlang standard library. [ETS](http://www.erlang.org/doc/man/ets.html) and [DETS](http://www.erlang.org/doc/man/dets.html) are key value data stores built into [OTP](http://www.erlang.org/doc/). OTP also provides a relational database called [mnesia](http://www.erlang.org/doc/man/mnesia.html) with its own query language called QLC. Both Elixir and Erlang also have a number of libraries for working with a wide range of popular data stores.

The data world is your oyster, but we won't be covering these options in these guides.
144 changes: 101 additions & 43 deletions guides/howto/custom_error_pages.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
# Custom Error Pages

Phoenix provides an `ErrorView`, `lib/hello_web/views/error_view.ex`, to render errors in our applications. The full module name will include the name of our application, as in `Hello.ErrorView`.
Phoenix has a view called the `ErrorView` which lives in `lib/hello_web/views/error_view.ex`. The purpose of the `ErrorView` is to handle errors in a general way, from one centralized location.

Phoenix will detect any 400 or 500 status level errors in our application and use the `render/2` function in our `ErrorView` to render an appropriate error template. Any errors which don't match an existing clause of `render/2` will be caught by `template_not_found/2`.
## The ErrorView

We can also customize the implementation of any of these functions however we like.

Here's what the `ErrorView` looks like.
For new applications, the ErrorView looks like this:

```elixir
defmodule Hello.ErrorView do
use Hello.Web, :view
defmodule HelloWeb.ErrorView do
use HelloWeb, :view

# If you want to customize a particular status code
# for a certain format, you may uncomment below.
Expand All @@ -27,76 +25,136 @@ defmodule Hello.ErrorView do
end
```

> NOTE: In the development environment, this behavior will be overridden. Instead, we will get a really great debugging page. In order to see the `ErrorView` in action, we'll need to set `debug_errors:` to `false` in `config/dev.exs`. The server must be restarted for the changes to become effective.
Before we dive into this, let's see what the rendered `404 not found` message looks like in a browser. In the development environment, Phoenix will debug errors by default, showing us a very informative debugging page. What we want here, however, is to see what page the application would serve in production. In order to do that we need to set `debug_errors: false` in `config/dev.exs`.

```elixir
use Mix.Config

config :hello, HelloWeb.Endpoint,
http: [port: 4000],
debug_errors: false,
code_reloader: true,
cache_static_lookup: false,
watchers: [node: ["node_modules/webpack/bin/webpack.js", "--mode", "development", "--watch-stdin",
cd: Path.expand("../assets", __DIR__)]]
. . .
```

To learn more about custom error pages, please see [The Error View](views.html#the-errorview) section of the View Guide.
After modifying our config file, we need to restart our server in order for this change to take effect. After restarting the server, let's go to [http://localhost:4000/such/a/wrong/path](http://localhost:4000/such/a/wrong/path) for a running local application and see what we get.

#### Custom Errors
Ok, that's not very exciting. We get the bare string "Not Found", displayed without any markup or styling.

Elixir provides a macro called `defexception` for defining custom exceptions. Exceptions are represented as structs, and structs need to be defined inside of modules.
The first question is, where does that error string come from? The answer is right in the `ErrorView`.

In order to create a custom error, we need to define a new module. Conventionally this will have "Error" in the name. Inside of that module, we need to define a new exception with `defexception`.
```elixir
def template_not_found(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
```

We can also define a module within a module to provide a namespace for the inner module.
Great, so we have this `template_not_found/2` function that takes a template and an `assigns` map, which we ignore. The `template_not_found/2` is invoked whenever a Phoenix.View attempts to render a template bu tno template is found.

Here's an example from the [Phoenix.Router](https://github.com/phoenixframework/phoenix/blob/master/lib/phoenix/router.ex) which demonstrates all of these ideas.
In order words, to provide custom error pages, we could simply define a the proper `render/2` function clause in `HelloWeb.ErrorView`.

```elixir
defmodule Phoenix.Router do
defmodule NoRouteError do
@moduledoc """
Exception raised when no route is found.
"""
defexception plug_status: 404, message: "no route found", conn: nil, router: nil

def exception(opts) do
conn = Keyword.fetch!(opts, :conn)
router = Keyword.fetch!(opts, :router)
path = "/" <> Enum.join(conn.path_info, "/")

%NoRouteError{message: "no route found for #{conn.method} #{path} (#{inspect router})",
conn: conn, router: router}
end
end
. . .
def render("404.html", _assigns) do
"Page Not Found"
end
```

Plug provides a protocol called `Plug.Exception` where we are able to customize the status and add actions that exception structs can returns on the debug error page.
But we can do even better.

Phoenix generates an `ErrorView` for us, but it doesn't give us a `lib/hello_web/templates/error` directory. Let's create one now. Inside our new directory, let's add a template, `404.html.eex` and give it some markup - a mixture of our application layout and a new `div` with our message to the user.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">

<title>Welcome to Phoenix!</title>
<link rel="stylesheet" href="/css/app.css">
</head>

<body>
<div class="container">
<div class="header">
<ul class="nav nav-pills pull-right">
<li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
</ul>
<span class="logo"></span>
</div>

<div class="phx-hero">
<p>Sorry, the page you are looking for does not exist.</p>
</div>

<div class="footer">
<p><a href="http://phoenixframework.org">phoenixframework.org</a></p>
</div>

</div> <!-- /container -->
<script src="/js/app.js"></script>
</body>
</html>
```

If we wanted to supply a status of 404 for an `Ecto.NoResultsError`, we could do it by defining an implementation for the `Plug.Exception` protocol like this:
Now when we go back to [http://localhost:4000/such/a/wrong/path](http://localhost:4000/such/a/wrong/path), we should see a much nicer error page. It is worth noting that we did not render our `404.html.eex` template through our application layout, even though we want our error page to have the look and feel of the rest of our site. This is to avoid circular errors. For example, what happens if our application failed due to an error in the layout? Attempting to render the layout again will just trigger another error. So ideally we want to minimize the amount of dependencies and logic in our error templates, sharing only what is necessary.

## Custom Exceptions

Elixir provides a macro called `defexception` for defining custom exceptions. Exceptions are represented as structs, and structs need to be defined inside of modules.

In order to create a custom exception, we need to define a new module. Conventionally this will have "Error" in the name. Inside of that module, we need to define a new exception with `defexception`.

```elixir
defimpl Plug.Exception, for: Ecto.NoResultsError do
defmodule MyApp.SomethingNotFoundError do
defexception [:message]
end
```

You can raise your new exception like this:

```elixir
raise MyApp.SomethingNotFoundError, "oops"
```

By default, Plug and Phoenix will treat all exceptions as 500 errors. However, Plug provides a protocol called `Plug.Exception` where we are able to customize the status and add actions that exception structs can returns on the debug error page.

If we wanted to supply a status of 404 for an `MyApp.SomethingNotFoundError`, we could do it by defining an implementation for the `Plug.Exception` protocol like this:

```elixir
defimpl Plug.Exception, for: MyApp.SomethingNotFoundError do
def status(_exception), do: 404
def actions(_exception), do: []
end
```

Note that this is just an example: Phoenix [already does this](https://github.com/phoenixframework/phoenix_ecto/blob/master/lib/phoenix_ecto/plug.ex) for `Ecto.NoResultsError`, so you don't have to.
Alternatively, you could define a `plug_status` field directly in the exception struct:

#### Actions
```elixir
defmodule MyApp.SomethingNotFoundError do
defexception [:message, plug_status: 404]
end
```

However, implementing the `Plug.Exception` protocol by hand can be convenient in certain occasions, such as when providing Actionable ERrors.

## Actionable Errors

Exception actions are functions that can be triggered by the error page, it is basically a list of maps defining a `label` and a `handler` to be executed.

Actions are functions that can be triggered by the error page, it is basically a list of maps defining a `label` and a `handler` to be executed.
It is rendered in the error page as a collection of buttons and follows the format of: `[%{label: String.t(), handler: {module(), function :: atom(), args :: []}}]`.

If we wanted to return some actions for an `Ecto.NoResultsError` we would implement `Plug.Exception` like this:
If we wanted to return some actions for an `MyApp.SomethingNotFoundError` we would implement `Plug.Exception` like this:

```elixir
defimpl Plug.Exception, for: Ecto.NoResultsError do
defimpl Plug.Exception, for: MyApp.SomethingNotFoundError do
def status(_exception), do: 404
def actions(_exception), do: [%{
label: "Run seeds",
handler: {Code, :eval_file, "priv/repo/seeds.exs"}
}]
end
```
```
4 changes: 2 additions & 2 deletions guides/introduction/up_and_running.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ $ mix phx.new hello
> A note about [Ecto](ecto.html): Ecto allows our Phoenix application to communicate with a data store, such as PostgreSQL, MySQL, and others. If our application will not require this component we can skip this dependency by passing the `--no-ecto` flag to `mix phx.new`. This flag may also be combined with `--no-webpack` to create a skeleton application.
> To learn more about `mix phx.new` you can read the [Mix Tasks Guide](phoenix_mix_tasks.html#phoenix-specific-mix-tasks).
> To learn more about `mix phx.new` you can read the [Mix Tasks Guide](mix_tasks.html#phoenix-specific-mix-tasks).
```console
mix phx.new hello
Expand Down Expand Up @@ -59,7 +59,7 @@ You can also run your app inside IEx (Interactive Elixir) as:

Once our dependencies are installed, the task will prompt us to change into our project directory and start our application.

Phoenix assumes that our PostgreSQL database will have a `postgres` user account with the correct permissions and a password of "postgres". If that isn't the case, please see the [Mix Tasks Guide](phoenix_mix_tasks.html#ecto-specific-mix-tasks) to learn more about the `mix ecto.create` task.
Phoenix assumes that our PostgreSQL database will have a `postgres` user account with the correct permissions and a password of "postgres". If that isn't the case, please see the [Mix Tasks Guide](mix_tasks.html#ecto-specific-mix-tasks) to learn more about the `mix ecto.create` task.

Ok, let's give it a try. First, we'll `cd` into the `hello/` directory we've just created:

Expand Down
62 changes: 52 additions & 10 deletions guides/plug.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ We are able to add this module plug to our browser pipeline via `plug HelloWeb.P

To see the assign in action, go to the layout in "lib/hello_web/templates/layout/app.html.eex" and add the following close to the main container:

```eex
```html
<main role="main" class="container">
<p>Locale: <%= @locale %></p>
```
Expand All @@ -124,7 +124,11 @@ That's all there is to Plug. Phoenix embraces the plug design of composable tran

## Where to plug

The endpoint, router, and controllers in Phoenix accept plugs declarations. In the endpoint, we did:
The endpoint, router, and controllers in Phoenix accept plugs.

### Endpoint plugs

Endpoints organize all the plugs common to every request, and apply them before dispatching into the router with its custom pipelines. We added a plug to the endpoint like this:

```elixir
defmodule HelloWeb.Endpoint do
Expand All @@ -134,6 +138,39 @@ defmodule HelloWeb.Endpoint do
plug HelloWeb.Router
```

The default Endpoint plugs do quite a lot of work. Here they are in order:

- [Plug.Static](https://hexdocs.pm/plug/Plug.Static.html) - serves static assets. Since this plug comes before the logger, serving of static assets is not logged

- [Phoenix.CodeReloader](https://hexdocs.pm/phoenix/Phoenix.CodeReloader.html) - a plug that enables code reloading for all entries in the web directory. It is configured directly in the Phoenix application

- [Plug.RequestId](https://hexdocs.pm/plug/Plug.RequestId.html) - generates a unique request id for each request.

- [Plug.Telemetry](https://hexdocs.pm/plug/Plug.Telemetry.html) - adds instrumentation points so Phoenix can log the request path, status code and request time by default.

- [Plug.Parsers](https://hexdocs.pm/plug/Plug.Parsers.html) - parses the request body when a known parser is available. By default parsers parse urlencoded, multipart and json (with `jason`). The request body is left untouched when the request content-type cannot be parsed

- [Plug.MethodOverride](https://hexdocs.pm/plug/Plug.MethodOverride.html) - converts the request method to PUT, PATCH or DELETE for POST requests with a valid `_method` parameter

- [Plug.Head](https://hexdocs.pm/plug/Plug.Head.html) - converts HEAD requests to GET requests and strips the response body

- [Plug.Session](https://hexdocs.pm/plug/Plug.Session.html) - a plug that sets up session management. Note that `fetch_session/2` must still be explicitly called before using the session as this Plug just sets up how the session is fetched

In the middle of the endpoint, there is also a conditional block:

```elixir
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :demo
end
```

This block is only executed in development. It enables live reloading (if you change a CSS file, they are updated in-browser without refreshing the page), code reloading (so we can see changes to our application without restarting the server), and check repo status (which makes sure our database is up to date, raising readable and actionable error otherwise).

### Router plugs

In the router, we can declare plugs insided pipelines:

```elixir
Expand All @@ -148,17 +185,20 @@ defmodule HelloWeb.Router do
plug :put_secure_browser_headers
plug HelloWeb.Plugs.Locale, "en"
end
```

As we will see in [the Routing guide](routing.html), the pipelines themselves are plugs, which means we can do something like:
scope "/", HelloWeb do
pipe_through :browser

```elixir
pipeline :enhanced_browser do
plug :browser
plug :something_else
end
get "/", PageController, :index
end
```

Routes are defined inside scopes and scopes may pipe through multiple pipelines. Once a route matches, Phoenix invokes all plugs defined in all pipelines associated to that route. For example, accessing "/" will pipe through the `:browser` pipeline, consequently invoking all of its plugs.

As we will see in [the Routing guide](routing.html), the pipelines themselves are plugs. There we will also discuss all plugs in the `:browser` pipeline.

### Controller plugs

Finally, controllers are plugs too, so we can do:

```elixir
Expand All @@ -168,7 +208,7 @@ defmodule HelloWeb.HelloController do
plug HelloWeb.Plugs.Locale, "en"
```

In particular, controller plugs provide an extension that allows us to execute plugs only within certain actions. You can do:
In particular, controller plugs provide a feature that allows us to execute plugs only within certain actions. For example, you can do:

```elixir
defmodule HelloWeb.HelloController do
Expand Down Expand Up @@ -253,3 +293,5 @@ end
To make this all work, we converted the nested blocks of code and used `halt(conn)` whenever we reached a failure path. The `halt(conn)` functionality is essential here: it tells Plug that the next plug should not invoked.

At the end of the day, by replacing the nested blocks of code with a flattened series of plug transformations, we are able to achieve the same functionality in a much more composable, clear, and reusable way.

To learn more about Plugs, see the documentation for [the Plug project](https://hexdocs.pm/plug), which provides many built-in plugs and functionalities.
Loading

0 comments on commit 4f3a5fb

Please sign in to comment.