└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Blog Tutorial 2 | 3 | Please note that this was written for a much older version of Phoenix. While some of the concepts may apply, it is worth using this in tandem with [the official docs](http://www.phoenixframework.org/docs/overview). 4 | 5 | ## Getting Phoenix 6 | 7 | Since the blog application is built using the [Phoenix framework](https://github.com/phoenixframework/phoenix), the first step is to generate a new Phoenix application. 8 | 9 | 1. Get phoenix by running `git clone git@github.com:phoenixframework/phoenix.git` 10 | 2. `cd phoenix` 11 | 3. Get the dependencies and compile `mix do deps.get compile` 12 | 4. Generate a new Phoenix application `mix phoenix.new blog ../blog` 13 | 14 | This will create a Phoenix application with the default skeleton. This also needs to fetch the dependencies and compile, then we can view the default Phoenix home page in the browser. 15 | 16 | 1. `cd ../blog` 17 | 2. `mix do deps.get, compile` 18 | 3. `mix phoenix.server` 19 | 4. Navigate to http://localhost:4000 20 | 21 | This is probably a good point to commit the application so that our code doesn't get mixed in with the code generated by Phoenix. 22 | 23 | 1. `git init` 24 | 2. `git add .` 25 | 3. `git commit` 26 | 27 | ## Adding a database 28 | 29 | Since this is a blog, our posts and comments will need to sit in a database somewhere, in this case a PostgreSQL database. We're going to use [Ecto](https://github.com/elixir-lang/ecto) to interact with the database. We also need to use [Postgrex](https://github.com/ericmj/postgrex) which is used by the Ecto PostgreSQL adapter. 30 | 31 | To add dependencies to an Elixir application we use have to update the `mix.exs` file with our dependencies. Since the two packages we are using exist on the [Hex package manager](https://hex.pm) we can simply specify the name of the dependency and it will be fetched from there. The new `deps` function should look like: 32 | 33 | ```elixir 34 | defp deps do 35 | [ 36 | {:phoenix, github: "phoenixframework/phoenix"}, 37 | {:cowboy, "~> 1.0"}, 38 | {:ecto, "~> 0.2.5"}, 39 | {:postgrex, "~> 0.6.0"} 40 | ] 41 | end 42 | ``` 43 | 44 | We can now run `mix do deps.get, compile` to fetch these dependencies and compile them. You will not that the `phoenix` dependency uses a keyword list as its second argument with the key `github` this tells mix to fetch the dependency from the GitHub repository `phoenixframework/phoenix` 45 | 46 | ### Starting ecto 47 | 48 | Even though Ecto and Postgrex have been imported, that doesn't mean that they are running. We need to start them. 49 | 50 | TODO: explain the concepts in http://elixir-lang.org/getting_started/mix_otp/5.html#5.2-understanding-applications 51 | 52 | In order to start Ecto and Postgrex we need to update the `application` function `mix.exs` to: 53 | 54 | ```elixir 55 | def application do 56 | [mod: {Blog, []}, 57 | applications: [:phoenix, :cowboy, :logger, :postgrex, :ecto]] 58 | end 59 | ``` 60 | 61 | ### Configuring the database 62 | 63 | Now that we have Ecto available to us, we can generate a repository - Ecto defines this as a wrapper around the database. Ecto comes with a mix task to generate a repository. The list of available mix tasks for a project can be seen by running `mix help` 64 | 65 | gazler@gazler-desktop:~/development/elixir/blog$ mix help 66 | mix # Run the default task (current: mix run) 67 | ... 68 | mix ecto.create # Create the database for the repo 69 | mix ecto.drop # Drop the database for the repo 70 | mix ecto.gen.migration # Generates a new migration for the repo 71 | mix ecto.gen.repo # Generates a new repository 72 | mix ecto.migrate # Runs migrations up on a repo 73 | mix ecto.rollback # Reverts migrations down on a repo 74 | ... 75 | iex -S mix # Start IEx and run the default task 76 | 77 | More information on an individual task can be seen by running `mix help TASKNAME` 78 | 79 | gazler@gary-desktop:~/development/elixir/blog$ mix help ecto.gen.repo 80 | 81 | Generates a new repository. 82 | The repository will be placed in the lib directory. 83 | 84 | Examples 85 | > mix ecto.gen.repo Repo 86 | 87 | Since our application is called `Blog` our repository will be called `Blog.Repo` we can run `mix ecto.gen.repo Blog.Repo` to generate this. 88 | 89 | By default, a generated repo expects the `url` function to be filled in by the user to point to the database. Since the location of the database is up to the developer, it should not be dictated by the project. Instead of defining the database url directly in this file, we will allow this to be specified in a config file. 90 | 91 | ### Testing the database repository 92 | 93 | It is important that this works reliably, so we will add some tests to ensure it works as intended. Elixir comes with its own test framework [ExUnit](http://elixir-lang.org/getting_started/mix_otp/1.html#1.3-running-tests). 94 | 95 | In order to write some tests, we need to create a file to put them in. The `mix test` task will run all the tests in the `test` directory that end in `_test.exs`. Create the file `test/blog/repo_test.exs` with the following test: 96 | 97 | ```elixir 98 | defmodule Blog.RepoTest do 99 | use ExUnit.Case 100 | 101 | test "conf uses application config if defined" do 102 | config = [ 103 | username: "user", 104 | password: "pass", 105 | hostname: "localhost", 106 | database: "testdb", 107 | port: 5342 108 | ] 109 | Application.put_env(:ecto, Blog.Repo, config) 110 | assert Blog.Repo.conf == config 111 | end 112 | end 113 | ``` 114 | 115 | This tests that the application uses the config specified by `Application.put_env`. To make this test pass, we can do replace the contents of `lib/blog/repo.ex` with: 116 | 117 | ```elixir 118 | defmodule Blog.Repo do 119 | use Ecto.Repo, adapter: Ecto.Adapters.Postgres 120 | require Logger 121 | 122 | def conf do 123 | Application.get_env(:ecto, Blog.Repo) 124 | end 125 | 126 | def priv do 127 | app_dir(:blog, "priv/repo") 128 | end 129 | end 130 | ``` 131 | 132 | ### Mix configuration 133 | 134 | You may be wondering what `Application.get_env` does. Phoenix utilizes the `Mix.Config` module to define the config for the Application. If you look inside the `config` directory then you will see a number of files. We are going to add a new file for configuring the database. We will call this file `database.exs` 135 | 136 | ```elixir 137 | use Mix.Config 138 | 139 | config :ecto, Blog.Repo, 140 | username: "user", 141 | password: "password", 142 | hostname: "localhost", 143 | port: 5432, 144 | database: "blog_development" 145 | ``` 146 | 147 | The `config` function we use here comes from [Mix.Config](http://elixir-lang.org/docs/stable/mix/Mix.Config.html#config/3) where `:ecto` is the application, `Blog.Repo` is the key and the rest is a keyword list of options. The keys match to the config that Ecto expects to be returned from the `conf` function. 148 | 149 | Even though we have added this file, we still need to include it. You will remember earlier that we said that the location of the database should be up to the developer. To ensure this is the case, we don't actually want this `database.exs` file to exist in the repository. What we will do instead is create a copy of it that we expect to developer to copy back to `database.exs` we will then remove `database.exs` from version control so that we don't expose our database credentials to the world. 150 | 151 | 1) Make a copy of database.exs `cp database.exs database.exs.example` 152 | 2) Add database.exs to .gitignore file `echo "config/database.exs" >> .gitignore` 153 | 154 | The last thing we need to do is load our new `database.exs` file into the config. Add the following to the bottom of `config/config.exs` 155 | 156 | ```elixir 157 | import_config "database.exs" 158 | ``` 159 | 160 | The database can now be created by running `mix ecto.create` 161 | 162 | ### Starting a repository 163 | 164 | In order to use an Ecto repository, it needs to be started. We want to ensure that when our web application is running, Ecto is running. To do this we add it to our supervision tree. Update `lib/blog.ex` to the following: 165 | 166 | ```elixir 167 | defmodule Blog do 168 | use Application 169 | 170 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 171 | # for more information on OTP Applications 172 | def start(_type, _args) do 173 | import Supervisor.Spec, warn: false 174 | 175 | children = [ 176 | worker(Blog.Repo, []) 177 | ] 178 | 179 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 180 | # for other strategies and supported options 181 | opts = [strategy: :one_for_one, name: Blog.Supervisor] 182 | Supervisor.start_link(children, opts) 183 | end 184 | end 185 | ``` 186 | 187 | The code changes in this commit can be seen at https://github.com/Gazler/elixir-blog/commit/feb75c387b7e44908cdb94c8ea0f6926fea59ce5 188 | 189 | ## Creating the post model 190 | 191 | When you generate a Phoenix application, a `web/models` directory is created. This is where we will put our Ecto models. An Ecto model is an Elixir representation of a database record. A database record is stored in a table. In order to store a record in a table, we first need to create a table. To ensure that all developers create have access to this table, we will create a migration for it. You can create a migration by calling `mix ecto.gen.migration Blog.Repo add_posts_table` 192 | 193 | This will create a file in the `priv/blog/migrations` directory. This is the directory that is specified in the `priv` function in our `Blog.Repo` module. The body of this function was generated when we called `mix.gen.repo` earlier. 194 | 195 | A migration has two functions defined: 196 | 197 | * `up` - migrating from an early point to a later point 198 | * `down` - migrating from a later point to an earlier one 199 | 200 | In this case, we want to create a posts table on the up migration and destroy it on the down migration. Populate the `priv/repo/migrations/TIMESTAMP_add_posts_table.exs` migration with: 201 | 202 | ```elixir 203 | defmodule Blog.Repo.Migrations.AddPostsTable do 204 | use Ecto.Migration 205 | 206 | def up do 207 | "CREATE TABLE if NOT EXISTS posts( 208 | id serial primary key, 209 | name text, 210 | created_at timestamp default CURRENT_DATE, 211 | updated_at timestamp 212 | )" 213 | end 214 | 215 | def down do 216 | "DROP TABLE posts" 217 | end 218 | end 219 | ``` 220 | 221 | We can then perform the migration by running `mix ecto.migrate Blog.Repo` this will create the table in the database. 222 | 223 | Now that we have the table, we can create the matching Ecto model. Create the file `web/models/post.ex` with the following: 224 | 225 | ```elixir 226 | defmodule Blog.Post do 227 | use Ecto.Model 228 | 229 | schema "posts" do 230 | field :name 231 | field :created_at, :datetime 232 | field :updated_at, :datetime 233 | end 234 | end 235 | ``` 236 | 237 | You will notice that the schema matches the table name "posts" and the fields that we defined in the migration. The id field is not mentioned in the schema as this is generated by Ecto by default. 238 | 239 | Now that we have the model and the table, we should be able to create a blog post. We will do this in the elixir console. Open up an elixir console using `iex -S mix` and do the following: 240 | 241 | iex(1)> post = %Blog.Post{name: "My First Post"} 242 | %Blog.Post{created_at: nil, id: nil, name: "My First Post", updated_at: nil} 243 | iex(2)> Blog.Repo.insert(post) 244 | %Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11, 245 | sec: 0, year: 2014}, id: 2, name: "My First Post", updated_at: nil} 246 | 247 | You will notice that the `updated_at` field doen't have a time in them. We can use the `Ecto.DateTime.utc` function to get the current time. To update the `updated_at` field do: 248 | 249 | iex(3)> post = Blog.Repo.get(Blog.Post, 1) 250 | %Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11, 251 | sec: 0, year: 2014}, id: 1, name: "My First Post", updated_at: nil} 252 | iex(4)> post = %{post | updated_at: Ecto.DateTime.utc} 253 | %Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11, 254 | sec: 0, year: 2014}, id: 1, name: "My First Post", 255 | updated_at: %Ecto.DateTime{day: 14, hour: 16, min: 6, month: 11, sec: 19, 256 | year: 2014}} 257 | 258 | We can validate that the created_at field has been updated by calling: 259 | 260 | iex(6)> Blog.Repo.get(Blog.Post, 1) 261 | %Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11, 262 | sec: 0, year: 2014}, id: 1, name: "My First Post", 263 | updated_at: %Ecto.DateTime{day: 14, hour: 16, min: 6, month: 11, sec: 19, 264 | year: 2014}} 265 | 266 | Having to manually set the `updated_at` field to be the current time whenever we create a new record is not ideal. We should also default this to the timestamp when the record is created. Let's modify the migration to do that. Change the up migration to: 267 | 268 | ```elixir 269 | def up do 270 | "CREATE TABLE if NOT EXISTS posts( 271 | id serial primary key, 272 | name text, 273 | created_at timestamp default CURRENT_DATE, 274 | updated_at timestamp default CURRENT_DATE 275 | )" 276 | end 277 | ``` 278 | 279 | Now run `mix ecto.rollback` to run the `down` migration. This will delete the table and the posts we created. Now run `mix ecto.migrate` again. This will run the `up` migration but now the `updated_at` field will default to the creation timestamp. We can validate this has worked in an iex session. 280 | 281 | iex(1)> Blog.Repo.insert(%Blog.Post{name: "A Blog Post"}) 282 | %Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11, 283 | sec: 0, year: 2014}, id: 1, name: "A Blog Post", 284 | updated_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11, sec: 0, 285 | year: 2014}} 286 | 287 | You may also find the following repo functions helpful: 288 | 289 | * `Blog.Repo.all(Blog.Post)` returns all of the blogs posts 290 | * `Blog.Repo.delete(%Blog.Post{id: 1})` will delete the post with id 1 291 | * `Blog.Repo.delete_all(%Blog.Post)` will delete all posts 292 | 293 | The code changes for this commit can be seen at https://github.com/Gazler/elixir-blog/commit/b7ea7f4f215bed1faf2a8923ff83b72998da3ebc 294 | 295 | ## Creating a web page for the posts 296 | 297 | Now that we have a posts model that can store posts in the database, we want a way of displaying. Create a few posts in the database inside the console so we have some test data to display. 298 | 299 | The posts will sit in our web application at the `/posts` route. In order for this to work, we need to add this route to the Phoenix router. Since we will be able to create, read, update and delete posts, we will use the `resources` function available in the `Phoenix.Router` module. 300 | 301 | Add the following to the `web/router.ex` file under the `get "/", Blog.PageController, :index` line: 302 | 303 | ```elixir 304 | resources "posts", Blog.PostsController 305 | ``` 306 | 307 | This will create several web endpoints for our application. You can see the routes by running `mix phoenix.routes` the output should look like this: 308 | 309 | page_path GET / Blog.PageController.index/2 310 | posts_path GET /posts Blog.PostsController.index/2 311 | posts_path GET /posts/:id/edit Blog.PostsController.edit/2 312 | posts_path GET /posts/new Blog.PostsController.new/2 313 | posts_path GET /posts/:id Blog.PostsController.show/2 314 | posts_path POST /posts Blog.PostsController.create/2 315 | posts_path PATCH /posts/:id Blog.PostsController.update/2 316 | PUT /posts/:id Blog.PostsController.update/2 317 | posts_path DELETE /posts/:id Blog.PostsController.destroy/2 318 | 319 | You can read more about Phoenix Routing at https://github.com/lancehalvorsen/phoenix-guides/blob/master/C_routing.md 320 | 321 | The `resources` function has created all of the `posts_path` routes for us. You can see the controller that they map to, which we passed as the second argument. Let's create this controller in `web/controllers/posts_controller.ex` with the following contents: 322 | 323 | ```elixir 324 | defmodule Blog.PostsController do 325 | use Phoenix.Controller 326 | 327 | plug :action 328 | plug :render 329 | 330 | def index(conn, _params) do 331 | conn 332 | end 333 | end 334 | ``` 335 | 336 | * For a description of `plug :action` see https://github.com/lancehalvorsen/phoenix-guides/blob/master/D_controllers.md 337 | * For a description of `plug :render` see https://github.com/lancehalvorsen/phoenix-guides/blob/master/D_controllers.md#rendering 338 | 339 | The render plug will look for a `Phoenix.View` file in the path `web/views/posts_view.ex`. Create that file with the following content: 340 | 341 | ```elixir 342 | defmodule Blog.PostsView do 343 | use Blog.View 344 | end 345 | ``` 346 | 347 | This view simply extends the `Blog.View` module that was generated by Phoenix. We don't need any additional functions in this file just now. 348 | 349 | The next thing we need is a template. Because the action in our controller is called `index` it is expected that this file is called `web/templates/posts/index.eex` Create it with the following contents: 350 | 351 | ```elixir 352 |

Posts

353 | ``` 354 | 355 | Start the application with `mix phoenix.start` and navigate to [http://localhost:4000/posts](http://localhost:4000/posts) and you should see a page with the Phoenix logo and the word "Posts" on there. 356 | 357 | ### Displaying the posts 358 | 359 | We now have a place to display our posts. To do this, we need to pass the posts through to the template from the controller. Update the `Blog.PostsController` module with the following `index` action: 360 | 361 | ```elixir 362 | def index(conn, _params) do 363 | conn 364 | |> assign(:posts, posts) 365 | end 366 | 367 | defp posts, do: Blog.Repo.all(Blog.Post) 368 | ``` 369 | 370 | The `assign` function takes two arguments: 371 | 372 | * A symbol in this case :posts - this is the name of the variable that will be available in the template. 373 | * A value, in this case we call the private function `posts` which returns all the posts like we did in the iex session earlier. in this case :posts - this is the name of the variable that will be available in the template. 374 | * A value, in this case we call the private function `posts` which returns all the posts like we did in the iex session earlier. 375 | 376 | We can now use this value in the posts template. Add the following to `web/templates/posts/index.html.eex` 377 | 378 | ```elixir 379 | 384 | ``` 385 | 386 | Now when you navigate to [http://localhost:4000/posts](http://localhost:4000/posts) you should see all the test posts you created earlier. 387 | 388 | ### Changing the layout 389 | 390 | Our posts page still have the Phoenix logo and footer on there. This is because we are using the application layout that was created when we ran the Phoenix generator. To change this we will create a new layout. Create a file `web/templates/layout/main.html.eex` with the following contents: 391 | 392 | ```elixir 393 | 394 | 395 | 396 | 397 | 398 | Blog 399 | 400 | 401 | 402 | 403 |
404 | <%= @inner %> 405 |
406 | 407 | 408 | ``` 409 | 410 | We now need to update the index action to point to this layout. Update the index action to the following: 411 | 412 | ``` 413 | def index(conn, _params) do 414 | conn 415 | |> put_layout(:main) 416 | |> assign(:posts, posts) 417 | end 418 | ``` 419 | 420 | When you visit the page now, you will no longer see the Phoenix header and footer. 421 | 422 | The code changes in this commit can be seen at https://github.com/Gazler/elixir-blog/commit/2225befc4c886983540a0d3592b46bc19f8667a9 423 | --------------------------------------------------------------------------------