PhoenixKit.ModuleRegistry (phoenix_kit v1.7.187)

Copy Markdown View Source

Runtime registry of all PhoenixKit modules (internal and external).

External modules are auto-discovered from beam files via PhoenixKit.ModuleDiscovery. Any dep that depends on :phoenix_kit and uses use PhoenixKit.Module is found automatically — no config line needed.

External Module Registration

External hex packages are auto-discovered. Just add the dep:

{:phoenix_kit_hello_world, "~> 0.1.0"}

For explicit registration (backwards compatible):

config :phoenix_kit, :modules, [PhoenixKitHelloWorld]

Runtime Registration

PhoenixKit.ModuleRegistry.register(MyModule)
PhoenixKit.ModuleRegistry.unregister(MyModule)

Query API

ModuleRegistry.all_modules()           # All registered module atoms
ModuleRegistry.enabled_modules()       # Only currently enabled
ModuleRegistry.all_admin_tabs()        # Collect admin tabs from all modules
ModuleRegistry.all_settings_tabs()     # Collect settings tabs
ModuleRegistry.all_user_dashboard_tabs() # Collect user dashboard tabs
ModuleRegistry.all_children()          # Collect supervisor child specs
ModuleRegistry.all_permission_metadata() # Collect permission metadata
ModuleRegistry.feature_enabled_checks()  # Build {mod, :enabled?} map
ModuleRegistry.get_by_key("tickets")   # Find module by key
ModuleRegistry.get_module_key_for_namespace("PhoenixKitTickets")
                                       # Resolve top-level namespace → key

Summary

Functions

Collect all admin tabs from all registered modules.

Collect all supervisor child specs from all registered modules.

Returns all feature module key strings from registered modules.

Returns all registered module atoms.

Collect permission metadata from all registered modules.

Collect top-level route path segments reserved by all installed modules.

Collect route modules from all registered modules.

Collect all settings tabs from all registered modules.

Collect sitemap source modules contributed by all enabled modules.

Collect all user dashboard tabs from all registered modules.

Returns a specification to start this module under a supervisor.

Returns dependency warnings for the Modules page.

Returns all registered modules that are currently enabled.

Build a feature_enabled_checks map from registered modules.

Find a registered module by its key string.

Find a registered module's key by matching a top-level Elixir namespace.

Check if the registry has been initialized.

Returns the full catalog of known external PhoenixKit packages.

Collect modules that have versioned migrations.

Returns known external PhoenixKit packages that are not currently installed.

Returns permission descriptions map from registered modules.

Returns permission icons map from registered modules.

Returns permission labels map from registered modules.

Register a module that implements PhoenixKit.Module behaviour.

Rescan beam files and absorb any external modules that weren't known at registry init.

Run every enabled module's migrate_legacy/0 callback.

Collect supervisor child specs from the static module list.

Returns the sub-permissions declared by registered modules, keyed by the parent module key. Each entry carries the COMPOSED dotted key ("calendar.view_others") plus its label and description.

Unregister a module.

Whether a module base permission key is well-formed (lowercase, no dots, within length). A dotted base key would collide with composed sub-keys, so the permission plumbing ignores base keys that fail this check.

Functions

all_admin_tabs()

@spec all_admin_tabs() :: [PhoenixKit.Dashboard.Tab.t()]

Collect all admin tabs from all registered modules.

Note: iterates all modules and calls admin_tabs/0 on each call. For cached access in rendering paths, use PhoenixKit.Dashboard.Registry.get_admin_tabs/0.

all_children()

@spec all_children() :: [Supervisor.child_spec()]

Collect all supervisor child specs from all registered modules.

all_feature_keys()

@spec all_feature_keys() :: [String.t()]

Returns all feature module key strings from registered modules.

all_modules()

@spec all_modules() :: [module()]

Returns all registered module atoms.

all_permission_metadata()

@spec all_permission_metadata() :: [PhoenixKit.Module.permission_meta()]

Collect permission metadata from all registered modules.

all_reserved_route_prefixes()

@spec all_reserved_route_prefixes() :: [String.t()]

Collect top-level route path segments reserved by all installed modules.

