This guide covers reordering, mix tasks, and testing strategies.
Reordering Media
Media items within a collection have an order_column that controls their
display order. PhxMediaLibrary provides two functions for managing order.
Reorder by ID List
Set the exact order for all items in a collection by passing an ordered list of IDs. This runs in a single database transaction:
# Set explicit order: id3 first, id1 second, id2 third
{:ok, count} = PhxMediaLibrary.reorder(post, :images, [id3, id1, id2])IDs not present in the collection are silently ignored. Items whose IDs are not in the provided list keep their current order but are shifted after the explicitly ordered items.
Move a Single Item
Move one media item to a specific 1-based position within its collection:
{:ok, updated_media} = PhxMediaLibrary.move_to(media, 1) # move to first
{:ok, updated_media} = PhxMediaLibrary.move_to(media, 3) # move to thirdThe position is clamped to the valid range — passing a position larger than the collection size moves the item to the end.
Drag-and-Drop Reordering
A common pattern for LiveView drag-and-drop:
def handle_event("reorder", %{"ids" => ordered_ids}, socket) do
case PhxMediaLibrary.reorder(socket.assigns.post, :images, ordered_ids) do
{:ok, _count} ->
{:noreply, stream_existing_media(socket, :media, socket.assigns.post, :images)}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Reorder failed: #{inspect(reason)}")}
end
endDeleting Media
# Delete a single media item (removes files from storage too)
PhxMediaLibrary.delete(media)
# Clear all media in a collection (batch-optimized, single DELETE query)
{:ok, count} = PhxMediaLibrary.clear_collection(post, :images)
# Clear all media for a model (batch-optimized)
{:ok, count} = PhxMediaLibrary.clear_media(post)Both clear_collection/2 and clear_media/1 delete files from storage for
each item, then remove all matching database records in a single DELETE query
(avoiding N+1).
Mix Tasks
Install
Generate the media table migration with all required fields:
mix phx_media_library.install
Regenerate Conversions
Regenerate derived images after changing conversion definitions:
mix phx_media_library.regenerate --conversion thumb
mix phx_media_library.regenerate --collection images
mix phx_media_library.regenerate --dry-run
Regenerate Responsive Images
mix phx_media_library.regenerate_responsive
mix phx_media_library.regenerate_responsive --collection images
Clean Orphaned Files
Remove files from storage that no longer have a corresponding database record:
# Dry run — see what would be deleted
mix phx_media_library.clean
# Actually delete
mix phx_media_library.clean --force
Generate Custom Migration
Add custom fields to the media table:
mix phx_media_library.gen.migration add_blurhash_field
Oban Setup for Async Conversions
By default, PhxMediaLibrary uses Task.Supervisor for background conversion
processing. This is fine for development but doesn't survive restarts or
support retries. For production, use the Oban adapter.
1. Add Oban to your dependencies
# mix.exs
{:oban, "~> 2.18"}2. Configure Oban with a :media queue
# config/config.exs
config :my_app, Oban,
repo: MyApp.Repo,
queues: [default: 10, media: 10]Adjust concurrency based on your server capacity:
# Low-traffic app
queues: [media: 5]
# High-traffic app with beefy servers
queues: [media: 20]3. Tell PhxMediaLibrary to use the Oban adapter
# config/config.exs
config :phx_media_library,
async_processor: PhxMediaLibrary.AsyncProcessor.Oban4. Start Oban in your supervision tree
# lib/my_app/application.ex
children = [
MyApp.Repo,
{Oban, Application.fetch_env!(:my_app, Oban)},
# ...
]How It Works
When media is uploaded and conversions are defined, PhxMediaLibrary enqueues an
Oban job with the media ID, conversion names, and the mediable_type. The
PhxMediaLibrary.Workers.ProcessConversions worker then:
- Looks up the media record from the database
- Discovers the originating Ecto schema module from the
mediable_type - Retrieves the full
Conversiondefinitions (width, height, quality, fit, etc.) - Processes each conversion and updates the media record
Retry Behaviour
The worker is configured with max_attempts: 3. Failed jobs use Oban's default
exponential backoff. You can monitor failed jobs via Oban.Web or your own
telemetry handlers.
Synchronous Processing
If you need conversions to complete immediately (e.g. generating a thumbnail
before returning a response), call process_sync/2 directly:
PhxMediaLibrary.AsyncProcessor.Oban.process_sync(media, conversions)Testing
In-Memory Storage
For tests, use the in-memory storage adapter to avoid filesystem side effects:
# config/test.exs
config :phx_media_library,
repo: MyApp.Repo,
disks: [
local: [
adapter: PhxMediaLibrary.Storage.Memory
]
]Start the memory storage agent in your test_helper.exs:
{:ok, _} = PhxMediaLibrary.Storage.Memory.start_link()Test Fixtures
Create test fixture files in test/support/fixtures/ and use them in your
tests:
defmodule MyApp.MediaFixtures do
@fixtures_path Path.join([__DIR__, "..", "support", "fixtures"])
def fixture_path(filename), do: Path.join(@fixtures_path, filename)
def sample_image, do: fixture_path("sample.jpg")
def sample_pdf, do: fixture_path("sample.pdf")
endTesting Media Addition
defmodule MyApp.PostMediaTest do
use MyApp.DataCase
alias PhxMediaLibrary
test "adds an image to a post" do
post = insert(:post)
assert {:ok, media} =
post
|> PhxMediaLibrary.add(fixture_path("sample.jpg"))
|> PhxMediaLibrary.to_collection(:images)
assert media.collection_name == "images"
assert media.mime_type == "image/jpeg"
assert media.size > 0
end
test "rejects files exceeding max_size" do
post = insert(:post)
assert {:error, {:file_too_large, _actual, _max}} =
post
|> PhxMediaLibrary.add(fixture_path("large_file.bin"))
|> PhxMediaLibrary.to_collection(:uploads)
end
test "rejects invalid MIME types" do
post = insert(:post)
assert {:error, :invalid_mime_type} =
post
|> PhxMediaLibrary.add(fixture_path("sample.exe"))
|> PhxMediaLibrary.to_collection(:images)
end
endTesting with Telemetry
Attach telemetry handlers in tests to verify events are emitted:
test "emits telemetry on media add" do
ref = make_ref()
test_pid = self()
:telemetry.attach(
"test-#{inspect(ref)}",
[:phx_media_library, :add, :stop],
fn _event, measurements, metadata, _config ->
send(test_pid, {:telemetry, measurements, metadata})
end,
nil
)
post = insert(:post)
{:ok, _media} =
post
|> PhxMediaLibrary.add(fixture_path("sample.jpg"))
|> PhxMediaLibrary.to_collection(:images)
assert_receive {:telemetry, %{duration: duration}, %{collection: :images}}
assert duration > 0
:telemetry.detach("test-#{inspect(ref)}")
endTemporary Directory Pattern
For tests that need real files on disk, use the tmp_dir ExUnit tag:
@tag :tmp_dir
test "stores file to disk", %{tmp_dir: tmp_dir} do
# Configure storage to use the temp directory
# Files are automatically cleaned up after the test
end