Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laminas Config Aggregator Laravel Package

laminas/laminas-config-aggregator

A Laminas package that aggregates configuration from multiple sources—PHP files, arrays, and modules—into a single merged config, with support for caching, glob patterns, and post-processing hooks for use in Laminas and Mezzio applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: the README's Usage block and the docs site (docs.laminas.dev/laminas-config-aggregator), specifically the providers and post-processors pages. That's ~95% of the surface area. The API is three constructor arguments and one method:

$aggregator = new ConfigAggregator(
    $providers = [],          // list of provider callables/objects
    $cachedConfigFile = null, // path to a generated PHP cache file
    $postProcessors = []      // callables applied to the merged array
);

$config = $aggregator->getMergedConfig(); // plain array

Install:

composer require laminas/laminas-config-aggregator
# only if you need JSON/YAML/XML/INI sources:
composer require laminas/laminas-config

Where to look first: the PhpFileProvider class. It globs PHP files and includes each one, so every file must return an array. That is the entire mental model — no container, no facades, no service providers, no env(). You get an array back and you decide what to do with it.

Your first real use case as a Laravel dev: you're integrating a Laminas/Mezzio package (or a legacy Zend Framework module) into a Laravel app and you don't want to hand-translate its *.global.php config tree. Aggregate it, then push the result into Laravel's config repository:

// app/Providers/LaminasConfigBridgeProvider.php
public function register(): void
{
    $aggregator = new ConfigAggregator([
        new PhpFileProvider(base_path('vendor/acme/legacy-package/config/autoload/*.global.php')),
        new PhpFileProvider(base_path('config/laminas/*.global.php')), // your overrides go last
    ]);

    config($aggregator->getMergedConfig());
}

From there everything behaves like normal Laravel config: config('db.dsn'), Config::get(), and php artisan config:cache will serialize the result.

The other legitimate use case is the inverse: you're authoring a framework-agnostic package or CLI tool and need a config loader that isn't tied to Laravel. Same library, no Laravel in the loop.

Implementation Patterns

Ordering is precedence. Providers are merged in array order, later wins. Establish a convention and stick to it: base/package config first, environment config next, local/override files last. glob() returns alphabetically sorted results, so *.global.php gives you deterministic ordering for free — name files accordingly (00-base.global.php, 90-overrides.global.php).

Environment layering without a framework. Build the provider list conditionally at bootstrap rather than inventing env-var interpolation:

$providers = [
    new PhpFileProvider(__DIR__ . '/../config/autoload/*.global.php'),
];

if (getenv('APP_ENV') === 'production') {
    $providers[] = new PhpFileProvider(__DIR__ . '/../config/autoload/*.production.php');
}

$aggregator = new ConfigAggregator($providers);

Post-processors for cross-references. This is the feature that earns the package its keep. A post-processor is any callable (or invokable class) receiving the merged array and returning an array. Use it to resolve placeholders à la Symfony's ParameterBag — e.g. %db.host% — so a config file can reference values defined in another file instead of duplicating them:

$aggregator = new ConfigAggregator(
    $providers,
    null,
    [
        static function (array $config): array {
            $config['db']['dsn'] = sprintf(
                'mysql:host=%s;dbname=%s',
                $config['db']['host'],
                $config['db']['name']
            );
            return $config;
        },
    ]
);

Do all derived-value logic here, not in the config files themselves — config files stay data, post-processors stay logic, and the merge stays predictable.

Cache in production, never in local dev. The second constructor argument is a cache file path. When it exists, it is loaded instead of running providers:

$cacheFile = getenv('APP_ENV') === 'production'
    ? __DIR__ . '/../storage/framework/cache/laminas-config-' . getenv('APP_ENV') . '.php'
    : null; // null = always rebuild

$aggregator = new ConfigAggregator($providers, $cacheFile, $postProcessors);

Pair this with a deploy step that deletes the file before rebuilding (rm -f storage/framework/cache/laminas-config-*.php in your deploy script), and add a small artisan command that instantiates the aggregator once as a cache warmer. Because your cache filename includes the environment, a config:cache artifact from staging can't be picked up in production.

Map into Laravel config deliberately. Don't dump the aggregated array at the top level and hope keys don't collide. Namespace it, or map specific known keys:

