Creating Slugs

We want our videos to have a unique URL-friendly identifier, called a slug. This approach lets us have a unique identifier that will build URLs that are friendlier to people and search engines. We need to create the slug from the title so we can represent a video titled Programming Elixir as a URL-friendly slug, such as 1-programming-elixir, where 1 is the video ID.

The first step is to add a slug column to the database:

 $ ​​mix​​ ​​ecto.gen.migration​​ ​​add_slug_to_videos

We generate a new migration. Remember, your name will differ based on the timestamp attached to the front of the file, but you can find the new file in the priv/repo/migrations directory. Let’s fill it in like this:

 def​ change ​do
  alter table(​:videos​) ​do
  add ​:slug​, ​:string
 end
 end

Our new migration uses the alter macro, which changes the schema for both up and down migrations. With the migration in place, let’s apply it to the database:

 $ ​​mix​​ ​​ecto.migrate
 [info] == Running Rumbl.Repo.Migrations.AddSlugToVideos.change/0 forward
 [info] alter table videos
 [info] == Migrated in 0.0s

Next, we need to add the new field to the video schema in lib/rumbl/multimedia/video.ex, beneath the other fields:

 field ​:slug​, ​:string

The whole premise of a slug is that you can automatically generate a permanent field from other fields, some of which may be updatable. Let’s do this by changing the changeset function, like this:

 def​ changeset(video, attrs) ​do
  video
  |> cast(attrs, [​:url​, ​:title​, ​:description​, ​:category_id​])
  |> validate_required([​:url​, ​:title​, ​:description​])
  |> assoc_constraint(​:category​)
  |> slugify_title()
 end
 
 defp​ slugify_title(changeset) ​do
 case​ fetch_change(changeset, ​:title​) ​do
  {​:ok​, new_title} -> put_change(changeset, ​:slug​, slugify(new_title))
 :error​ -> changeset
 end
 end
 
 defp​ slugify(str) ​do
  str
  |> String.downcase()
  |> String.replace(​~r/[^w-]+/​u, ​"​​-"​)
 end

We modify the generated changeset, just as we did the changeset for the password. We build the slug field within our changeset. The code couldn’t be simpler. The pipe operator makes it easy for us to tell a story with code.

If a change is made to the title, we build a slug based on the new title with the slugify function. Otherwise, we simply return the changeset. slugify downcases the string and replaces nonword characters with a - character. cast, assoc_constraint, fetch_change and put_change are all functions defined in Ecto.Changeset, imported at the top of our video module.

Don’t miss the importance of what we’ve done here. We’re once again able to change how data gets into the system, without touching the controller and without using callbacks or any other indirection. All of the changes to be performed by the database are clearly outlined in the changeset. At this point, you’ve learned all the concepts behind changesets, and the benefits are becoming clearer:

  • Because Ecto separates changesets from the definition of a given record, we can have a separate change policy for each type of change. We could easily add a JSON API that creates videos, including the slug field, for example.

  • Changesets filter and cast the incoming data, making sure sensitive fields like a user role cannot be set externally, while conveniently casting them to the type defined in the schema.

  • Changesets can validate data—for example, the length or the format of a field—on the fly, but validations that depend on data integrity are left to the database in the shape of constraints.

  • Changesets make our code easy to understand and implement because they can compose easily, allowing us to specify each part of a change with a function.

In short, Ecto cleanly encapsulates the concepts of change, and we benefit tremendously as users. Now that we can generate slugs for the videos, let’s make sure we use them in our links.

Extending Phoenix with Protocols

To use slugs when linking to the video page, let’s open up the lib/rumbl_web/templates/video/index.html.eex template and see how links are generated:

 <%=​ link ​"​​Watch"​, ​to:​ Routes.watch_path(@conn, ​:show​, video),
 class:​ ​"​​button"​ ​%>

RumblWeb.Router generates the Routes.watch_path. It’s available to our controller code because of the Routes alias in lib/rumbl_web.ex. When we pass a struct like video to watch_path, Phoenix automatically extracts its ID to use in the returned URL. To use slugs, we could simply change the route call to the following:

 Routes.watch_path(@conn, ​:show​, ​"​​#{​video.id​}​​-​​#{​video.slug​}​​"​)

