Victor Björklund

Build your custom Phoenix phx.new generator

Published: Aug 10 2023

As a developer, one of the gravest sins is violating the principle of DRY (Don’t Repeat Yourself). This holds true within a project and even between projects. If you find yourself repeatedly performing a task, it’s worth considering automation. Elixir and Phoenix strike a good balance between readability and avoiding boilerplate code. Phoenix, in particular, makes starting a new project easy and comes with useful mix tasks and generators.

The problem

Despite how good the tasks and generators are you most likely need to edit and change a lot of things since it is unlikely that your apps style and functionality will totally match the generators. And this is fine and not really a big issue.

Especially if you mainly work on one or two projects since you just change the code once and it is done. In fact the Phoenix team never intended for the the generators to be anything other than a learning tool. But some of us just playing around and creating small new projects every week and then it can gets rather boring to make the same changes to a new project over and over again.

Possible solutions

There are various solutions to this problem. You could create a template project on GitHub and clone it for each new project. Another option is to develop a library that addresses common aspects of projects. In this article, we’ll explore a different approach: modifying and extending Phoenix’s mix task generators to align with your project requirements.

Before diving into the how-to, it’s important to consider when not to pursue this route. If you work on just a few projects annually, the effort might not be justified. Additionally, maintaining these custom mix tasks can become challenging when new Phoenix versions are released.

If you’re still on board, let’s proceed. You can customize everything from design to configuration, but for simplicity, we’ll focus on a specific example. We’ll integrate Oban, a background job queue library, into the mix phx.new generator for non-umbrella apps. However, keep in mind that Oban will be added to all projects, though you can make it optional with a flag.

Step 1 - Make a copy of the installer folder

Navigate to the Phoenix project on GitHub and download the code for latest release (currently 1.7.7). Copy the “installer” folder to a new location, renaming it as desired. For instance, I’ll choose “phx_new_task.”

Step 2 - Create a new mix phx.new task

The first thing we will do is go to /lib/mix/tasks/phx.new.ex and rename it to something else because we don’t want to overwrite the mix phx.new command. If you want you can of course replace mix phx.new totally with your own implementation but I prefer to give it a slightly different name so I can have both versions installed at the same time. I will go with /lib/mix/tasks/phx.new_project.ex and don’t forget to also change the module name inside the file from Mix.Tasks.Phx.New to Mix.Tasks.Phx.NewProject.

Step 3 - Change the code

Here’s where the real fun starts. Familiarize yourself with the code structure within the folder to grasp its layout. Elixir’s readability simplifies this process, even when delving into library code. The primary hurdle might be the usage of macros; if you’re not familiar, this might feel different.

With an overview of the existing code, let’s identify the changes needed. Referring to the Oban documentation, we find four installation steps (excluding worker creation):

Adding Oban as dependency

The way this generator works is that it copies certain files from the folder called /lib/templates and make edits in that file based on the flags you provided when invoking the task. So open the file /templates/phx_single/mix.exs and in the deps function you just add Oban as a dependency {:oban, "~> 2.15"}. Now we are done with this first part.

Edit the config

In order for Oban to work we need to add some configuration. Start by opening the file templates/phx_single/config/config.exs and paste in the default config for Oban:

config :my_app, Oban, 
repo: MyApp.Repo, 
plugins: [Oban.Plugins.Pruner], 
queues: [default: 10]

But as you see we need to change :my_app and MyApp.Repo (unless all your apps are called MyApp) and we need to do this in a dynamic way to account for the app name provided by the user. If we peak at the code in templates/phx_single/config/config.exs we can find out how we can achieve that. For example we can see the config for ecto:

config :<%= @app_name %><%= if @namespaced? do %>,

namespace: <%= @app_module %><% end %><%= if @ecto do %>,

ecto_repos: [<%= @app_module %>.Repo]<% end %><%= if @generators do %>,

generators: <%= inspect @generators %><% end %><% end %>

So let’s change the Oban config to:

config :<%= @app_name %>, Oban, 
repo: <%= @app_module %>.Repo, 
plugins: [Oban.Plugins.Pruner], 
queues: [default: 10]

And lets do the same for the default config for templates/phx_single/config/test.exs

config :my_app, Oban, testing: :inline

And change it to:

config :<%= @app_name %>, Oban, testing: :inline

Creating the Ecto migration

This one is a little bit more complicated because we need to create a new file that does not exist. Start by creating a new migration file inside the templates/phx_ecto folder. The name doesn’t matter and same with the migration date (just don’t put a future date) so I will go with 20230804180731_add_oban.exs.

In the migration file you paste in the default code from the hexdocs:

defmodule MyApp.Repo.Migrations.AddObanJobsTable do
  use Ecto.Migration

  def up do
    Oban.Migration.up(version: 11)
  end

  # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if
  # necessary, regardless of which version we've migrated `up` to.
  def down do
    Oban.Migration.down(version: 1)
  end
end

And again we need to change “MyApp” to a dynamic name that will be changed when we run the generator:

defmodule <%= @app_module %>.Repo.Migrations.AddObanJobsTable do
  use Ecto.Migration

  def up do
    Oban.Migration.up(version: 11)
  end

  # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if
  # necessary, regardless of which version we've migrated `up` to.
  def down do
    Oban.Migration.down(version: 1)
  end
end

And since this a new file we need to tell the generator to copy the file and were it should be placed. Open the file lib/phx_new/single.ex and you see a list of template files that will be copied to the new project.

template(:ecto, [
{:eex, :app,
"phx_ecto/repo.ex": "lib/:app/repo.ex",
"phx_ecto/formatter.exs": "priv/repo/migrations/.formatter.exs",
"phx_ecto/seeds.exs": "priv/repo/seeds.exs",
"phx_ecto/data_case.ex": "test/support/data_case.ex"},
{:keep, :app, "phx_ecto/priv/repo/migrations": "priv/repo/migrations"}
])

And add our new file to the list and put it in the priv/repo/migrations folder:

template(:ecto, [
{:eex, :app,
"phx_ecto/repo.ex": "lib/:app/repo.ex",
"phx_ecto/formatter.exs": "priv/repo/migrations/.formatter.exs",
"phx_ecto/seeds.exs": "priv/repo/seeds.exs",
"phx_ecto/20230804180731_add_oban.exs": "priv/repo/migrations/20230804180731_add_oban.exs",
"phx_ecto/data_case.ex": "test/support/data_case.ex"},
{:keep, :app, "phx_ecto/priv/repo/migrations": "priv/repo/migrations"}
])

Add Oban to the application tree

Edit templates/phx_single/lib/app_name/application.ex to add Oban to the application children:

@impl true

def start(_type, _args) do

children = [

# Start the Telemetry supervisor

<%= @web_namespace %>.Telemetry,<%= if @ecto do %>

# Start the Ecto repository

<%= @app_module %>.Repo,<% end %>
{Oban, Application.fetch_env!(:my_app, Oban)},

And of course we then need to change :my_app to :<%= @app_name %>

{Oban, Application.fetch_env!(:<%= @app_name %>, Oban)},

Step 4 - Install and run

After completing the above steps, execute mix do archive.build, archive.install in the project directory to install the mix task globally. Now, you can create a new project using mix phx.new_project hello_world, generating an app with preinstalled Oban functionality.

Conclusion

This guide presents a starting point for further enhancements. Next steps could involve incorporating conditional flags to selectively add Oban or introducing other libraries commonly used in your projects. Feel free to explore and adapt based on your requirements.