$merged = $aggregator->getMergedConfig();

config([
    'legacy.db'     => $merged['db'] ?? [],
    'legacy.cache'  => $merged['cache_storage'] ?? null,
    'legacy.redis'  => $merged['redis'] ?? [],
]);

Multiple formats when you must. LaminasConfigProvider (new LaminasConfigProvider(__DIR__ . '/config/*.{json,yaml,php}')) pulls in laminas/laminas-config, and per-format readers may need their own dependencies (YAML needs symfony/yaml). Only reach for this if config genuinely lives outside PHP; otherwise PhpFileProvider keeps the dependency tree and the failure modes smaller.

Providers are deduplicated. Since 1.16.0 identical providers are collapsed, so registering the same glob twice (common when a package's bootstrap and your app's bootstrap both add it) won't double-contribute. Still, keep the list explicit — don't rely on dedupe to sort out conflicting globs.

Gotchas and Tips

Numeric keys append, they don't replace. This is the bug that will bite you. Merging uses Laminas\Stdlib\ArrayUtils::merge semantics: nested string keys merge recursively with later precedence, but integer-keyed entries are appended when the key already exists. A list of middleware, allowed hosts, or paths in two files produces a concatenated list, not an override — and duplicate entries can silently accumulate. For lists you intend to replace, merge in a post-processor with array_values(array_unique(...)) or array_merge explicitly.

Numeric-keyed arrays behave differently from Laravel. Laravel doesn't merge config files at all — a later config/foo.php wholesale replaces an earlier one, and package config is merged with array_merge only during mergeConfigFrom. Don't carry that intuition over; test list-heavy config explicitly.

Cache files are not invalidated. There's no mtime check, no hash of sources, no dependency graph. If the file is there, it wins. Consequences: a stale cache after editing config, and a cache generated in one environment leaking into another if you use a single fixed filename. Namespace the filename by environment and delete it in your deploy pipeline.

Always use absolute paths in globs. new PhpFileProvider('config/*.php') resolves against getcwd(), which is not what you think under php artisan, under a queue worker, or under php -S. Use base_path(), __DIR__, or dirname(__DIR__). This is the single most common "works in tinker, fails in the queue" failure.

PhpFileProvider executes the file. It's an include, not a parse — side effects run, and return must produce an array. A file that returns a Closure, or null from a missing return, blows up at merge time. If getMergedConfig() throws, bisect by instantiating providers one at a time:

foreach ($providers as $p) {
    dump((new ConfigAggregator([$p]))->getMergedConfig());
}

No env() equivalent — and you shouldn't add one via getenv() inside config files. Doing so hides environment coupling in leaf files and breaks the whole point of a deterministic, cacheable config artifact. Read env in the bootstrap and select providers based on it; keep the config files pure data. If you need real interpolation, use a post-processor, not a getenv() call per file.

Cache file permissions. Set ConfigAggregator::CACHE_FILEMODE (octal, e.g. 0600) if the default is too permissive for your host — config caches often contain credentials. Also verify the target directory is writable by the deploy user, not just the web user; a cache that fails to write in prod silently costs you a full provider scan on every request.

Classes are soft @final. The library marks its classes as soft-final via Psalm. Extending ConfigAggregator to preprocess the provider list (added in 1.13.0) is supported, but prefer composing a factory that returns a configured instance. It's a 400-line library — if you find yourself subclassing heavily, you want a different tool.

Post-processor order matters as much as provider order. They chain, each receiving the previous output. Keep them idempotent and side-effect free; a post-processor that mutates by reference rather than returning a new array makes the chain impossible to reason about and will differ between the cached and uncached path.

Don't fight Laravel's config system with it. If the config is yours and lives in config/*.php, you gain nothing by adding this package — you already have merge-on-load and config:cache. Adopt it when the config isn't yours (vendor package, legacy app), when you need multiple formats from one entry point, or when the consumer is a CLI/worker outside the HTTP kernel. Otherwise you're maintaining two caching layers that can disagree with each other.

Low bus factor. 43 stars, zero dependents. The code is small, stable, and read in ten minutes — budget the time to read ConfigAggregator and PhpFileProvider in full rather than relying on the changelog or ecosystem answers. That read is cheaper than a single debugging session against a stale cache file.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin