PhoenixKitComments.ResourceHandler behaviour (PhoenixKitComments v0.4.0)

Copy Markdown View Source

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
end

What 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.

Someone liked a comment. Fires only on an actual change of state.

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

reaction()

@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.

resource_type()

@type resource_type() :: String.t()

The host's own type key for a commentable record, e.g. "order".

resource_uuid()

@type resource_uuid() :: Ecto.UUID.t()

The record's uuid.

Callbacks

on_comment_created(resource_type, resource_uuid, t)

(optional)
@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.

on_comment_deleted(resource_type, resource_uuid, t)

(optional)
@callback on_comment_deleted(
  resource_type(),
  resource_uuid(),
  PhoenixKitComments.Comment.t()
) :: any()

A comment was soft-deleted.

on_comment_disliked(resource_type, resource_uuid, reaction)

(optional)
@callback on_comment_disliked(resource_type(), resource_uuid(), reaction()) :: any()

Someone disliked a comment.

on_comment_liked(resource_type, resource_uuid, reaction)

(optional)
@callback on_comment_liked(resource_type(), resource_uuid(), reaction()) :: any()

Someone liked a comment. Fires only on an actual change of state.

on_comment_undisliked(resource_type, resource_uuid, reaction)

(optional)
@callback on_comment_undisliked(resource_type(), resource_uuid(), reaction()) :: any()

Someone withdrew a dislike.

on_comment_unliked(resource_type, resource_uuid, reaction)

(optional)
@callback on_comment_unliked(resource_type(), resource_uuid(), reaction()) :: any()

Someone withdrew a like.

resolve_comment_resources(list)

(optional)
@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

callbacks()

@spec callbacks() :: [{atom(), arity()}]

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.

event_callbacks()

@spec event_callbacks() :: [{atom(), arity()}]

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.