NAME

Mojolicious::Plugin::Fondation - Hierarchical plugin loader with configuration priority and resource sharing

VERSION

version 0.02

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:

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

Each plugin receives a merged configuration built from three sources, combined with Hash::Merge. The cascade priority is:

1. Direct configuration (passed in dependencies array)

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' },
    };
}

The merge rules are:

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.

RESOURCE DIRECTORIES (share/)

Fondation automatically handles shared resources from plugins:

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.

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
};

Default Actions

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.

PLUGIN REGISTRY

After loading, every plugin is recorded in the registry — a hashref keyed by fully-qualified plugin name. Each entry stores:

The Manager owns the registry. Mojolicious::Plugin::Fondation::API exposes it for read-only access (same hashref, no copy). Post-load actions (Templates, Controllers, Static) iterate over it, as do zone helpers (render_zone, render_zone_js) and the finalyze phase.

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 } }
    ]

The fondation_meta contract

All Fondation-aware plugins must 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',
        },
    };
}

This method is called before register to collect metadata without instantiating the plugin. It is the cornerstone of Fondation's composition model: every plugin declares what it needs and what it provides.

Why return $self is required

To fully participate in Fondation's features (especially fondation_finalyze), a plugin must return $self from its register method.

If you don't return $self:

HELPERS

Fondation registers the following helpers:

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'

ABOUT THIS PROJECT

Fondation and its plugin ecosystem were developed with significant assistance from an AI coding agent.

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.