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

Config Laravel Package

symfony/config

The Symfony Config component helps you find, load, combine, autofill, and validate configuration values from any source—YAML, XML, INI files, or even a database—giving your PHP applications flexible, reliable configuration handling.

View on GitHub
Deep Wiki
Context7

Getting Started

For a Laravel developer, symfony/config is not a replacement for config/*.php, config() or php artisan config:cache. It is a schema engine: you describe the shape of a configuration array once, and the component merges multiple sources, applies defaults, normalizes legacy formats, validates types/ranges/enums, and throws a precise exception with the failing path. Day-to-day, that makes it a package-author tool. If you maintain a package with a published config file, or an app where config comes from files plus database/tenant overrides, this is the cleanest way in PHP to stop hand-rolling isset()/is_array() validation. If you only have a handful of scalar settings, Laravel's Validator or plain casts are cheaper.

Install and check what you already have:

composer require symfony/config
composer show symfony/config   # often already present transitively

The first classes to read are Symfony\Component\Config\Definition\Builder\TreeBuilder (schema definition), Symfony\Component\Config\Definition\Processor (merge + validate), InvalidConfigurationException (error handling), and ConfigCache/Resource\* only if you need your own cache invalidation beyond Laravel's.

The smallest useful setup is a ConfigurationInterface class plus a Processor call:

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

final class AuditConfiguration implements ConfigurationInterface
{
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $tree = new TreeBuilder('audit');
        $children = $tree->getRootNode()->children();

        $children->booleanNode('enabled')->defaultTrue();
        $children->enumNode('driver')->values(['log', 'db'])->defaultValue('log');
        $children->integerNode('retention_days')->min(1)->max(365)->defaultValue(30);

        $events = $children->arrayNode('events');
        $events->scalarPrototype()->end();
        $events->defaultValue(['created', 'updated']);

        return $tree;
    }
}
$config = (new Processor())->processConfiguration(
    new AuditConfiguration(),
    [require config_path('audit.php')]
);

Your first real use case: in a package service provider, register the validated config as a singleton and let the tree supply defaults, instead of duplicating defaults in the published file and in code:

public function register(): void
{
    $this->app->singleton(AuditConfig::class, function ($app) {
        return new AuditConfig(
            (new Processor())->processConfiguration(
                new AuditConfiguration(),
                [$app['config']->get('audit', [])]
            )
        );
    });
}

The component also ships a YamlReferenceDumper (what Symfony's config:dump-reference uses), which is a cheap way to generate docs for your package config from the same tree.

Implementation Patterns

Put defaults in the tree, not in the published config file. A published config/audit.php should contain only user-editable values. Every default, range and enum belongs in the tree so there is one source of truth. This also sidesteps Laravel's mergeConfigFrom() being shallow (it uses array_merge, not array_replace_recursive) — nested package defaults get wiped by a user's partial config. Let the tree fill the gaps after merging.

Use the Processor for precedence, not array_merge. Pass configs in ascending priority; later arrays win. Symfony deep-merges array nodes, which is usually what you want for options and usually not what you want for lists.

$config = (new Processor())->processConfiguration(new AuditConfiguration(), [
    $packageDefaults,          // framework fallbacks
    $publishedFile,            // config/audit.php
    $tenantOverrides,           // database / per-tenant settings
]);

For list-like nodes, disable deep merging so an override replaces rather than appends: $node->performNoDeepMerging(). For a whole array node that must be replaced wholesale, ->cannotBeOverwritten() prevents later sources from touching it at all.

Model repeated blocks with prototypes. Channels, queues, connections and disks are where hand-rolled validation falls apart:

$channels = $children->arrayNode('channels')->useAttributeAsKey('name');
$channels->normalizeKeys(false);
$channels->arrayPrototype()
    ->children()
        ->scalarNode('driver')->defaultValue('single')->end()
        ->scalarNode('path')->cannotBeEmpty()->end()
        ->integerNode('level')->defaultValue(100)->end()
    ->end();
$channels->requiresAtLeastOneElement();
$channels->defaultValue([]);

Normalize legacy and env-shaped input at the edge of the tree. beforeNormalization() runs per source before merging, which is the right place to accept back-compatible keys or string booleans coming from .env:

$children->booleanNode('enabled')
    ->beforeNormalization()
        ->ifString()
        ->then(static fn (string $v) => filter_var($v, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? $v)
    ->end()
    ->defaultTrue();

Prefer doing the env() resolution in the Laravel config file ('enabled' => (bool) env('AUDIT_ENABLED', true)) and keep the schema environment-agnostic.

Validate at deploy time, not on every request. Building and processing a tree is not free, and in production php artisan config:cache has already frozen config. Add a small Artisan command (audit:validate-config) and run it in CI/deploy; or cache the processed array keyed by the file hash. If you want resource-aware invalidation outside Laravel's cache, use ConfigCache:

use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Resource\FileResource;

$cache = new ConfigCache(storage_path('framework/audit-config.php'), app()->environment('local'));
if (! $cache->isFresh()) {
    $config = (new Processor())->processConfiguration(
        new AuditConfiguration(),
        [require config_path('audit.php')]
    );
    $cache->write('<?php return '.var_export($config, true).';', [new FileResource(config_path('audit.php'))]);
}
$config = require storage_path('framework/audit-config.php');

Non-PHP sources are just arrays. The Config component does not ship a YAML/XML/JSON parser of its own; it has generic loaders (FileLoader, DelegatingLoader, GlobFileLoader, DirectoryLoader, LoaderResolver). In Laravel, parse the file yourself with Symfony\Component\Yaml\Yaml::parseFile() or json_decode(), then feed the array to the Processor. This is the pattern for DB-backed settings: query, decode, pass as the highest-priority array.

Use the right node types. enumNode()->values(Driver::class) (or enumFqcn() for backed enums) gives you enum validation and defaultValue(Driver::Log). variableNode() accepts arbitrary mixed data and is the correct escape hatch for "free-form options" — do not use scalarNode for arrays or objects.

Generate docs/static analysis from the tree. If you publish a package, dumping a reference from the tree keeps README/config docs in sync. Be cautious about building new tooling on Symfony's fluent PHP config-builder generators: they are deprecated as of Symfony 8.0.

Gotchas and Tips

  • mergeConfigFrom() is shallow. Laravel's helper uses array_merge, so a user's nested array replaces your nested defaults. Rely on tree defaults, or use array_replace_recursive explicitly, but do not expect mergeConfigFrom to deep-merge.
  • Dashed keys are normalized by default. PrototypedArrayNode::normalizeKeys(true) converts my-channel to my_channel, which silently breaks lookups by the key users wrote. Call ->normalizeKeys(false) whenever keys are user-chosen (channel names, queue names, tenant IDs).
  • Dots in node names are path separators. A node literally named logging.channels is interpreted as a path. If you need dotted keys, change the separator with setPathSeparator('/') or keep dotted keys as dynamic array keys under a single node.
  • Unknown keys throw by default. That strictness is the point, but it breaks the moment a user adds a custom key to a published file. Decide deliberately: keep strict and document it, or use ignoreExtraKeys(true) — note that the second argument controls whether the extra keys are removed from the result (false keeps them).
  • Never combine isRequired() with defaultValue(). A required node with a default is contradictory; Symfony 8 deprecates/removes this. Required means "must be present".
  • BooleanNode wants real booleans. .env values are strings, and "false" is not false to this component. Cast in the config file or normalize with beforeNormalization(); otherwise you get InvalidTypeException in production after an env change.
  • %env(FOO)% is not resolved here. Env placeholders are a DependencyInjection feature (EnvPlaceholderParameterBag), not a Config feature. Numeric nodes tolerate placeholder strings in recent 6.4/7.x/8.x releases, but standalone use will leave them as strings. Resolve with Laravel's env()/config() before processing.
  • env() returns null once config is cached. Do not read env() inside a service provider or schema — Laravel only evaluates env() from config files when config:cache runs. Always validate values that come through config().
  • canBeEnabled()/canBeDisabled() inject an enabled key and have had merge bugs fixed in 7.4.4/8.0.4. If you support older patch versions, pin ^7.4.6/^8.0.6 or avoid combining them with treatNullLike/treatFalseLike.
  • Prototype defaults do not materialize automatically. An absent array node may not include child defaults; use ->addDefaultsIfNotSet() when you need the merged result to always contain the fully defaulted structure. ->requiresAtLeastOneElement() and ->defaultValue([]) are separate concerns.
  • ->end() drift is the classic authoring bug. One missing end() attaches nodes to the wrong parent and produces confusing "Unrecognized option" errors. Assign the NodeBuilder to a variable and add siblings without long chains, or dump the tree with YamlReferenceDumper while building it.
  • Errors are verbose but useful. Catch InvalidConfigurationException (it implements the component's Exception interface) and surface $e->getMessage(), which includes the full path like audit.channels.daily.path. In a package, wrap it in your own exception mentioning the config file name, otherwise users see a wall of nested array output.
  • Version constraints matter in Laravel. Symfony 8 requires PHP >= 8.4, and Laravel 11/12 apps commonly run PHP 8.2/8.3, so Composer will typically resolve symfony/config 7.4.x. Pin accordingly and check composer why symfony/config before assuming the major version.
  • Don't cache or serialize the tree. Node objects have internal state and serialization behavior that has changed across patch releases (__sleep/__wakeup replaced by __serialize/__unserialize). Cache the plain processed array, as in the ConfigCache example.
  • Reflection/glob resources are only useful if you cache. GlobResource, DirectoryResource, ReflectionClassResource and ClassExistenceResource exist for ConfigCache freshness checks; if you validate on deploy only, you do not need them and they add noise.
  • Runtime overrides bypass boot-time validation. Config::set('audit.driver', 'x') after boot will not be checked. If tenants or admin panels mutate config at runtime, route those values through the same Processor call (or a dedicated validator) rather than trusting the boot-time result.
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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core