In my last blog post, I dug into Elm and while I like a number of things conceptually about Elm, the process of implementing nothing more than a maybe 8 element HTML form left me hugely frustrated and annoyed. I also didn't like the way that Elm bundles code and display into one thing. I may be a traditionalist perhaps but the separation of views and code really does work well in practice. I get that Elm is a different thing but it shouldn't be this hard to just put some text and an html form on the screen.

And, so, I'm putting Elm to the side for a bit and going to experiment with using Elixir and Phoenix for my previous Laravel application. If nothing else this should be much more straightforward as it is moving from one MVC style framework to another albeit from an OO lang (php) to a functional lang (elixir).

Step 1: Getting Up To Date

I'm on OSX and my goal was to get to the current Elixir and Phoenix before I started, This I found to be surprisingly problematic. And, perhaps, it was my fault as I don't claim to be great with HomeBrew. I started with:

brew update
brew install elixir@1.12

But no matter what I did, I couldn't get past elixir 1.11. So my next step was to get rid of everything.

Getting Rid of Everything

I started with:

brew uninstall elixir

And then I got a warning about a shallow copy issue which told me to run:

git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow
			
	brew update
	
	brew install elixir

which finally resulted in:

	❯ elixir -v
	Erlang/OTP 24 [erts-12.3.1] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit] [dtrace]

	Elixir 1.13.3 (compiled with Erlang/OTP 24)

Installing Phoenix

A bit of googling gave me this command to install the newest Phoenix:

	mix archive.install hex phx_new

Step 2: Generating a New App

Although I've actually been to ElixirCon, things have changed a lot in a few years. Here's the new syntax for generating an app:

mix phx.new kit_selector3 --database mysql

Normally I'd be using postgres which is the default for Elixir but I had an existing MySQL database hence the "–database mysql".

Step 3: Setting Database Connection Parametters

The next step is to edit the file config/dev.exs and update the database connection parameters.

Step 4: Using an Existing Database

The default for MVC style application development is to always assume that the database is entirely new. In this case, however, I have an existing database and schema to work with of about 10 tables. My general approach with this case is to actually implement the migrations needed for each table. That allows the test environment to actually match the existing database. The general trick with this is to use a technique which amounts to "don't_create_the_table_if_it_already_exists" type of create statement.

Here's my Laravel schema for my products table:

	<?php

	use Illuminate\Database\Migrations\Migration;
	use Illuminate\Database\Schema\Blueprint;
	use Illuminate\Support\Facades\Schema;

	class CreateProductsTable extends Migration
	{
	    /**
	     * Run the migrations.
	     *
	     * @return void
	     */
	    public function up()
	    {
	        Schema::create('products', function (Blueprint $table) {
	            $table->id();
	            $table->timestamps();
	            $table->integer('product_type_id');
	            $table->integer('product_style_id');
	            $table->integer('product_design_id');
	            $table->integer('product_colorway_id');
	            $table->string('image_front_path');
	            $table->string('image_back_path');
	            $table->string('image_left_path');
	            $table->string('image_right_path');
	            $table->string('team_name');
	            $table->string('team_number');
        
        
        
	        });
	    }

	    /**
	     * Reverse the migrations.
	     *
	     * @return void
	     */
	    public function down()
	    {
	        Schema::dropIfExists('products');
	    }
	}

And here's a generate statement:

mix phx.gen.schema Product products product_type_id:integer product_style_id:integer product_design_id:integer product_colorway_id:integer image_front_path:string image_back_path:string image_left_path:string image_right_path:string team_name:string team_number:string

