Model Callbacks in Phoenix

I've started to learn the Phoenix web framework and explore it's many features and conventions.

I was playing with Models which are handled by the ecto lib. I had a Post model that I wanted to give a "pretty" url. So that I could do something like this in my controller.

def show(conn, %{"id" => slug}) do  
    post = Repo.get_by!(Post, slug: slug)
    render(conn, "show.html", post: post)
end  

Instead of the default behavior of finding a post by its ID, I wanted to find it by a slug field.

This would cause a problem though if the slug field was left blank, but having to type in your own slug every time you created a new post is no fun! This is a great opportunity to try out one of the callbacks that Ecto provides.

In my Post model I added before_insert :generate_slug callback. This will only be called when we insert a new record into our repo which is exactly when we want.

To handle the callback you need to create a function that accepts the Ecto.Changeset struct. For my purposes the generate_slug function needs to check to see if slug contains a value if so just send back the changeset and if not, then send back the changeset with that field modified.

defp generate_slug(changeset) do  
    case fetch_field(changeset, :slug) do
      {:changes, nil} ->
        changeset = put_change(changeset, :slug, title_to_slug(fetch_field(changeset, :title)))
      _ ->
        changeset
     end
  end

The titletoslug function leaves a lot to be desired but its a great starting point.

def title_to_slug({_, title}) do  
    title
    |> String.downcase
    |> String.replace(" ", "-")
  end

The full model at this point.

defmodule BirdBlog.Post do  
  use BirdBlog.Web, :model

  before_insert :generate_slug

  schema "posts" do
    field :title, :string
    field :slug, :string
    field :body, :string

    timestamps
  end

  @required_fields ~w(title body)
  @optional_fields ~w(slug)

  @doc """
  Creates a changeset based on the `model` and `params`.
  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end

  def title_to_slug({_, title}) do
    title
    |> String.downcase
    |> String.replace(" ", "-")
  end

  defp generate_slug(changeset) do
    case fetch_field(changeset, :slug) do
      {:changes, nil} ->
        changeset = put_change(changeset, :slug, title_to_slug(fetch_field(changeset, :title)))
      _ ->
        changeset
     end
  end

end