NAME
Mojolicious::Plugin::Fondation - Hierarchical plugin loader with configuration priority and resource sharing
VERSION
version 0.01
SYNOPSIS
# In your Mojolicious application (myapp.pl or startup)
plugin 'Fondation' => {
dependencies => [
{ 'Fondation::User' => { title => 'Custom User' } },
'Fondation::Authorization',
],
};
# In a plugin (e.g. lib/Mojolicious/Plugin/Fondation/User.pm)
package Mojolicious::Plugin::Fondation::User;
use Mojo::Base 'Mojolicious::Plugin', -signatures;
sub fondation_meta {
return {
dependencies => [],
defaults => {
title => 'User Management',
items_per_page => 20,
},
};
}
sub register ($self, $app, $conf) {
$app->routes->get('/users' => sub ($c) { $c->render(text => "Users!") });
return $self; # Important for finalyze actions
}
sub fondation_finalyze ($self, $app, $long_name) {
# Optional: code to run after all plugins are loaded
$self->log->debug("User plugin fully loaded");
}
1;
DESCRIPTION
Fondation attempts to provide a foundation for building websites from pre-built bricks. It is a plugin loader for Mojolicious that enables hierarchical, recursive plugin loading with automatic configuration merging, resource sharing (controllers, templates), and post-load actions.
It is designed for modular applications where multiple plugins contribute routes, templates, and behavior, while avoiding duplicate loads and respecting configuration priorities.
Key features:
Recursive plugin loading via
dependenciesConfiguration cascade: direct > app config > plugin defaults
Automatic discovery and registration of
share/templatesApplication-level
share/templateshave priorityExtensible post-load actions (Templates, Controllers, custom)
Deferred initialization via
fondation_finalyzeContextual logging via
$self->log(Mojo::Log context)
NAME
Mojolicious::Plugin::Fondation - Hierarchical plugin loader with configuration priority and resource sharing
LOADING ORDER
1. load_plugin_recursive
└─ Fondation -> dependencies recursively
2. run_post_load_actions
└─ For every plugin (load order), execute all actions
(Templates, Controllers, custom)
└─ App-level share/templates added with highest priority
3. run_finalyze
└─ For every plugin (load order), call fondation_finalyze()
QUICK START
# myapp.pl
use Mojolicious::Lite;
plugin 'Fondation' => {
dependencies => [
'Fondation::User',
'Fondation::Authorization',
],
};
# Fondation loads Authorization, which depends on Role + Permission.
# Result: Role -> Permission -> Authorization -> User
# All share/templates and controllers are auto-discovered.
With a config file
# myapp.conf
{
'Fondation' => {
dependencies => ['Fondation::User', 'Fondation::Authorization'],
},
'Fondation::User' => {
title => 'User Management',
},
}
# myapp.pl
plugin 'Config';
plugin 'Fondation'; # reads dependencies from config
CONFIGURATION PRIORITIES
Configuration for each plugin is merged in this order (highest priority first):
- 1. Direct configuration (passed in
dependenciesarray) -
plugin 'Fondation' => { dependencies => [ { 'Fondation::User' => { title => 'Direct override' } }, ], }; - 2. Application configuration file (e.g. myapp.conf)
-
{ 'Fondation::User' => { title => 'From config file', } } - 3. Plugin defaults (returned by
fondation_meta) -
sub fondation_meta { return { defaults => { title => 'Default value' }, }; }
RESOURCE DIRECTORIES (share/)
Fondation automatically handles shared resources from plugins:
share/templates-> pushed to$app-renderer->paths>share/public-> pushed to$app-static->paths>
The application's own share/templates directory (if it exists) is added with unshift and therefore has **highest priority** (application templates override plugin templates).
ACTIONS (POST-LOAD PROCESSING)
Actions are classes that run after **all** plugins are loaded, iterating over each plugin in load order. They perform specific initialization tasks such as registering templates or controllers. Actions are configurable via the actions key in Fondation's configuration.
Default actions:
Templates,Controllers,StaticCustom actions: you can write your own action class by subclassing
Mojolicious::Plugin::Fondation::Action::Baseor declare them viafondation_meta -provides_actions>.
Configuration
Set the actions key in the Fondation configuration. You rarely need this -- the defaults ['Templates', 'Controllers'] are used, plus any plugin-provided actions from fondation_meta - provides_actions>.
plugin 'Fondation' => {
actions => ['Templates'], # keep Templates, drop Controllers
dependencies => ['MyPlugin'], # MyAction auto-added if declared
};
Configuration merge
Each plugin receives a merged configuration built from three sources, combined with Hash::Merge. The cascade priority is:
- 1. Direct config -- passed inline in the
dependencieslist (highest priority) - 2. App config -- from
myapp.confor$app->configvia Mojolicious::Plugin::Config - 3. Plugin defaults -- from
fondation_meta -defaults> (lowest priority)
The merge rules are:
Scalars -- overwrite. The highest-priority non-empty value wins.
Hashes -- merged recursively. Keys present at multiple levels are resolved by priority; keys present at only one level survive untouched.
Arrays -- concatenated. All values from all levels are kept, ordered by priority: direct elements first, then app config, then defaults.
Example: a plugin declares allowed_roles => ['user'] in its defaults, the app config adds allowed_roles => ['editor'], and a direct dependency passes allowed_roles => ['admin']. The merged result is ['admin', 'editor', 'user'] -- all three roles are available, with the highest-priority one first.
The dependencies key is not special -- it follows the same array concatenation rules. This means an app config can add extra dependencies without repeating those already declared.
Default Actions
Templates(Mojolicious::Plugin::Fondation::Action::Templates)Adds the plugin's
share/templatesdirectory to the application's template search paths.Controllers(Mojolicious::Plugin::Fondation::Action::Controllers)Automatically discovers controller modules under the plugin's namespace (
Plugin::Name::Controller::*) and adds that namespace to$app->routes->namespaces, making the controllers available to the router.Static(Mojolicious::Plugin::Fondation::Action::Static)Adds the plugin's
share/publicdirectory to the application's static file search paths and stores its location in the registry aspublic_dirfor other consumers.
Writing a Custom Action
Create a new class that inherits from Mojolicious::Plugin::Fondation::Action::Base and implements the after_load method:
package My::Action;
use Mojo::Base 'Mojolicious::Plugin::Fondation::Action::Base', -signatures;
sub after_load ($self, $long_name, $conf, $share_dir) {
my $manager = $self->manager;
my $app = $manager->app;
$self->log->debug("MyAction executed");
}
1;
To auto-enable your action from a plugin, declare it in fondation_meta:
sub fondation_meta {
return {
provides_actions => ['MyAction'],
};
}
The action class must live at ${PluginNS}::Action::MyAction. Fondation resolves it automatically.
If the action name does not start with Mojolicious::, Fondation will prepend Mojolicious::Plugin::Fondation::Action:: to it as a fallback.
THE fondation_meta METHOD
All Fondation-aware plugins should define a class method fondation_meta:
sub fondation_meta {
return {
dependencies => ['XXX', 'YYY'], # loaded before this plugin
provides_actions => ['MyAction'], # optional custom action
defaults => {
title => 'Default Title',
},
};
}
dependencies-> array of plugin names to load firstprovides_actions-> optional array of custom action short namesdefaults-> fallback configuration values
This method is called before register to collect metadata without instantiating the plugin.
THE return $self REQUIREMENT
To fully participate in Fondation's features (especially fondation_finalyze), a plugin **must return $self** from its register method:
sub register ($self, $app, $conf) {
# Your code...
return $self;
}
If you don't return $self:
Instance is not stored in the registry
fondation_finalyzecannot be calledPlugin is still loaded and actions run, but finalyze features are skipped
CREATING A FONDATION-AWARE PLUGIN
1. **File structure**
lib/Mojolicious/Plugin/Fondation/MyPlugin.pm
share/templates/myplugin/ (optional)
2. **Minimal code**
package Mojolicious::Plugin::Fondation::MyPlugin;
use Mojo::Base 'Mojolicious::Plugin', -signatures;
sub fondation_meta {
return {
dependencies => [],
defaults => { enable_feature => 1 },
};
}
sub register ($self, $app, $conf) {
$app->routes->get('/myplugin' => sub ($c) {
$c->render(text => "Hello from MyPlugin!");
});
return $self; # Required for finalyze
}
sub fondation_finalyze ($self, $app, $long_name) {
$self->log->debug("MyPlugin fully initialized");
}
1;
3. **Loading**
In your Fondation config:
dependencies => [
'MyPlugin',
# or { 'MyPlugin' => { enable_feature => 0 } }
]
4. **Testing**
- Check registry: C<< $app->manager->registry >>
- Check logs for contextual debug (C<< [$short] message >>)
- Verify templates are added
- Use C<< /fondation/debug/registry >> route (development mode) to inspect
HELPERS
Fondation registers the following helpers:
manager-- returns the Mojolicious::Plugin::Fondation::Manager instancehas_helper($name)-- checks whether a helper is registeredl($key)-- fallback identity function (overridden by I18N plugins)check_group/check_perm-- permissive fallbacks (allow all)notify_user-- no-op that returns a resolved Promiserender_zone($zone)-- renders HTML zones from all pluginsrender_zone_js($zone)-- includes JS zones from all plugins
ZONES
Zones let plugins inject HTML or JavaScript fragments into named zones defined by the application layout. Each plugin can provide zone templates under share/templates/zones/.
Directory structure
share/templates/zones/
html/header/ -> picked up by render_zone('header')
greeting.html.ep
js/footer/ -> picked up by render_zone_js('footer')
init.js.ep
How it works
render_zone($zone) iterates over every loaded plugin in load order and renders all .html.ep templates found in share/templates/zones/html/$zone/. The output is concatenated.
render_zone_js($zone) does the same for .js.ep files, but reads the raw content instead of rendering through the template engine.
Usage in templates
%= render_zone 'header'
%= render_zone_js 'footer'
AUTHOR
Daniel Brosseau <dab@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Daniel Brosseau.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.