To start your Phoenix server:
- Run
mix deps.get
to install dependencies - Start Phoenix endpoint with
mix phx.server
or inside IEx withiex -S mix phx.server
Now you can visit localhost:4000/topics
from your browser.
- Course: https://www.udemy.com/course/the-complete-elixir-and-phoenix-bootcamp-and-tutorial/
- Repo I've used to see implementation using latest Phoenix version: https://github.com/tfnielson-se/elixir-phoenix-udemy
- Official website: https://www.phoenixframework.org/
- This is mainly done running
mix phoenix.new discuss
- It will scaffold the project, creating a basic configuration for things like username and password for the database
- It will also create a basic web UI, and a sample page controller with associated view
- You need to run
mix ecto.create
: it will create the database using the parameters inside config folder. Ecto is a toolkit for data mapping and language integrated query for Elixir. You can see it as a wrapper for the database - Then with
mix ecto.gen.migration add_topics
you will create the first migration. A migration file is used to define changes to the database schema, such as creating or altering tables, adding or removing columns, and other modifications - Running
mix ecto.migrate
will actually mutate the database structure
- Create
Topic Model
file. We just need a title String, thechangeset
fun is "boilerplate" to validate data before inserting into database, we receive a basic struct and we validate it
- In the router define the entry
get "/", TopicController, :index
. This will invoke the funindex
ofTopicController
everytime you receive a GET request at/
path - Create the
TopicController
. For theindex
fun the controller gets all the Topics using Repo (from Ecto) and passes them to a view.render
is the function to render a given view, with parameters. Here the view is:index
, so it will loadlib/discuss_web/controllers/topic_html/index.html.heex
and we pass all the topics - The view will use some special markup provided by phoenix to render a header, an action and a table. Using
~p"/topics/...
urls are validated at compile time to avoid errors
- In the router define the controller and the method get similar to what you already did
"get /topics/new", TopicController, :new
- The controller creates an empty changeset and passes to the new view
lib/discuss_web/controllers/topic_html/new.html.heex
. Inside this view you'll use<.topic_form />
markup. Inlib/discuss_web/controllers/topic_html.ex
there is the definition, it will require an Ecto ChangeSet and an action. Inlib/discuss_web/controllers/topic_html/topic_form.html.heex
there is the implementation using special Phoenix markup for forms - Back to
lib/discuss_web/controllers/topic_html/new.html.heex
you'll pass the changesed you got from the controller, and specify~p"/topics"
as action. This will be mapped into a post request to/topics
. You'll map this new route as always, specifyingpost "/topics", TopicController, :create
into the router. InsideTopicController
create
you build a ChangeSet based on the data in the body of the HTTP post request,extracted with pattern matching , then use Ecto Repo to insert in the database. If this is successful the user will be redirected to the index, otherwise the page will be reloaded, passing the changeset with errors, so the user can see them
- In the router define the controller and the method
get "/topics/:id/edit", TopicController, :edit
. In the controller load the given Topic, extracting the id with pattern matching, and create a changeset to pass to the view. In the viewlib/discuss_web/controllers/topic_html/edit.html.heex
you reuse the<.topic_form />
passing both the changeset and the action. This action will be mapped into a put request to"/topics/:id"
- Map this request in the router with
put "/topics/:id", TopicController, :update
. In the controller, inside update method you extract both the topic_id and the data submitted by the form thanks to pattern matching and create a changed merging old and new data, saving the result to the database.
- Back to
lib/discuss_web/controllers/topic_html/index.html.heex
you added a linkhref={~p"/topics/#{topic}"} method="delete"
. In this case instead of mapping a new individual route in the router you useresources "/", TopicController
, this will follow REST conventions and maps all the previous routes plus theDELETE
one. - The controller will delete the desired topic, extracting the id from the request
mix ecto.gen.migration add_users
terminal command will createpriv/repo/migrations/20240329152151_add_users.exs
where you define the database data.timestamps()
adds utilities such as last update.mix ecto.gen.migration add_user_id_to_topics
createspriv/repo/migrations/20240405124213_add_user_id_to_topics.exs
where you link each topic to a usermix ecto.migrate
runs the migration and actually mutates the database structure- As last step create and update our models:
lib/discuss_web/models/user.ex
andlib/discuss_web/models/topics.ex
usinghas_many
andbelongs_to
. They are used by Ecto for relationship mapping
- Add https://github.com/ueberauth/ueberauth to
mix.exs
dependencies, bothueberauth
andueberauth_github
, and its configurationconfig :ueberauth
inconfig.exs
(you need to create a GitHub application to getclient_id
andclient_secret
) - Add sign-in and logout links to the header in
lib/discuss_web/components/layouts/app.html.heex
(ignore@conn.assigns[:user]
at the moment). - Update the router to handle
get "/signout"
,get "/:provider"
andget "/:provider/callback"
delegating toAuthController
- In the new AuthController, you are going to use
plug Ueberauth
to delegate some part of the implementation to the library. Indeed, you do not have to implementrequest
method, Ueberauth is doing it for you.signout
instead is pretty basic, just clear the session. Incallback
you extract the result auth value using pattern matching, and create a User to save or update values in the database. The key is that you also update the session with the current user:put_session(:user_id, user.id)
- A plug is like an interceptor, executed for every request
- Let's start with
DiscussWeb.Plugs.SetUser
. You look for:user_id
in the session, and you add the associated user to theconn
object, so you always have the current logged user available. To "enable" this plug, set it up in therouter
- Define another plug,
DiscussWeb.Plugs.RequireAuth
that will redirect the user to the index, if not logged and use insideTopicController
- Create another plug, this time directly inside
TopicController
because it is the only user:check_topic_owner
. This one will check if the requested topic was created by the logged user
- Now that a topic has an associated user, when creating it we must associate the current logged user with
build_assoc
. This is used by Ecto to manage relationships - Now you can update some links (1, 2) checking the user in the session