This approach is easy to plug in, but it has a giant flaw. It’s brittle because it’s not DRY. Each place we need a link, we need to build the URL with the id and slug fields. If we forget to use the same structure in any of the future watch_path calls, we’ll end up linking to the wrong URL. There’s a better way.

We can customize how Phoenix generates URLs for the videos. Phoenix and Elixir have the perfect solution for this. Phoenix knows to use the id field in a Video struct because Phoenix defines a protocol, called Phoenix.Param. By default, this protocol extracts the id of the struct, if one exists.

However, since Phoenix.Param is an Elixir protocol, we can customize it for any data type in the language, including the ones we define ourselves. Let’s do so for the Video struct. Create a new lib/rumbl_web/param.ex file with the following content:

 defimpl​ Phoenix.Param, ​for:​ Rumbl.Multimedia.Video ​do
 def​ to_param(%{​slug:​ slug, ​id:​ id}) ​do
 "​​#{​id​}​​-​​#{​slug​}​​"
 end
 end

We’re implementing the Phoenix.Param protocol for the Rumbl.Multimedia.Video struct. The protocol requires us to implement the to_param function, which receives the video struct itself. We pattern-match on the video slug and ID and use it to build a string as our slug. Our param.ex file will serve as a home for other protocol implementations as we continue building our application.

The beauty behind Elixir protocols is that we can implement them for any data structure, anywhere, any time. We can place our implementation in the same file as the video definition, or anywhere else that makes sense. Because we can get Phoenix parameters without changing Phoenix or the Video module itself, we get a much cleaner polymorphism than we would otherwise.

Let’s give this a try in IEx:

 iex>​ video = %Rumbl.Multimedia.Video{​id:​ 1, ​slug:​ ​"​​hello"​}
 %Rumbl.Multimedia.Video{id: 1, slug: "hello", ...}
 
 iex>​ alias RumblWeb.Router.Helpers, ​as:​ Routes
 
 iex>​ Routes.watch_path(%URI{}, ​:show​, video)
 "/watch/1-hello"

We build a video and then call Routes.watch_path, passing our video as an argument. The new path uses both the id and slug fields. Note that we give the URI struct to watch_path instead of the usual connection. The URI struct is part of Elixir’s standard library, and all route functions accept it as their first argument. This convenience is particularly useful when building URLs outside of your web request. Think emails, messages, and so on. Let’s play a bit with this idea:

 iex>​ url = URI.parse(​"​​http://example.com/prefix"​)
 %URI{...}
 
 iex>​ Routes.watch_path(url, ​:show​, video)
 "/prefix/watch/1-hello"
 
 iex>​ Routes.watch_url(url, ​:show​, video)
 "http://example.com/prefix/watch/1-hello"

You can also ask your endpoint to return the struct_url, based on the values you’ve defined in your configuration files:

 iex>​ url = RumblWeb.Endpoint.struct_url()
 %URI{...}
 iex>​ Routes.watch_url(url, ​:show​, video)
 "http://localhost:4000/watch/1-hello"

With Phoenix.Param properly implemented for our videos, we can try it out. Start your server back up with mix phx.server, then access “My Videos” and click the “Watch” link for any existing video.

Well, that was less than ideal. You see a page with an error that looks something like this:

 value `"13-hello-world"` in `where` cannot be cast to type :id in query:
 
 from v in Rumbl.Multimedia.Video,
  where: v.id == ^"13-",
  select: v

Primary keys in Ecto have a default type of :id. For now, we can consider :id to be an :integer. When a new request goes to /watch/13-hello-world, the router matches 13-hello-world as the id parameter and sends it to the controller. In the controller, we try to make a query by using the id, and it complains. Let’s look at the source of the problem:

 def​ show(conn, %{​"​​id"​ => id}) ​do
  video = Multimedia.get_video!(id)
  render(conn, ​"​​show.html"​, ​video:​ video)
 end

