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

michel/options-resolver

A lightweight PHP options resolver for validating and normalizing configuration arrays with defined defaults, required keys, allowed types and values, and nested option support—keeping package configuration predictable and errors clear.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a plain-PHP library, not a Laravel package — there is no service provider, no facade, and nothing to publish. Install it with Composer and use it directly:

composer require michel/options-resolver

Where to look first: the README for the fluent entry point, then src/ for the resolver class and its exception types. Because the package is small, reading the whole source (a few hundred lines) is faster and more reliable than trusting the docs — do that before you build anything on top of it.

First use case: validating the options array of a class that takes a bag of configuration — a client wrapper, a driver, a notification channel, a report generator. Instead of hand-rolled array_key_exists checks scattered through the constructor, you declare the schema once and resolve:

final class ImageProcessor
{
    private array $options;

    public function __construct(array $options = [])
    {
        $resolver = new OptionsResolver();          // adjust to the package's entry point
        $resolver
            ->setDefaults(['quality' => 80, 'format' => 'webp', 'strip_metadata' => true])
            ->setRequired(['disk'])
            ->setAllowedTypes('disk', 'string')
            ->setAllowedTypes('quality', 'int')
            ->setAllowedValues('format', ['webp', 'jpeg', 'png']);

        $this->options = $resolver->resolve($options);
    }
}

Start by converting one existing class that currently does ad-hoc array munging. That gives you a realistic sense of the API surface and its error messages before you spread the pattern across the codebase.

Implementation Patterns

Schema-per-integration, resolved at the boundary. Define the option schema next to the class that consumes it, and resolve in the constructor or a static factory. Everything downstream gets a plain, known-shape array — no further defensive checks. This pairs naturally with Laravel's config/services.php: keep the values in config, keep the schema in code.

Normalize, then validate. Use normalizers to coerce the messy values Laravel hands you (strings from .env, null from env vars that were never set, comma-separated lists) into their real types before the type checks run. This is the single most valuable pattern with a strict resolver, because config('services.x.enabled') is very often the string "false".

$resolver->setNormalizer('enabled', fn ($v) => filter_var($v, FILTER_VALIDATE_BOOL));
$resolver->setNormalizer('tags', fn ($v) => is_string($v) ? array_filter(explode(',', $v)) : $v);

Typed accessors over array access. Don't let the resolved array leak into controllers or jobs. Expose named getters ($processor->quality(), $processor->disk()) so the schema stays an implementation detail and static analysis keeps working.

Factory + container binding. For anything with multiple drivers (mail, storage, payments), resolve the option bag inside a factory that reads a config block and returns a configured instance. Bind that factory as a singleton so the resolution cost and the validation failure happen once, at boot or first use — not inside a loop. If invalid configuration should fail fast, resolving during a service provider's register()/boot() turns a runtime surprise into a deploy-time failure.

Reusable option sets for jobs and commands. Command signatures and queued job payloads are both "arrays with hidden rules." A shared resolver definition for, say, date-range options (from, to, granularity) can be used by an Artisan command, a queued export job, and an HTTP request — one schema, three entry points.

Nested options via closures. For sub-bags (headers, retry, credentials), use an allowed-values closure or a nested resolver so the child array is validated with the same rigor. Without this, setAllowedTypes('retry', 'array') accepts anything inside.

Testing pattern. Write one test per rule you actually rely on: unknown key throws, missing required key throws, wrong type throws, normalizer output is correct. Since exceptions come from the library, assert on its exception class (or on your own re-thrown one) rather than on message text.

Framework integration tips. Wrap resolution in a small base class or trait (HasValidatedOptions) so you're not repeating the try/catch and re-throw boilerplate. If you expose option validation to end users, catch the library exception and convert it to a ValidationException so Laravel renders a 422 instead of a 500.

Gotchas and Tips

Zero stars, zero dependents, one maintainer. Treat this as an in-house library with a nice README, not as infrastructure. Pin an exact version in composer.json, read the diff before every update, and be prepared to fork. Symfony's OptionsResolver is the battle-tested equivalent with nearly the same vocabulary — if you find yourself fighting this package, switching is a small refactor, not a rewrite. That is the main strategic risk to weigh before adopting it widely.

The release date (2026-09-04) is ahead of today. That's a strong signal the metadata is unreliable — check tags on the repository itself and pin to a specific commit or tag you have actually built against rather than to ^1.0 or dev-main.

Strict types reject your env vars. .env only produces strings; config() preserves them unless you cast. "false", "0", and "" will all fail an int/bool check. Always normalize boolean and integer options explicitly, and never rely on PHP's loose comparison to save you.

A default of null still counts as defined. If a key is required and has a default, the default usually wins and setRequired never fires. Decide per key whether you want "missing" to be an error or a fallback, and don't set both.

Unknown keys are the point — but they break upgrades. Strict rejection of undefined options means adding an option to a caller before adding it to the schema is a fatal error. When you change a schema, update all call sites in the same commit; add the option as a no-op default first if you need a rolling deploy.

Nested arrays are shallow by default. setAllowedTypes('x', 'array') validates nothing inside. For deep payloads, chain a closure validator or a nested resolver, otherwise your strictness stops exactly where it matters most.

Don't resolve per iteration. Resolution is cheap but not free, and doing it inside a foreach over rows (imports, API batches) turns a validation layer into a hot path. Resolve once, pass the array or object around.

Config caching caveats. php artisan config:cache serializes config values and cannot serialize closures. Keep all closures (normalizers, validators) in PHP classes and only scalars/arrays in config/. If resolution fails only after config:cache, that's the reason.

Debugging failures. The exceptions name the offending option — log the full resolved input alongside the message when you catch it, since the exception rarely shows the value that failed. For normalizer bugs, temporarily var_dump inside the normalizer; order matters, and a later normalizer sees the previous one's output.

Extension points to lean on. Normalizers (transform), allowed values/closure validators (assert), and defaults (fill) are the three levers. Prefer a normalizer plus a type check over a single clever closure — the error messages are better and the intent is obvious to the next developer.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core