PhxMediaLibrary.HasMedia (PhxMediaLibrary v0.6.1)

Copy Markdown View Source

Adds media association capabilities to an Ecto schema.

Usage

There are two styles for defining collections and conversions: the declarative DSL (recommended) and the function-based approach.

Nest convert calls inside collection ... do ... end blocks so it's immediately clear which conversions apply to which collections. Collections without image content (like :documents) omit the do block — no conversions will run for those uploads.

defmodule MyApp.Post do
  use Ecto.Schema
  use PhxMediaLibrary.HasMedia

  schema "posts" do
    field :title, :string
    has_media()
    timestamps()
  end

  media_collections do
    collection :images, disk: :s3, max_files: 20 do
      convert :thumb, width: 150, height: 150, fit: :cover
      convert :preview, width: 800, quality: 85
      convert :banner, width: 1200, height: 400, fit: :crop
    end

    collection :documents, accepts: ~w(application/pdf)

    collection :avatar, single_file: true, fallback_url: "/images/default.png" do
      convert :thumb, width: 150, height: 150, fit: :cover
    end
  end
end

Declarative DSL — flat style

Define collections and conversions in separate blocks. Always use the :collections option to scope conversions explicitly — without it, a conversion runs for every collection (including non-image ones like documents, which will cause processing errors):

defmodule MyApp.Post do
  use Ecto.Schema
  use PhxMediaLibrary.HasMedia

  schema "posts" do
    field :title, :string
    has_media()
    timestamps()
  end

  media_collections do
    collection :images, disk: :s3, max_files: 20
    collection :documents, accepts: ~w(application/pdf)
    collection :avatar, single_file: true, fallback_url: "/images/default.png"
  end

  media_conversions do
    convert :thumb, width: 150, height: 150, fit: :cover, collections: [:images, :avatar]
    convert :preview, width: 800, quality: 85, collections: [:images]
    convert :banner, width: 1200, height: 400, fit: :crop, collections: [:images]
  end
end

Function-based approach

defmodule MyApp.Post do
  use Ecto.Schema
  use PhxMediaLibrary.HasMedia

  schema "posts" do
    field :title, :string
    has_media()
    timestamps()
  end

  def media_collections do
    [
      collection(:images, disk: :local),
      collection(:documents, accepts: ~w(application/pdf)),
      collection(:avatar, single_file: true)
    ]
  end

  def media_conversions do
    [
      # Always scope conversions to specific collections
      conversion(:thumb, width: 150, height: 150, fit: :cover, collections: [:images, :avatar]),
      conversion(:preview, width: 800, quality: 85, collections: [:images])
    ]
  end
end

How has_media() Works

The has_media() macro injects a polymorphic has_many :media association into your schema. This enables standard Ecto preloading:

post = Repo.preload(post, :media)
post.media  #=> [%PhxMediaLibrary.Media{}, ...]

Under the hood, the association uses mediable_id as the foreign key and a :where clause to scope results by mediable_type (derived from the schema's table name).

You can also define collection-scoped associations:

schema "posts" do
  has_media()
  has_media(:images)
  has_media(:documents)
end

Which enables:

post = Repo.preload(post, [:media, :images, :documents])
post.images  #=> only media in the "images" collection

Overriding the Media Type

By default the polymorphic mediable_type is the Ecto table name (e.g. "posts" for schema "posts"). You can override it at the module level:

use PhxMediaLibrary.HasMedia, media_type: "blog_posts"

Or override the __media_type__/0 function:

def __media_type__, do: "blog_posts"

Collection Options

  • :disk - Storage disk to use (default: configured default)
  • :accepts - List of accepted MIME types
  • :single_file - Only keep one file in collection (default: false)
  • :max_files - Maximum number of files to keep
  • :max_size - Maximum file size in bytes (e.g. 10_000_000 for 10 MB)
  • :fallback_url - URL to use when collection is empty
  • :verify_content_type - Verify file content matches declared MIME type (default: true)

Conversion Options

  • :width - Target width in pixels
  • :height - Target height in pixels
  • :fit - How to fit the image (:contain, :cover, :fill, :crop)
  • :quality - JPEG/WebP quality (1-100)
  • :format - Output format (:jpg, :png, :webp)
  • :collections - Only apply to specific collections

Summary

Functions

Define a media collection.

Define a media conversion (function-style).

Define a media conversion (DSL-style).

Declares that this schema has media associations.

Functions

collection(name, opts \\ [])

Define a media collection.

Can be used in two ways:

  1. Inside a media_collections do ... end block (DSL style — the collection is automatically registered):

    media_collections do
      collection :images, disk: :s3
      collection :avatar, single_file: true
    end
  2. Inside a media_collections/0 function (function style — return a list of collections):

    def media_collections do
      [
        collection(:images, disk: :s3),
        collection(:avatar, single_file: true)
      ]
    end

Options

  • :disk - Storage disk to use
  • :accepts - List of accepted MIME types
  • :single_file - Only keep one file (default: false)
  • :max_files - Maximum number of files
  • :fallback_url - URL when collection is empty
  • :fallback_path - Path when collection is empty

conversion(name, opts)

Define a media conversion (function-style).

Used inside a media_conversions/0 function return list:

def media_conversions do
  [
    conversion(:thumb, width: 150, height: 150, fit: :cover),
    conversion(:preview, width: 800, quality: 85)
  ]
end

Options

  • :width - Target width in pixels
  • :height - Target height in pixels
  • :fit - Resize strategy (:contain, :cover, :fill, :crop)
  • :quality - Output quality for JPEG/WebP (1-100)
  • :format - Output format (:jpg, :png, :webp, :original)
  • :collections - Only apply to these collections (default: all)
  • :queued - Process asynchronously (default: true)

convert(name, opts)

Define a media conversion (DSL-style).

Used inside a media_conversions do ... end block:

media_conversions do
  convert :thumb, width: 150, height: 150, fit: :cover
  convert :preview, width: 800, quality: 85
  convert :banner, width: 1200, height: 400, fit: :crop, collections: [:images]
end

This is an alias for conversion/2 — both produce identical PhxMediaLibrary.Conversion structs. The convert name reads more naturally in the declarative DSL context.

Options

  • :width - Target width in pixels
  • :height - Target height in pixels
  • :fit - Resize strategy (:contain, :cover, :fill, :crop)
  • :quality - Output quality for JPEG/WebP (1-100)
  • :format - Output format (:jpg, :png, :webp, :original)
  • :collections - Only apply to these collections (default: all)
  • :queued - Process asynchronously (default: true)

has_media(collection_name \\ nil)

(macro)

Declares that this schema has media associations.

When called without arguments (has_media()), it injects a polymorphic has_many :media association that references all media items for the model regardless of collection.

When called with a collection name atom (has_media(:images)), it injects a collection-scoped has_many using that name. For example, has_media(:images) creates has_many :images scoped to the "images" collection.

This macro must be called inside the schema block, after the schema "table_name" declaration so that the table name is available for the polymorphic :where clause.

Examples

schema "posts" do
  field :title, :string

  # All media for this model
  has_media()

  # Collection-scoped associations (optional convenience)
  has_media(:images)
  has_media(:documents)
  has_media(:avatar)

  timestamps()
end

Then you can preload naturally:

post = Repo.preload(post, [:media, :images, :avatar])
post.media     #=> all media items
post.images    #=> only items in "images" collection
post.avatar    #=> only items in "avatar" collection