Each entry is a literal path segment (no slashes, e.g. "legal") a module owns for its own LiveViews/controllers. A dispatcher that routes requests based on database-driven path segments (e.g. Publishing's group catch-all) should treat any segment in this list as NOT its own, even if it happens to have matching data, so it doesn't swallow a route another module owns. Declaring a prefix is passive — it changes nothing unless a dispatcher actively consults this function.

Iterates all_modules/0, not enabled_modules/0 (unlike all_sitemap_sources/0): the route this guards against is normally compiled into the host's router (e.g. live "/legal", LegalLive), which exists independent of the owning module's runtime enabled/disabled Settings toggle. Gating on enabled_modules/0 would drop the reservation the moment a module is disabled while its data (and the host's compiled route) still exist, reopening the exact hijack this is meant to prevent for as long as the module stays disabled.

Each module's returned segments are trimmed of leading/trailing slashes and non-list/non-string returns are dropped, so a malformed reserved_route_prefixes/0 implementation (e.g. returning "/legal" or a bare string instead of a list) can't silently defeat the reservation or crash this per-request-hot-path aggregation.

all_route_modules()

@spec all_route_modules() :: [module()]

Collect route modules from all registered modules.

all_settings_tabs()

@spec all_settings_tabs() :: [PhoenixKit.Dashboard.Tab.t()]

Collect all settings tabs from all registered modules.

all_sitemap_sources()

@spec all_sitemap_sources() :: [module()]

Collect sitemap source modules contributed by all enabled modules.

Each entry implements PhoenixKit.Modules.Sitemap.Sources.Source. The sitemap Generator appends these to its base source list (deduplicated) so module-owned content appears in the sitemap with no host config.

Iterates enabled_modules/0, so a disabled module contributes nothing — even in flat-sitemap mode, where the Generator deliberately force-collects every source and bypasses each source's own enabled?/0 (force: true, see Sources.Source.safe_collect/2 and Generator flat-mode generation). Gating at the module level here keeps the "disabled module emits no URLs" guarantee in both modes. A contributed source's own enabled?/0 remains a secondary gate in index mode (see Generator.generate_module/2).

all_user_dashboard_tabs()

@spec all_user_dashboard_tabs() :: [PhoenixKit.Dashboard.Tab.t()]

Collect all user dashboard tabs from all registered modules.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

dependency_warnings()

@spec dependency_warnings() :: [map()]

Returns dependency warnings for the Modules page.

Each warning is a map: %{module: module(), module_name: String.t(), requires_key: String.t()}

Called on Modules page render — computes live (not hot path).

enabled_modules()

@spec enabled_modules() :: [module()]

Returns all registered modules that are currently enabled.

feature_enabled_checks()

@spec feature_enabled_checks() :: %{required(String.t()) => {module(), atom()}}

Build a feature_enabled_checks map from registered modules.

Returns %{"storage" => {PhoenixKit.Modules.Storage, :enabled?}, ...}

get_by_key(key)

@spec get_by_key(String.t()) :: module() | nil

Find a registered module by its key string.

get_module_key_for_namespace(top_namespace)

@spec get_module_key_for_namespace(String.t()) :: String.t() | nil

Find a registered module's key by matching a top-level Elixir namespace.

Used by the admin permission layer to resolve a plugin LiveView's namespace (e.g. "PhoenixKitEntities" from PhoenixKitEntities.Web.Entities) to the plugin's permission key (e.g. "entities").

Returns the key string or nil when no registered module matches.

initialized?()

@spec initialized?() :: boolean()

Check if the registry has been initialized.

known_external_packages()

@spec known_external_packages() :: [map()]

Returns the full catalog of known external PhoenixKit packages.

Fetches live from Hex.pm with a 10-minute in-memory cache. Merged with any entries from config :phoenix_kit, extra_known_packages: [...]. Config entries take precedence over Hex entries when the package field collides.

modules_with_migrations()

@spec modules_with_migrations() :: [{String.t(), module()}]

Collect modules that have versioned migrations.

Returns a list of {module_name, migration_module} tuples for all registered modules that implement migration_module/0 and return a non-nil value.

not_installed_packages()

@spec not_installed_packages() :: [map()]

Returns known external PhoenixKit packages that are not currently installed.

Used by the admin Modules page to inform users about available packages they can add as dependencies.

permission_descriptions()

@spec permission_descriptions() :: %{required(String.t()) => String.t()}

Returns permission descriptions map from registered modules.

permission_icons()

@spec permission_icons() :: %{required(String.t()) => String.t()}

Returns permission icons map from registered modules.

permission_labels()

@spec permission_labels() :: %{required(String.t()) => String.t()}

Returns permission labels map from registered modules.

register(module)

@spec register(module()) :: :ok

Register a module that implements PhoenixKit.Module behaviour.

rescan()

@spec rescan() :: {:ok, [module()]}

Rescan beam files and absorb any external modules that weren't known at registry init.

Returns {:ok, new_modules} — the freshly-absorbed module atoms (often [] after the first call). Safe to call repeatedly.

Intended use: parent app calls this from Application.start/2 after Supervisor.start_link/2 so late-loading :phoenix_kit_<x> deps whose beams are only available after PhoenixKit's own supervision tree is up get picked up deterministically — without timer-based polling. mix phoenix_kit.install and mix phoenix_kit.update wire this call in automatically; existing apps can call it manually.

Also useful in dev for hot-reload recovery after recompiling a module package, or in tests that dynamically load fixture modules.

run_all_legacy_migrations()

@spec run_all_legacy_migrations() :: %{required(module()) => :ok | {:error, term()}}

Run every enabled module's migrate_legacy/0 callback.

Iterates registered modules, calls migrate_legacy/0 on each that implements it, swallows per-module errors so the host-app boot can't be taken down by a flaky migration. Each module's implementation is expected to be idempotent (safe to re-run on every boot).

Activity logging happens inside each module's migrate_legacy/0 — this orchestrator only logs the per-module pass/fail outcome to the Logger, not to Activity.

Designed to be called once from a host app's Application.start/2 after the Repo and supervision tree are up:

def start(_type, _args) do
  children = [...]
  result = Supervisor.start_link(children, opts)
  PhoenixKit.ModuleRegistry.run_all_legacy_migrations()
  result
end

Returns a summary map: %{module_atom => :ok | {:error, term()}}.

start_link(opts \\ [])

static_children()

@spec static_children() :: [Supervisor.child_spec()]

Collect supervisor child specs from the static module list.

This does NOT require the GenServer to be running, making it safe to call from the PhoenixKit.Supervisor init (before the registry starts).

sub_permission_map()

@spec sub_permission_map() :: %{required(String.t()) => [map()]}

Returns the sub-permissions declared by registered modules, keyed by the parent module key. Each entry carries the COMPOSED dotted key ("calendar.view_others") plus its label and description.

Malformed declarations (bad key format, missing fields, key over 50 chars, duplicate keys within one module) are dropped with a logged warning rather than raising — a broken module must not take down permission resolution for everyone else.

unregister(module)

@spec unregister(module()) :: :ok

Unregister a module.

valid_base_key?(key)

@spec valid_base_key?(String.t()) :: boolean()

Whether a module base permission key is well-formed (lowercase, no dots, within length). A dotted base key would collide with composed sub-keys, so the permission plumbing ignores base keys that fail this check.