Avatar

Kittens Website



CRUD Application with Phoenix

Written by:

andrea

in ELIXIRPHOENIX

development
crud

Simple introduction to Elixir

GOAL

This article was made while I was exploring the Elixir world for the first time, so please keep in mind that all is written from a beginner perspective.

What I had in mind at the time was to learn Elixir with a hands-on approach, trying to replicate a simple project I previously made in Python + SQLite + Flask.

The application will expose a simple web interface to handle information about groceries: the user will be able to add, remove, modify categories, shops and products. The data coming from the web part will be handled by the Elixir backend thta will do some basic validation and processing and send the data to a persistant storage (Postgresql)

To be more specific we will build a CRUD application.

What the fuck is a CRUD application?

If you were living under a rock (like me, up to a week ago) a CRUD application is just a web application in which you enter, modify, delete and see data. Nothing more, nothing less. A simple project to get started with the language and some common utility. In the case of Elixir the reference web framework is Phoenix, so that’s an easy choice. Let’s get started.

Init

Maybe the hardest part of Elixir is getting started: find tutorials, examples and useful learning resources is not so easy when the language is fairly new. I know that the language itself is 8yo, but was just recently that it has become widely spread, maybe for its built in concurrent capability, or maybe for the promise to be as powerful as Erlang with a human readable syntax.

Anyway your best pick for the documentation will be the following links:

Phoenix

“Phoenix is a web development framework written in Elixir which implements the server-side Model View Controller (MVC) pattern. Many of its components and concepts will seem familiar to those of us with experience in other web frameworks like Ruby on Rails or Python’s Django.”

Phoenix Documentation

mix phx.new --no-webpack productDBphoenix

For the purpose of this demo we will use Ecto to manage our database, but we don’t need to use the asset builder because we will not add or modify assets (for the moment).

So, now edit ‘config/dev.exs’ and change the relevant section to add the information about the database

# Example for my dev.exs, postgres is in a docker container
# accessible by its ip address
config :productDBphoenix, ProductDBphoenix.Repo,
  username: "postgres",
  password: "postgres",
  database: "productdbphoenix_dev",
  hostname: <postgres server address>,
  show_sensitive_data_on_connection_error: true,
  pool_size: 10

Forms

Phoenix can auto generate forms for you if you have an idea of what the data should be

The generic syntax is

mix phx.gen.html <Context> <Entity> <entities> <attribute> ...

phx gen doc

The command also auto generate the related schema for the database. If you don’t want to generate schema or you would like to do it yourself pass the flag ‘–no-schema’ but for the purpose of this demo I will use the default schema generated by phoenix and apply some modifications to it.

Phoenix can auto generate table statements (and then initialize the database)

mix phx.gen.schema <Schema> <schema-plural> <field:type>

Let’s define some html (and related schema) for our model and view

mix phx.gen.html ProductDB Category categories name:string notes:string
mix phx.gen.html ProductDB Shop shops name:string brand:string address:string notes:string
mix phx.gen.html ProductDB Product products name:string quantity:integer price:float ppu:float \
    shop:references:shops category_id:references:categories notes:string

Note 1: that all the ‘:string’ can be omitted because if the ‘type’ is not specified Phoenix will assume you want a simple string field

Note 2: From the second line Phoenix will interactively ask you if you want to add the new Entity to the existing context. This is up to you, in this case the application is tiny and all entities are related to each other, but may happen that you are developing a bigger application, in which is it clear that multiple group of entities could be defined and for the sake of logical separation of the sources it is a good idea to use different a context

Init resources

Even if we instructed phoenix do define our resources our database has not been init by the database manager: Ecto.

“Ecto is an official Elixir project providing a database wrapper and integrated query language. With Ecto we’re able to create migrations, define schemas, insert and update records, and query them.”

Ecto is fully integrated with Phoenix as long as the actual database interfaces are up to date (for example at the time this guide is written SQLite connector is not compatible with the latest Phoenix/Ecto)

To init the database layer use

mix ecto.create
mix ecto.migrate

