NAME

Object::Configure - Runtime Configuration for an Object

VERSION

0.24

DESCRIPTION

Object::Configure injects runtime configuration and logging into Perl class constructors. It is a thin layer on top of Config::Abstraction (reads config files and environment variables) and Log::Abstraction (logging).

Call configure($class, \%params) at the start of your new() method. It:

The module also provides optional hot-reload support: a background process watches config files and sends SIGUSR1 to trigger an in-place update of registered objects without restarting the application.

Hot reload is not supported on Windows (SIGUSR1 does not exist there).

SYNOPSIS

Example 1: Add configurable logging to your own class

package My::Module;
use Object::Configure;

sub new {
    my ($class, %args) = @_;

    # configure() reads config files + env vars, sets up a logger,
    # and returns a hashref ready to bless.
    my $params = Object::Configure::configure($class, \%args);

    return bless $params, $class;
}

sub do_work {
    my $self = shift;
    $self->{logger}->info('Starting do_work');
    # ...
}

# Usage -- reads ~/.conf/my-module.yml if it exists:
my $obj = My::Module->new(config_file => '/etc/myapp/my-module.yml');
$obj->do_work;

Example 2: Configure a third-party class you cannot modify

use Object::Configure;

# Wrap LWP::UserAgent so it reads its settings from a YAML file.
my $ua = Object::Configure::instantiate(
    class       => 'LWP::UserAgent',
    config_file => '/etc/myapp/lwp.yml',
    timeout     => 30,      # fallback if the file has no timeout key
);

$ua->get('https://example.com');

Example 3: Multi-level inheritance -- config files merge automatically

# ~/.conf/my-base-class.yml
# My__Base__Class:
#   timeout: 30
#   retries: 3

# ~/.conf/my-child-class.yml
# My__Child__Class:
#   timeout: 60   # overrides base; retries:3 is inherited

package My::Child::Class;
our @ISA = ('My::Base::Class');
use Object::Configure;

sub new {
    my ($class, %args) = @_;
    # Walks @ISA, merges base config then child config.
    # Result: timeout=60, retries=3
    my $params = Object::Configure::configure($class, \%args);
    return bless $params, $class;
}

Example 4: Hot reload -- objects update when the config file changes

package My::Service;
use Object::Configure;

sub new {
    my ($class, %args) = @_;
    my $params = Object::Configure::configure($class, \%args);
    my $self   = bless $params, $class;

    # Register so reload_config() updates $self in-place on file change.
    Object::Configure::register_object($class, $self)
        if $params->{_config_file};

    return $self;
}

package main;

my $svc = My::Service->new(config_file => '/etc/myapp/service.yml');

# Fork a background watcher; it sends SIGUSR1 when the file changes.
Object::Configure::enable_hot_reload(
    interval => 5,
    callback => sub { print "Config reloaded at ", scalar localtime, "\n" },
);

while (1) { sleep 1 }          # event loop

Object::Configure::disable_hot_reload();    # clean shutdown

Example 5: Override settings with environment variables (no file needed)

# Shell:
#   export My__Module__log_level=debug
#   export My__Module__timeout=120

package My::Module;
use Object::Configure;

sub new {
    my ($class, %args) = @_;
    # configure() picks up My__Module__* env vars automatically.
    my $params = Object::Configure::configure($class, \%args);
    return bless $params, $class;
}

my $obj = My::Module->new;
# $obj->{log_level} eq 'debug'   $obj->{timeout} == 120

CONFIGURATION

Config file naming

The config file name is derived from the class name by lowercasing it and replacing :: with hyphens (-):

My::Parent::Class  =>  my-parent-class.yml

The file is searched for in the directory of the config_file argument, then in any directories listed in config_dirs.

Section key naming inside the config file

Inside the YAML (or JSON or conf) file, the section key uses double underscores in place of :::

# my-parent-class.yml
---
My__Parent__Class:
  timeout: 30
  retries: 3

Configuration resolution order

The following sources are merged from lowest to highest priority. A value from a higher-priority source always wins over a lower-priority one.

UNIVERSAL configuration

