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

Options Resolver Laravel Package

symfony/options-resolver

OptionsResolver is array_replace on steroids: build option systems with required options, defaults, type and value validation, and normalization, with clear exceptions when configuration is invalid.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it explicitly in a Laravel app or package rather than relying on a transitive Symfony dependency:

composer require symfony/options-resolver:^7.4

Use ^7.4 for Laravel apps on PHP 8.2/8.3. The 8.x line requires PHP 8.4, so pin deliberately if you are not there yet. If Composer already pulls it in via another package, still add it to require so your direct dependency is explicit.

Look first at the official Symfony OptionsResolver docs, then at these classes in vendor/symfony/options-resolver/:

  • OptionsResolver — the main configuration API.
  • Options — the resolved-options object passed to normalizers and lazy defaults.
  • OptionsResolverIntrospector — read back required options, defaults, allowed types, normalizers, deprecations, etc. Useful when debugging or building internal tooling.
  • Exception/ — the exception hierarchy you will catch at boundaries.

The most common day-to-day first use case is validating a third-party service config before constructing a client. Create a small resolver class instead of scattering isset() and is_numeric() checks through a service provider:

namespace App\Support;

use Symfony\Component\OptionsResolver\OptionsResolver;

final class StripeOptions
{
    public static function resolve(array $options): array
    {
        return (new OptionsResolver())
            ->setRequired(['secret'])
            ->setDefaults([
                'timeout' => 10,
                'retries' => 3,
                'debug' => false,
            ])
            ->setAllowedTypes('secret', 'string')
            ->setAllowedTypes('timeout', 'int')
            ->setAllowedTypes('retries', 'int')
            ->setAllowedTypes('debug', 'bool')
            ->setAllowedValues('timeout', static fn (int $value): bool => $value > 0)
            ->setAllowedValues('retries', static fn (int $value): bool => $value >= 0)
            ->resolve($options);
    }
}

Then resolve once where the service is built:

$this->app->singleton(StripeClient::class, function ($app) {
    return new StripeClient(
        StripeOptions::resolve($app['config']->get('services.stripe', []))
    );
});

This gives you fail-fast validation at container binding time, a self-documenting config contract, and one place to change validation rules.

Implementation Patterns

One resolver class per integration. Keep resolvers small and named by the thing they configure: StripeOptions, QueueOptions, UploadOptions, NotifierOptions. The resolver should return a plain array or a typed DTO, not a resolver object.

final class QueueOptions
{
    public static function resolve(array $options): array
    {
        return (new OptionsResolver())
            ->setDefaults([
                'connection' => 'redis',
                'queue' => 'default',
                'tries' => 1,
                'backoff' => [5, 15],
            ])
            ->setAllowedTypes('connection', 'string')
            ->setAllowedTypes('queue', 'string')
            ->setAllowedTypes('tries', 'int')
            ->setAllowedTypes('backoff', 'array')
            ->setAllowedValues('connection', ['redis', 'database', 'sqs'])
            ->setNormalizer('queue', static fn ($options, $value) => trim((string) $value))
            ->resolve($options);
    }
}

Resolve at the boundary, not deep in domain code. Service providers, package bootstrapping, Artisan commands, and job constructors are good boundaries. Once resolved, pass the array or DTO through the application. Do not inject OptionsResolver into controllers or domain services.

Package configuration pattern. If you maintain a Laravel package, keep the published config file as plain Laravel config, merge defaults, then validate at binding time:

$this->mergeConfigFrom(__DIR__.'/../config/notifier.php', 'notifier');

$this->app->singleton(Notifier::class, function ($app) {
    return new Notifier(
        NotifierOptions::resolve($app['config']['notifier'])
    );
});

This catches bad package config during app boot or first resolution, not halfway through a notification send.

Nested configuration. For nested arrays such as database or SSL settings, use a child resolver and validate through a normalizer. This is version-safe and keeps each layer’s rules isolated:

$db = new OptionsResolver();
$db->setRequired(['host', 'port'])
   ->setAllowedTypes('host', 'string')
   ->setAllowedTypes('port', 'int');