This will both init ecto and the database, create tables following the structure described with the above commands

At this point our “resources” will be ready to use, but we will not be able to use them. To know what route (a.k.a. url) Phoenix will recognize enter

mix phx.routes

Phoenix will output just the default path

    page_path  GET     /                                      ProductDBphoenixWeb.PageController :index

To make a quick check that everything is working run

mix phx.server

and go check here that everything is working properly.

Note that the resources we created earlier are still unaccessible, you can check in the project folder that templates and logic is there but is still disconnected from the frontend. To enable you, the user, to reach the resources we need to add routes to Phoenix

Routes, for you who lived under a rock, are the high level description of the connection between the frontend request (HTTP GET) and the action performed by the backend.

Controller, is the backend handler of the request

Let’s add some new routes, open your ‘lib/productDBphoenix_web/router.ex’ and identify the “scope” block by the line

  scope "/", ProductDBphoenixWeb do
    pipe_through :browser

    get "/", PageController, :index

  end

get is our first default route that will recognize the HTTP GET requests and will look for the class PageController that will render its own index page. Since the phoenix framework has a excellent separation of view and logic, which allow to define every class in its own file the line could be seen as

get "/" lib/productDBphoenix_web/controllers/page_controller.ex, lib/productDBphoenix_web/templates/page/index.html.eex

Now under the already present get we will add the more powerful ‘resource’ directive, to have something like that

  scope "/", ProductDBphoenixWeb do
    pipe_through :browser

    get "/", PageController, :index

    resources "/shop", ShopController
    resources "/product", ProductController
    resources "/category", CategoryController

  end

Note a difference with the default get directive: we do not specify an “entrypoint”: all routes will be auto-generated, a quick check via ‘mix phx.routes’ will show all the new entries

$ mix phx.routes

    page_path  GET     /                                      ProductDBphoenixWeb.PageController :index
    shop_path  GET     /shop                                  ProductDBphoenixWeb.ShopController :index
    shop_path  GET     /shop/:id/edit                         ProductDBphoenixWeb.ShopController :edit
    shop_path  GET     /shop/new                              ProductDBphoenixWeb.ShopController :new
    shop_path  GET     /shop/:id                              ProductDBphoenixWeb.ShopController :show
    shop_path  POST    /shop                                  ProductDBphoenixWeb.ShopController :create
    shop_path  PATCH   /shop/:id                              ProductDBphoenixWeb.ShopController :update
               PUT     /shop/:id                              ProductDBphoenixWeb.ShopController :update
    shop_path  DELETE  /shop/:id                              ProductDBphoenixWeb.ShopController :delete
 product_path  GET     /product                               ProductDBphoenixWeb.ProductController :index
 product_path  GET     /product/:id/edit                      ProductDBphoenixWeb.ProductController :edit
 product_path  GET     /product/new                           ProductDBphoenixWeb.ProductController :new
 product_path  GET     /product/:id                           ProductDBphoenixWeb.ProductController :show
 product_path  POST    /product                               ProductDBphoenixWeb.ProductController :create
 product_path  PATCH   /product/:id                           ProductDBphoenixWeb.ProductController :update
               PUT     /product/:id                           ProductDBphoenixWeb.ProductController :update
 product_path  DELETE  /product/:id                           ProductDBphoenixWeb.ProductController :delete
category_path  GET     /category                              ProductDBphoenixWeb.CategoryController :index
category_path  GET     /category/:id/edit                     ProductDBphoenixWeb.CategoryController :edit
category_path  GET     /category/new                          ProductDBphoenixWeb.CategoryController :new
category_path  GET     /category/:id                          ProductDBphoenixWeb.CategoryController :show
category_path  POST    /category                              ProductDBphoenixWeb.CategoryController :create
category_path  PATCH   /category/:id                          ProductDBphoenixWeb.CategoryController :update
               PUT     /category/:id                          ProductDBphoenixWeb.CategoryController :update