You should see output like this:

	===> Compiling telemetry
	===> Rebar3 detected a lock file from a newer version. It will be loaded in compatibility mode, but important information may be missing or lost. It is recommended to upgrade Rebar3.
	===> Compiling telemetry_poller
	===> Rebar3 detected a lock file from a newer version. It will be loaded in compatibility mode, but important information may be missing or lost. It is recommended to upgrade Rebar3.
	===> Compiling cowboy_telemetry
	==> phoenix_live_dashboard
	Compiling 40 files (.ex)
	Generated phoenix_live_dashboard app
	==> swoosh
	Compiling 38 files (.ex)
	Generated swoosh app
	==> phoenix_ecto
	Compiling 7 files (.ex)
	Generated phoenix_ecto app
	==> kit_selector3
	* creating lib/kit_selector3/product.ex
	* creating priv/repo/migrations/20220402134750_create_products.exs

	Remember to update your repository by running migrations:

	    $ mix ecto.migrate

Your generated migration is here:

priv/repo/migrations/20220402134750_create_products.exs

and the migration looks as follows:

	defmodule KitSelector3.Repo.Migrations.CreateProducts do
	  use Ecto.Migration

	  def change do
	    create table(:products) do
	      add :product_type_id, :integer
	      add :product_style_id, :integer
	      add :product_design_id, :integer
	      add :product_colorway_id, :integer
	      add :image_front_path, :string
	      add :image_back_path, :string
	      add :image_left_path, :string
	      add :image_right_path, :string
	      add :team_name, :string
	      add :team_number, :string

	      timestamps()
	    end
	  end
	end

The trick here is to change the table creation from create table to:

	create_if_not_exists table(:thing)

I found this trick in an Elixir Forum post. This needs to be done before the migration is executed with:

 mix ecto.migrate

Now in my case, I failed to save my changed migration (the one with create_if_not_exists) before I ran the migrate command and I got this:

	❯ mix ecto.migrate
	Compiling 15 files (.ex)
	Generated kit_selector3 app

	09:51:50.157 [info]  == Running 20220402134750 KitSelector3.Repo.Migrations.CreateProducts.change/0 forward

	09:51:50.162 [info]  create table products
	** (MyXQL.Error) (1050) (ER_TABLE_EXISTS_ERROR) Table 'products' already exists
	    (ecto_sql 3.7.2) lib/ecto/adapters/sql.ex:760: Ecto.Adapters.SQL.raise_sql_call_error/1
	    (elixir 1.13.3) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
	    (ecto_sql 3.7.2) lib/ecto/adapters/sql.ex:852: Ecto.Adapters.SQL.execute_ddl/4
	    (ecto_sql 3.7.2) lib/ecto/migration/runner.ex:343: Ecto.Migration.Runner.log_and_execute_ddl/3
	    (ecto_sql 3.7.2) lib/ecto/migration/runner.ex:117: anonymous fn/6 in Ecto.Migration.Runner.flush/0
	    (elixir 1.13.3) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
	    (ecto_sql 3.7.2) lib/ecto/migration/runner.ex:116: Ecto.Migration.Runner.flush/0
	    (stdlib 3.17.1) timer.erl:166: :timer.tc/1

The take away here is that migrations will NOT overwrite existing tables. That's the exact right design choice for Phoenix to have made and I'm glad to see it.

Saving the migration file and trying again gave:

	mix ecto.migrate

	09:59:44.609 [info]  == Running 20220402134750 KitSelector3.Repo.Migrations.CreateProducts.change/0 forward

	09:59:44.614 [info]  create table if not exists products

	09:59:44.616 [info]  == Migrated 20220402134750 in 0.0s

I then implemented migrations for the remaining tables.

Generating Just a Controller

The application I'm converting from Laravel to Elixir has a mildly unusual structure:

  • There is one main controller, KitSelector
  • It renders a view
  • It isn't really a restful / CRUD style application
  • A form is displayed, an order is created and then JavaScript sends it to Shopify

This means that I need to generate a non-restful controller without a ton of the default views. This is something easily done in Rails but less easily done in Elixir. Googling for answers didn't prove to be tremendously useful so this led to trial and error style experimentation. Here was my first result:

	mix phx.gen.html KitSelector Cat cats --no-schema --no-context
	* creating lib/kit_selector3_web/controllers/cat_controller.ex
	* creating lib/kit_selector3_web/templates/cat/edit.html.heex
	* creating lib/kit_selector3_web/templates/cat/form.html.heex
	* creating lib/kit_selector3_web/templates/cat/index.html.heex
	* creating lib/kit_selector3_web/templates/cat/new.html.heex
	* creating lib/kit_selector3_web/templates/cat/show.html.heex
	* creating lib/kit_selector3_web/views/cat_view.ex
	* creating test/kit_selector3_web/controllers/cat_controller_test.exs

	Add the resource to your browser scope in lib/kit_selector3_web/router.ex:

	    resources "/cats", CatController