$resolver->setDefault('database', []);
$resolver->setAllowedTypes('database', 'array');
$resolver->setNormalizer('database', static function ($options, array $value) use ($db): array {
    return $db->resolve($value);
});

On Symfony 7.3+, defining nested options via setDefault() is deprecated in favor of setOptions(); Symfony 8 removes the old approach. If you build packages supporting both 7.4 and 8.x, prefer child-resolver composition or abstract the nested definition behind your own factory until you can require 8.x.

Lazy defaults for runtime/environment values. Use closures for values that should be computed only when resolved:

->setDefaults([
    'api_timeout' => static fn () => (int) config('services.api.timeout', 30),
    'debug' => static fn () => app()->environment('local'),
])

In Laravel, use config() rather than env() inside runtime resolver closures. Once php artisan config:cache has run, env() calls outside config files can return null.

Deprecation and migration pattern. When renaming or replacing package options, define the new option and deprecate the old one:

$resolver
    ->setDefault('backoff', [5, 15])
    ->setDeprecated('retry_after', '2.0', 'Use the "backoff" option instead.');

This gives consumers a warning path instead of a hard break. If you use remove() on options that were deprecated, upgrade to at least the patched 7.3.8 / 6.4.30 releases; those fixed remove() leaving deprecation state behind.

Commands and jobs. For Artisan commands with many options, resolve the assembled option array before calling the service:

$options = QueueOptions::resolve([
    'connection' => $this->option('connection'),
    'queue' => $this->option('queue'),
    'tries' => (int) $this->option('tries'),
]);

For queued jobs, resolve in the constructor or dispatch path and serialize only the plain resolved array/DTO. Never put an OptionsResolver instance into a queued payload.

Gotchas and Tips

Undefined options throw. OptionsResolver is strict by default. Passing a whole Laravel config array with extra keys will throw UndefinedOptionsException. Either resolve only the subset you own, use array_intersect_key(), or explicitly call setDefined() for keys you accept but do not require.

null is not implied. A default of null still needs null in allowed types:

->setDefault('timeout', null)
->setAllowedTypes('timeout', ['int', 'null'])

If you use unions, remember Symfony 7.3+ added broader union type support. Keep type declarations in normalizer/validator closures compatible with the resolved value.

Closure defaults are lazy, not values. If an option’s value is supposed to be a Closure, return it from another closure; otherwise OptionsResolver will execute it as a lazy default. Example: ->setDefault('callback', static fn () => $callback).

Normalizers canonicalize before validation rules matter. Use them for trimming, lowercasing, casting strings from env/config, and mapping legacy aliases. Keep them idempotent and avoid side effects such as DB calls or service resolution inside normalizers.

Catch the component exception interface at boundaries. Wrap Symfony\Component\OptionsResolver\Exception\ExceptionInterface in your own domain exception with context, but avoid dumping secret values. For debugging, use OptionsResolverIntrospector to inspect what was actually defined:

$introspector = new OptionsResolverIntrospector($resolver);
$introspector->getRequiredOptions();
$introspector->getDefault('timeout');
$introspector->getAllowedTypes('timeout');

Do not validate user request input with this. Laravel FormRequests, Validator, and route/model binding are the right tools for HTTP input. OptionsResolver is for service, package, and integration configuration contracts.

Config caching quirk. Resolving in a singleton binding is usually fine, but avoid resolving large config trees on every request in hot paths. Resolve once per request via the container, or compile to a plain config array in a deploy-time command if the validation is expensive. Do not mutate and reuse one resolver instance across unrelated resolutions; create a configured resolver per resolution or bind only the final resolved array.

Version traps. Symfony 8 requires PHP 8.4 and removes nested option definitions via setDefault(). Symfony 7.4 is the safer Laravel target today. If you support Laravel packages across multiple Symfony versions, keep your public config API stable and hide resolver internals behind *Options::resolve() factories.

Extension point: configureOptions() methods. For reusable services, expose a method that accepts an OptionsResolver and lets callers add or adjust rules:

public static function configureOptions(OptionsResolver $resolver): void
{
    $resolver->setDefaults(['queue' => 'default']);
}

Then your own factory calls it before resolve(). This mirrors Symfony’s own extension pattern and keeps package consumers from needing to know your internal resolver structure.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package