If you create universal.yml in your config directory with a UNIVERSAL: section, those settings apply to every class that uses Object::Configure unless a more-specific source overrides them:

# ~/.conf/universal.yml
---
UNIVERSAL:
  timeout: 30
  logger:
    level: warning

Logging

configure() always sets $params->{logger} to a Log::Abstraction instance. You can control it by passing a logger key:

# Arrayref: messages are captured into @log (protected from merge override)
configure($class, { logger => \@log });

# Hashref: options forwarded to Log::Abstraction::new()
configure($class, { logger => { level => 'debug', file => '/var/log/app.log' } });

# String 'NULL': disables all logging
configure($class, { logger => 'NULL' });

# Existing Log::Abstraction object: used as-is
configure($class, { logger => $my_logger });

Note: only arrayref loggers are stashed before the config merge and are guaranteed to survive it. 'NULL' and blessed logger objects can be overridden by a UNIVERSAL: section in universal.yml. See "COMMON PITFALLS".

Environment variable format

Environment variable names are constructed as:

ClassName__key

where :: in the class name is replaced by two underscores.

export My__Module__timeout=60
export My__Module__logger__level=debug

HOT RELOAD

Hot reload lets you edit a config file and have all live objects update themselves without restarting the program.

How it works

Private keys (those whose names start with _) are never overwritten during reload, so internal bookkeeping is safe.

Hot reload is not supported on Windows.

COMMON PITFALLS

Only arrayref loggers survive the config merge

If you pass logger => 'NULL' or logger => $existing_logger, the UNIVERSAL: section in universal.yml (or a site-local config file) can silently override your logger during the config merge.

Only arrayref loggers are stashed before the merge and are guaranteed to survive:

# Protected -- will NOT be overridden by universal.yml
configure($class, { logger => \@captured });

# NOT protected -- universal.yml can override these
configure($class, { logger => 'NULL' });
configure($class, { logger => $existing_obj });

Caller-supplied keys are the LOWEST priority

Despite being called "defaults", keys you pass directly to configure() are overridden by config files and environment variables:

# My__Module__timeout=60 is set in the shell.
# $params->{timeout} will be 60, not 30.
configure('My::Module', { timeout => 30 });

Use caller-supplied keys only as a last-resort fallback.

register_object() pushes; it does not replace

Calling register_object() twice for the same class registers two entries. Both objects receive updates on every reload. The second call does not remove the first.

Object::Configure::register_object('My::Class', $obj_a);
Object::Configure::register_object('My::Class', $obj_b);
# reload_config() now updates BOTH $obj_a and $obj_b

Private keys are never updated on hot reload

Any key whose name begins with _ is skipped during reload_config(). A config key named _my_setting in your YAML file will be ignored at reload time.

The 'class' key appears on objects created by instantiate()

instantiate() intentionally leaves the class key in the hashref passed to $class->new() as a debugging aid. Your object will have a class attribute set to the class name:

my $obj = Object::Configure::instantiate(class => 'My::Thing', ...);
print $obj->{class};    # prints 'My::Thing'

Memoization caches are not invalidated during a run

_get_inheritance_chain() and _find_class_config_file() cache their results for the lifetime of the process. If you alter @ISA or add config files after the first configure() call for a given class, the cache returns stale results.

disable_hot_reload() blocks for up to five seconds

It waits for the watcher process to exit (SIGTERM first, then SIGKILL). Do not call it from inside a signal handler or a timing-sensitive loop.

config_file must be developer-controlled

The config_file path is validated against directory traversal (../) but is not otherwise restricted. It must always be a developer-supplied path, never raw user input.

SUBROUTINES/METHODS

configure($class, \%params)

Merge configuration for $class from all available sources and return a hashref ready to pass to bless. This is the core function; call it at the start of your new() method.

Arguments

Returns

A hashref containing all merged configuration keys plus:

Error messages

API Specification

Input