category_path  DELETE  /category/:id                          ProductDBphoenixWeb.CategoryController :delete
    websocket  WS      /socket/websocket                      ProductDBphoenixWeb.UserSocket

The third column represent real paths understood by phoenix, so shop will really allow you to add shops to our database, try it now!

It is indeed magical, but fear not we will get out hands dirty now, because all the magic comes to an end. Remember how we defined a product? It has an internal reference to a shop and to a category, but if we reach the product page there will be no reference to it.

What appened is that phoenix generate a partial form, only for the stuff it know to handle, the rest is up to the programmer and that’s what we are going to implement.

Let’s start from the form, so open our ‘lib/productDBphoenix_web/templates/product/form.html.eex’, a file like

<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= label f, :name %>
  <%= text_input f, :name %>
  <%= error_tag f, :name %>

  <%= label f, :quantity %>
  <%= number_input f, :quantity %>
  <%= error_tag f, :quantity %>

  <%= label f, :price %>
  <%= number_input f, :price, step: "any" %>
  <%= error_tag f, :price %>

  <%= label f, :ppu %>
  <%= number_input f, :ppu, step: "any" %>
  <%= error_tag f, :ppu %>

  <%= label f, :notes %>
  <%= text_input f, :notes %>
  <%= error_tag f, :notes %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

This file is a template in which we define the content of the page through variable coming from the backend. Actually every directive is a function to which we pass several arguments.

As you can see just the basic stuff is defined here, let’s add a couple of statement to display a select box from which we can assign a shop and a category to our product

  <%= label f, :shop_id %>
  <%= select f, :shop_id, Enum.map(@shops, &{&1.name, &1.id}) %>

  <%= label f, :category_id %>
  <%= select f, :category_id, Enum.map(@categories, &{&1.name, &1.id}) %>

@shops and @categories are just variables that we need to define before being able to use them, so open the controller ‘lib/productDBphoenix_web/controllers/product_controller.ex’. This file is the main logic behind the pages responsible for the resource Product. On top we can see the definition

alias ProductDBphoenix.ProductDB
alias ProductDBphoenix.ProductDB.Product

ProductDB here is the name of our repository, these aliases are defined just for convenience. If needed also

alias ProductDBphoenix.ProductDB.Category
alias ProductDBphoenix.ProductDB.Shop

could be added as well, but that’s not needed at the moment. Since Phoenix and Ecto did actually set up a nice environment to work with let’s dig a bit more on what is available for us to play with.

In ‘lib/productDBphoenix/product_db.ex’ you will find a list of predefined querie that are needed to preserve our changes from the frontend, but we can make use of these functions directly.

Going back to the product controller, under the ‘def new(conn, _params) do’ (or really whatever hook you want to modify), we can add a definition for our variables using one of the predefined queries:

 def new(conn, _params) do
    changeset = ProductDB.change_product(%Product{})
    shops = ProductDB.list_shops
    categories = ProductDB.list_categories
    render(conn, "new.html", changeset: changeset, categories: categories, shops: shops)
  end

Note: do not forget to pass the variable to the render function, otherwise they will be undefined when you need them.

Now if you restart the server and look at the product page there will be two more options when you create a product, to assign a category and a shop ( you need to have at least one already in you database! )

But even if you assign shop and category in the form they will be not stored, because the backend (ecto) is not instructed to do so. Open ‘lib/productDBphoenix/product_db/product.ex’ and customize the ‘changeset’ function, that is called when you save a new product. The ‘cast’ function take the array of parameters to save, so add the relative fields.

    |> cast(attrs, [:name, :quantity, :price, :ppu, :shop_id, :category_id, :notes])

Optionally is possible to modify also the parameters passed to the ‘validate_required’ when you have optional fields. For example ‘notes’ is optional so we will have. Note that the select used for category and shop will force the user to choose a value so it is not necessary to validate the data.

    |> validate_required([:name, :quantity, :price, :ppu])

Extending other controller methods such as show and index will be left as an excercise to the reader. I hope you enjoyed the tutorial, if something is not clear or if you found some error please feel free to drop me an email