Yes I know that I don't have any Cat or cats in my application but I didn't find the online help, well, helpful:

	mix phx.gen.html --help
	** (Mix) Invalid arguments

	mix phx.gen.html, phx.gen.json, phx.gen.live, and phx.gen.context
	expect a context module name, followed by singular and plural names
	of the generated resource, ending with any number of attributes.
	For example:

	    mix phx.gen.html Accounts User users name:string
	    mix phx.gen.json Accounts User users name:string
	    mix phx.gen.live Accounts User users name:string
	    mix phx.gen.context Accounts User users name:string

	The context serves as the API boundary for the given resource.
	Multiple resources may belong to a context and a resource may be
	split over distinct contexts (such as Accounts.User and Payments.User).

Looking at the generated files led me to try this command:

mix phx.gen.html KitSelector KitSelector kit_selector --no-schema --no-context

which had this result:

	** (Mix) The context and schema should have different names

	mix phx.gen.html, phx.gen.json, phx.gen.live, and phx.gen.context
	expect a context module name, followed by singular and plural names
	of the generated resource, ending with any number of attributes.
	For example:

	    mix phx.gen.html Accounts User users name:string
	    mix phx.gen.json Accounts User users name:string
	    mix phx.gen.live Accounts User users name:string
	    mix phx.gen.context Accounts User users name:string

	The context serves as the API boundary for the given resource.
	Multiple resources may belong to a context and a resource may be
	split over distinct contexts (such as Accounts.User and Payments.User).

Another try led to this:

mix phx.gen.html General KitSelector kit_selector --no-schema --no-context

which had this result:

	mix phx.gen.html General KitSelector kit_selector --no-schema --no-context
	* creating lib/kit_selector3_web/controllers/kit_selector_controller.ex
	* creating lib/kit_selector3_web/templates/kit_selector/edit.html.heex
	* creating lib/kit_selector3_web/templates/kit_selector/form.html.heex
	* creating lib/kit_selector3_web/templates/kit_selector/index.html.heex
	* creating lib/kit_selector3_web/templates/kit_selector/new.html.heex
	* creating lib/kit_selector3_web/templates/kit_selector/show.html.heex
	* creating lib/kit_selector3_web/views/kit_selector_view.ex
	* creating test/kit_selector3_web/controllers/kit_selector_controller_test.exs

	Add the resource to your browser scope in lib/kit_selector3_web/router.ex:

	    resources "/kit_selector", KitSelectorController

Overall this feels pretty close to right and the views that aren't index.html.heex can simply be deleted with rm statements similar to:

rm lib/kit_selector3_web/templates/kit_selector/edit.html.heex

although I actually chose to leave them in place. Yes they are cruft but they may illustrate view techniques that I'll need to use.

Unfortunately when I went to test the application by starting the server, I got this result:

	mix phx.server
	Compiling 3 files (.ex)
	warning: variable "f" is unused (if the variable is not meant to be used, prefix it with an underscore)
	  lib/kit_selector3_web/templates/kit_selector/form.html.heex:1: KitSelector3Web.KitSelectorView."form.html"/1


	== Compilation error in file lib/kit_selector3_web/controllers/kit_selector_controller.ex ==
	** (CompileError) lib/kit_selector3_web/controllers/kit_selector_controller.ex:13: KitSelector3.General.KitSelector.__struct__/1 is undefined, cannot expand struct KitSelector3.General.KitSelector. Make sure the struct name is correct. If the struct name exists and is correct but it still cannot be found, you likely have cyclic module usage in your code

And that's where real life constraints got in the way of fun code explorations.