schema => {
    class => {
        type        => 'string',
        required    => 1,
        description => 'Fully-qualified Perl class name',
        pattern     => qr/\A[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/,
    },
    params => {
        type        => 'hashref',
        optional    => 1,
        default     => {},
        description => 'Caller-supplied defaults (lowest priority)',
        schema => {
            config_file => {
                type        => 'string',
                optional    => 1,
                description => 'Primary config file path',
            },
            config_dirs => {
                type        => 'arrayref',
                optional    => 1,
                description => 'Extra directories to search for config files',
            },
            logger => {
                type        => [qw(undef string arrayref hashref object)],
                optional    => 1,
                description => 'Logger spec -- see CONFIGURATION/Logging',
            },
            carp_on_warn => {
                type        => 'boolean',
                optional    => 1,
                default     => 0,
                description => 'Use Carp::carp for logger warnings',
            },
            croak_on_error => {
                type        => 'boolean',
                optional    => 1,
                default     => 1,
                description => 'Use Carp::croak for logger errors',
            },
        },
    },
}

Output

type        => 'hashref',
description => 'Merged configuration hashref, ready to bless',
schema => {
    logger => {
        type        => [qw(object string)],
        description => 'Log::Abstraction instance or the string "NULL"',
    },
    _config_file => {
        type        => 'string',
        optional    => 1,
        description => 'Path of the primary config file that was loaded',
    },
    _config_files => {
        type        => 'arrayref',
        optional    => 1,
        description => 'All config file paths that were loaded, in load order',
    },
}

instantiate(%params)

Configure and instantiate a third-party class without modifying the class itself.

instantiate is a convenience wrapper: it calls configure, passes the merged hashref to $class->new(...), and optionally registers the result for hot reload. Use it when you need runtime configuration for a class whose source you cannot change.

Arguments

Takes a flat hash (not a hashref). Recognized keys:

Returns

A blessed object of type $class.

Note: The returned object's hash will contain a class key holding the class name. This is intentional -- it is left in the hash as a debugging aid so you can always see which class an object came from. Do not depend on its absence.

Side Effects

Error messages

Same as configure(). In addition:

Usage Example

use Object::Configure;

my $ua = Object::Configure::instantiate(
    class       => 'LWP::UserAgent',
    config_file => 'lwp.yml',
    config_dirs => ['/etc/myapp'],
    timeout     => 30,
);

API Specification

Input

schema => {
    class => {
        type        => 'string',
        required    => 1,
        description => 'Fully-qualified class name; must respond to new(hashref)',
    },
    # all other keys forwarded to configure()
}

Output

type        => 'object',
description => 'Blessed instance of $class, with class key present in hash',
notes       => 'class key intentionally left in hash as a debugging aid',

HOT RELOAD FEATURES

enable_hot_reload(%opts)

Fork a background watcher that sends SIGUSR1 to the parent whenever a tracked configuration file changes on disk. Objects registered via register_object() then have their configuration reloaded automatically.

Unix only. On Windows this function is a silent no-op (SIGUSR1 does not exist).

Arguments

Takes a flat hash. All keys are optional.

Returns

The PID of the watcher child process (integer > 0), or undef/empty if hot reload was already active (idempotent: a second call returns immediately without forking again).

Side Effects

Error messages

Usage Example

Object::Configure::enable_hot_reload(
    interval => 5,
    callback => sub { warn "Config reloaded at " . localtime . "\n" },
);

while (1) { sleep 1 }  # watcher runs in the background

API Specification

Input

schema => {
    interval => {
        type        => 'integer',
        optional    => 1,
        default     => 10,
        minimum     => 1,
        description => 'Poll interval in seconds',
    },
    callback => {
        type        => 'coderef',
        optional    => 1,
        description => 'Called in parent after each reload',
    },
}

Output

type        => [qw(integer undef)],
description => 'PID of watcher child; undef/empty if already active',
condition   => 'value > 0 when defined',

enable_hot_reload : Interval x Callback -> PID | empty

Pre:
  interval >= 1   (enforced: negative/zero replaced with DEFAULT_INTERVAL)
  _config_watchers = {}

Post:
  _config_watchers.pid = result
  _config_watchers.callback = callback
  (forall t: t mod interval = 0 =>
      (exists f in _config_file_stats: mtime(f) changed =>
          send_signal(SIGUSR1, parent_pid)))

disable_hot_reload()

Stop the background watcher and clear hot-reload state.

Safe to call when hot reload is not active (no-op). After this call, configuration files are no longer monitored and %_config_watchers is empty.

Arguments

None.

Returns

Nothing (void).

Side Effects

Blocking: this function may take up to five seconds if the watcher ignores SIGTERM.

API Specification

Input

schema => {}   # no arguments

Output

type => 'void'

reload_config()

Immediately reload configuration from disk for every registered object.

Normally called automatically by the SIGUSR1 handler. You may call it manually to force a reload (e.g., in tests or on a custom signal).

Arguments

None.

Returns

An integer >= 0: the count of objects whose configuration was successfully reloaded.

Side Effects

API Specification

Input

schema => {}   # no arguments

Output

type        => 'integer',
description => 'Count of objects successfully reloaded',
condition   => 'value >= 0',

register_object($class, $obj)

Register a blessed object so it receives configuration updates when files change.

Push semantics: each call appends a new entry to the registry for $class. It does not replace a previous entry. Multiple objects of the same class are all tracked and all reloaded.

Arguments

Returns

Nothing (void).

Side Effects

Error messages

Usage Example

package My::Module;
use Object::Configure;

sub new {
    my ($class, %args) = @_;
    my $params = Object::Configure::configure($class, \%args);
    my $self   = bless $params, $class;
    Object::Configure::register_object($class, $self)
        if $self->{_config_file};
    return $self;
}

API Specification

Input

schema => {
    class => {
        type        => 'string',
        required    => 1,
        description => 'Class name for registry key',
    },
    obj => {
        type        => 'object',
        required    => 1,
        description => 'Blessed object to register; unblessed refs are rejected',
        blessed     => 1,
    },
}

Output

type => 'void'

restore_signal_handlers()

Restore $SIG{USR1} to the handler that was in place before register_object() installed the hot-reload handler, and clear $_original_usr1_handler.

Safe to call even when Object::Configure never installed a handler (no-op). On Windows this function has no effect (SIGUSR1 does not exist there).

Arguments

None.

Returns

Nothing (void).

Side Effects

API Specification

Input

schema => {}   # no arguments

Output

type => 'void'

get_signal_handler_info()

Return a snapshot of the current signal-handler and hot-reload state. This is a debugging aid; normal application code does not need to call it.

Arguments

None.

Returns

A hashref with these keys:

Usage Example

use Object::Configure;
use Data::Dumper;

Object::Configure::enable_hot_reload();
print Dumper(Object::Configure::get_signal_handler_info());
# {
#   original_usr1    => 'DEFAULT',
#   current_usr1     => sub { ... },
#   hot_reload_active => 1,
#   watcher_pid       => 12345,
# }

API Specification

Input

schema => {}   # no arguments

Output

type        => 'hashref',
description => 'Snapshot of signal-handler and watcher state',
schema => {
    original_usr1     => { type => [qw(coderef string undef)] },
    current_usr1      => { type => [qw(coderef string undef)] },
    hot_reload_active => { type => 'boolean'                  },
    watcher_pid       => { type => [qw(integer undef)]        },
}

SEE ALSO

LIMITATIONS

Formal Specification

configure

configure: Class x Params -> ConfigHash

Given:
- C: set of all class names
- P: set of all parameter hashes
- F: set of all file paths
- H: set of all configuration hashes

State:
- ConfigFiles: F -> H (maps file paths to configuration content)
- EnvVars: String -> String (environment variables)
- InheritanceChain: C -> seq C (ordered sequence of ancestor classes)

Pre-condition:
forall class in C, params in P:
    class != empty
    (params.config_file != empty =>
        (exists dir in params.config_dirs: readable(dir/params.config_file))
        OR readable(params.config_file))

Post-condition:
forall result in H:
    result = params
             (+) (merge f in InheritanceConfigFiles(class): ConfigFiles(f))
             (+) (merge v in RelevantEnvVars(class): v)
    result.logger in Log::Abstraction
    (forall k in dom params:
        (params(k) in CodeRef OR blessed(params(k))) => result(k) = params(k))

where (+) denotes hash merge with right-precedence

instantiate

instantiate: Params -> Object

Given:
- P: set of all parameter hashes
- C: set of all class names
- O: set of all objects

Pre-condition:
forall params in P:
    params.class in C
    params.class.can('new')

Post-condition:
forall result in O:
    exists config in H:
        config = configure(params.class, params)
        result = params.class.new(config)
        blessed(result) = params.class
        (config._config_file != empty =>
            result in _object_registry(params.class))

enable_hot_reload

enable_hot_reload: Interval x Callback -> PID

Given:
- I: set of positive integers (intervals in seconds)
- CB: set of code references
- PID: set of process identifiers

State:
- _config_watchers: {pid: PID, callback: CB}
- _config_file_stats: F -> Stat

Pre-condition:
forall interval in I, callback in CB union {empty}:
    interval >= 1
    _config_watchers = empty
    OS != 'MSWin32'

Post-condition:
forall result in PID:
    result > 0
    _config_watchers.pid = result
    _config_watchers.callback = callback
    (forall t in Time:
        (t mod interval = 0) =>
            (exists f in dom _config_file_stats:
                mtime(f) > _config_file_stats(f).mtime =>
                    send_signal(SIGUSR1, parent_process)))

disable_hot_reload

disable_hot_reload: () -> ()

State:
- _config_watchers: {pid: PID, callback: CB}

Pre-condition:
true

Post-condition:
_config_watchers = empty
(forall p in PID:
    p = _config_watchers.pid@pre =>
        NOT alive(p))

reload_config

reload_config: () -> N

State:
- _object_registry: C -> seq ObjectRef
- ConfigFiles: F -> H

Pre-condition:
true

Post-condition:
forall result in N:
    result = |{obj in flatten(ran _object_registry) |
               obj != empty
               obj._config_file in dom ConfigFiles}|
    (forall obj in flatten(ran _object_registry):
        obj != empty AND obj._config_file in dom ConfigFiles =>
            (forall k in dom ConfigFiles(obj._config_file):
                k NOT in PrivateKeys =>
                    obj(k)@post = ConfigFiles(obj._config_file)(k)))

where PrivateKeys = {k | k starts with '_'}

register_object

register_object: C x O -> ()

Given:
- C: set of class names
- O: set of blessed objects
- OR: C -> seq WeakRef(O) (object registry)

State:
- _object_registry: OR
- _original_usr1_handler: SignalHandler union {empty}
- $SIG{USR1}: SignalHandler

Pre-condition:
forall class in C, obj in O:
    class != empty
    obj != empty
    blessed(obj) != empty

Post-condition:
forall class in C, obj in O:
    exists ref in _object_registry(class):
        weak(ref) = obj
    (_original_usr1_handler = empty@pre =>
        (_original_usr1_handler@post = $SIG{USR1}@pre
         $SIG{USR1}@post = reload_config_handler))

restore_signal_handlers

restore_signal_handlers: () -> ()

State:
- _original_usr1_handler: SignalHandler union {empty}
- $SIG{USR1}: SignalHandler

Pre-condition:
true

Post-condition:
$SIG{USR1}@post = _original_usr1_handler@pre
_original_usr1_handler@post = empty

get_signal_handler_info

get_signal_handler_info: () -> InfoHash

Given:
- IH: set of all info hashes

State:
- _original_usr1_handler: SignalHandler union {empty}
- $SIG{USR1}: SignalHandler union {empty}
- _config_watchers: {pid: PID, callback: CB}

Pre-condition:
true

Post-condition:
forall result in IH:
    result.original_usr1 = _original_usr1_handler
    result.current_usr1 = $SIG{USR1}
    result.hot_reload_active = (_original_usr1_handler != empty)
    result.watcher_pid = _config_watchers.pid

SUPPORT

Please report bugs and feature requests at:

You will be notified automatically of progress on your report.

perldoc Object::Configure

LICENCE AND COPYRIGHT

Copyright 2025-2026 Nigel Horne.

Usage is subject to GPL2 licence terms. If you use it, please let me know.