WatchController.show is taking the id parameter and passing it to our Multimedia.get_video context function. Let’s continue digging and open up lib/rumbl/multimedia.ex:

 def​ get_video!(id), ​do​: Repo.get!(Video, id)

That’s the problem. We’re doing a Repo.get! by using the id field, which is now a string instead of an integer. Let’s fix that now.

Before doing a database query comparing against the id column, we need to cast 13-hello-world to an integer.

Extending Schemas with Ecto Types

Sometimes, the basic type information in our schemas isn’t enough. In those cases, we’d like to improve our schemas with types that have a knowledge of Ecto. For example, we might want to associate some behavior to our id fields. A custom type allows us to do that. Let’s implement one and place it in lib/rumbl/multimedia/permalink.ex. Our new behaviour, meaning an implementation of our interface, looks like this:

 defmodule​ Rumbl.Multimedia.Permalink ​do
  @behaviour Ecto.Type
 
 def​ type, ​do​: ​:id
 
 def​ cast(binary) ​when​ is_binary(binary) ​do
 case​ Integer.parse(binary) ​do
  {int, _} ​when​ int > 0 -> {​:ok​, int}
  _ -> ​:error
 end
 end
 
 def​ cast(integer) ​when​ is_integer(integer) ​do
  {​:ok​, integer}
 end
 
 def​ cast(_) ​do
 :error
 end
 
 def​ dump(integer) ​when​ is_integer(integer) ​do
  {​:ok​, integer}
 end
 
 def​ load(integer) ​when​ is_integer(integer) ​do
  {​:ok​, integer}
 end
 end

Behaviour or Behavior?

images/aside-icons/info.png

The Elixir and Erlang documentation use the European spelling of “behaviour” so we’ll stick with that one when we refer to the actual Elixir concept. We’ll use the “ior” spelling when we are talking about “behavior,” the word.

Rumbl.Multimedia.Permalink is a custom type defined according to the Ecto.Type behaviour. It expects us to define four functions:

type

Returns the underlying Ecto type. In this case, we’re building on top of :id.

cast

Called when external data is passed into Ecto. It’s invoked when values in queries are interpolated or also by the cast function in changesets.

dump

Invoked when data is sent to the database.

load

Invoked when data is loaded from the database.

By design, the cast function often processes end-user input. We should be both lenient and careful when we parse it. For our slug—that means for binaries—we call Integer.parse to extract only the leading integer. On the other hand, dump and load handle the struct-to-database conversion. We can expect to work only with integers at this point because cast does the dirty work of sanitizing our input. Successful casts return integers. dump and load return :ok tuples with integers or :error.

Let’s give our custom type a try with iex -S mix. Since we changed code in lib, you need to restart any running session.

 iex>​ alias Rumbl.Multimedia.Permalink, ​as:​ P
 iex>​ P.cast(​"​​1"​)
 {:ok, 1}
 iex>​ P.cast(1)
 {:ok, 1}

Integers and strings work as usual. That’s great. Let’s try something more complex:

 iex>​ P.cast(​"​​13-hello-world"​)
 {:ok, 13}

Perfect. An integer followed by a string, such as the ones we build with our protocol, works just as it should. Let’s try something that should break, like a string followed by an integer:

 iex>​ P.cast(​"​​hello-world-13"​)
 :error

And it breaks, just as it should. As long as the string starts with a positive integer, we’re good to go. The last step is to tell Ecto to use our custom type for the id field in lib/rumbl/multimedia/video.ex:

 @primary_key {​:id​, Rumbl.Multimedia.Permalink, ​autogenerate:​ true}
 schema ​"​​videos"​ ​do

Because Ecto automatically defines the id field for us, we can customize the primary key with the @primary_key module attribute. Just give it a tuple with the primary key name (:id). We tacked on the autogenerate: true option because our database autogenerates id values.

And that’s that. Access the page once again, and it should load successfully. By implementing a protocol and defining a custom type, we made Phoenix work exactly how we wanted without tightly coupling it to our implementation. Ecto types go way beyond simple casting, though. We’ve already seen the community handle field encryption, data uploading, and more, all neatly wrapped and contained inside an Ecto type.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.114.28