The contract a host module implements to hook into comments.
Register one per resource_type:
config :phoenix_kit, :comment_resource_handlers, %{
"post" => MyApp.Posts
}Every callback is optional — implement only the events you care about — so a handler is free to be a two-line module that reacts to one thing.
Why adopt the behaviour
Dispatch is by function_exported?/3, and it always will be: a handler is
named in host config, may live in another application, and may legitimately
implement none of these. That is a good property, and it has one bad
consequence — a callback this package never finds is indistinguishable from
one you chose not to write. Misname on_comment_create (no d), take two
arguments instead of three, and nothing happens: no error, no warning, no
notification, and nothing to grep for. The host discovers it when someone
reports that notifications stopped.
@behaviour PhoenixKitComments.ResourceHandler turns that into a compile
warning at the point of the mistake. It is not required and changes nothing
at runtime; it only makes the failure loud. Handlers registered today keep
working untouched.
defmodule MyApp.Posts do
@behaviour PhoenixKitComments.ResourceHandler
@impl true
def on_comment_created(_type, post_uuid, comment) do
MyApp.Posts.notify_author(post_uuid, comment)
end
endWhat is passed
The event callbacks all receive the resource_type and resource_uuid the
comment was filed against, so one handler can serve several types.
Reaction callbacks take a map rather than the comment alone because the
comment row carries its author, not the person reacting — :liker_uuid
is the only place that appears.
Paths are RAW
resolve_comment_resources/1 returns path WITHOUT the PhoenixKit url
prefix — the renderer applies Routes.path/1 once. A handler that
pre-prefixes gets a doubled prefix.
What is NOT decided here
Reaction callbacks fire only when the state actually changed
({:ok, :liked}), never on an :already_liked no-op. Self-action skipping
— not notifying someone who liked their own comment — is left to the
handler, which is the only party that knows whether that matters.
A callback that raises is caught and logged; it cannot fail the comment that triggered it.
Summary
Types
A reaction event. The comment plus who reacted — the row itself only knows its author.
The host's own type key for a commentable record, e.g. "order".
The record's uuid.
Callbacks
A comment was created. Check comment.parent_uuid to tell a reply apart.
A comment was soft-deleted.
Someone disliked a comment.
Someone liked a comment. Fires only on an actual change of state.
Someone withdrew a dislike.
Someone withdrew a like.
Titles and RAW deep-links for the given uuids.
Functions
The callbacks this package dispatches, as {name, arity}.
The comment-event callbacks, i.e. everything except
resolve_comment_resources/1.
Types
@type reaction() :: %{ comment: PhoenixKitComments.Comment.t(), liker_uuid: Ecto.UUID.t() }
A reaction event. The comment plus who reacted — the row itself only knows its author.
@type resource_type() :: String.t()
The host's own type key for a commentable record, e.g. "order".
@type resource_uuid() :: Ecto.UUID.t()
The record's uuid.
Callbacks
@callback on_comment_created( resource_type(), resource_uuid(), PhoenixKitComments.Comment.t() ) :: any()
A comment was created. Check comment.parent_uuid to tell a reply apart.
@callback on_comment_deleted( resource_type(), resource_uuid(), PhoenixKitComments.Comment.t() ) :: any()
A comment was soft-deleted.
@callback on_comment_disliked(resource_type(), resource_uuid(), reaction()) :: any()
Someone disliked a comment.
@callback on_comment_liked(resource_type(), resource_uuid(), reaction()) :: any()
Someone liked a comment. Fires only on an actual change of state.
@callback on_comment_undisliked(resource_type(), resource_uuid(), reaction()) :: any()
Someone withdrew a dislike.
@callback on_comment_unliked(resource_type(), resource_uuid(), reaction()) :: any()
Someone withdrew a like.
@callback resolve_comment_resources([resource_uuid()]) :: %{ optional(resource_uuid()) => map() }
Titles and RAW deep-links for the given uuids.
Returns %{uuid => %{title: String.t(), path: String.t()}}. May also
carry :full_title (tooltip) and :thumb_url (chip thumbnail); a uuid
the handler does not recognise is simply absent from the map.
Functions
The callbacks this package dispatches, as {name, arity}.
Exposed so the dispatcher and the contract can be checked against each other — a callback added to one and not the other is exactly the silent mismatch this module exists to prevent.
The comment-event callbacks, i.e. everything except
resolve_comment_resources/1.
These are the ones the dispatcher fires by name with three arguments;
resolve_comment_resources/1 is a lookup the renderer calls directly and
answers a different question, so checks over "the events" have to exclude
it rather than assume the contract is uniform.