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

Technical Evaluation

What this actually is. symfony/options-resolver is a framework-agnostic configuration-schema component, not a Laravel package. It gives you a declarative contract for an options array: required/optional keys, defaults, allowed types and values, normalizers, nested option sets, deprecation flags, and a resolve() call that throws on anything that doesn't match the contract. There is no service provider, no facade, no Blade/queue/HTTP integration — it is a pure PHP library we would consume inside our own code.

Architecture fit. Good fit as a boundary/adapter layer between untrusted-but-trusted-by-us configuration input and our services. The places it maps cleanly onto a Laravel codebase:

  • Constructor/factory options for our own service classes and SDK wrappers (payment gateways, mail transports, queue clients).
  • config/*.php payloads for first-party packages, validated once at provider boot rather than scattered config('x.y', $default) calls.
  • Normalizing job/payload arrays before dispatch, and third-party webhook/SDK option bags.
  • Deprecation shims for renamed config keys during a v1→v2 package or service migration.

It does not belong in the request-validation path. Laravel's Validator/FormRequest is the right tool for user input (localized messages, HTTP 422 semantics, Rule objects). OptionsResolver throws exceptions and is designed for developer-authored configuration, so using it on user input would convert validation errors into 500s.

Integration feasibility. Very high. composer require symfony/options-resolver is a single, extension-free dependency; in practice it is often already present transitively (Symfony Console, HttpClient, Mime, Translation, and many third-party Laravel packages pull it in). Before any decision, run composer show symfony/options-resolver and composer why symfony/options-resolver — if it's already in the lock file, the marginal cost of adopting it is documentation and code review, not dependency risk.

Technical risk. Low-to-moderate, and concentrated in version management rather than in the library itself:

  • Version/BCP. Major versions track Symfony's cadence. 7.3 deprecated defining nested options via setDefault() in favour of setOptions(); 8.0 removed the old path and requires PHP ≥ 8.4. If we adopt, we should adopt setOptions() from day one so the eventual v7→v8 move is a composer bump, not a refactor.
  • PHP floor. 7.4.x needs PHP 8.2+; 8.1.x needs PHP 8.4+. Our Laravel version pins decide this, not preference.
  • Static-analysis ergonomics. resolve() returns array; the type information lives in docblocks and @var annotations. PHPStan can't prove a resolved array's shape, so IDE/static coverage of option bags is weaker than a typed DTO. This is the main "why not just a DTO?" counterargument.
  • Closure defaults are evaluated at resolve time. Powerful for env-dependent defaults, but they create a gap between Laravel's cached config and what actually resolves at runtime — a real source of "works locally, differs in prod" bugs if overused.
  • Error surface. UndefinedOptionsException / InvalidOptionsException / MissingOptionsException are developer-facing messages, not localized or user-safe; they need mapping into our logging/exception handler policy.
  • Signal quality. The repo is a read-only split of symfony/symfony; stars, and especially the reported "Dependents: 0", understate real adoption by orders of magnitude. Judge it by the parent repo's maintenance record, not this one.

Key questions for the team.

  1. What are our actual PHP and Laravel floors across all production services, and which resolver major do they imply?
  2. Is the package already in composer.lock transitively? If yes, which version, and who owns its constraint?
  3. Which 2–3 concrete call sites have had real configuration bugs in the last 12 months (evidence for ROI)?
  4. Do we need IDE/static type safety strong enough that a typed DTO or Laravel Data/Spatie\LaravelData-style object is a better fit than an array resolver?
  5. Who owns the resolution of "Symfony major bumps twice a year" against our release train?
  6. Will we wrap it in an internal base class (recommended) so we can swap it later without touching 30 call sites?

Integration Approach

Stack fit. Works with any Laravel version on a supported PHP; it is framework-agnostic and has zero coupling to Laravel's container, config cache, or event system. The integration work is therefore convention work, not wiring work. Recommended shape:

  • Register pre-configured resolvers as singletons in a ConfigurationServiceProvider (one binding per contract: PaymentGatewayConfig, QueueWorkerConfig, etc.) and inject array results, or inject a small typed wrapper.
  • Resolve once at boot or at first use, never per request inside a hot loop, and never build a resolver inside a closure that runs per job.
  • Keep resolvers out of config/*.php so php artisan config:cache stays a plain array cache; build resolvers in providers from config() values.

Migration path (inventory → pilot → standardize → retire).

  1. Inventory. Grep for array_replace, array_merge defaults, isset($opts[...]) guards, and config('x', $default) chains. Tag each site by blast radius (does a misconfiguration take down payments/queues/API clients, or just a report?). Expect the highest-value targets to be queue workers, mail/SMS transports, and third-party SDK wrappers.
  2. Pilot (2–3 sites, 1 sprint). Build resolvers for the highest-blast-radius sites only. Do not refactor adjacent code. Success gate: all previously ad-hoc checks replaced, contract tests in place, no behavioural change to valid configurations.
  3. Standardize. Ship one internal base class (App\Support\Options\Resolver) that fixes our conventions: exception-to-log mapping, redaction of secret options before logging, a describe() method that dumps the schema for docs, and a rule that nested options always use setOptions(), never setDefault().
  4. Roll out. One PR per service, each with the schema and at least one negative test (missing required key, wrong type, disallowed value). Reviewers check for setOptions() usage and no closure defaults touching I/O.
  5. Retire legacy. Add setDeprecated() on keys being renamed, with a resolution date and a metric on how often the deprecated path fires; remove once the metric is flat for 30 days.

Compatibility and versioning. Pin to the resolver major that matches our PHP floor, use caret constraints, and treat the Symfony major as a tracked dependency in the platform upgrade plan rather than something that arrives via composer update. If any production service must stay on PHP 8.2/8.3, we land on the 7.x line and should note that the 7.3 deprecations are our deadline for setOptions() adoption. Prefer an LTS line (6.4 or 7.4) where we need a long support runway.

Sequencing. Pilot before standard before package-wide adoption. Do not attempt a "configuration refactor" programme; adopt it opportunistically whenever a service is already being touched, plus the small set of high-risk call sites identified in step 1.

Operational Impact

Maintenance. Upstream is owned by the Symfony core team, MIT-licensed, with a steady patch cadence (multiple releases across 5.4 → 8.1 lines visible in the last cycles) and a clear deprecation policy. Our exposure is limited to consuming it; we will never patch it. The maintenance burden we take on is internal: one base class, a documented convention, and a version-pin review on each Symfony major.

Support. No commercial SLA and no vendor to escalate to. Support is community + docs, and the docs live on symfony.com and are Symfony-framed (YAML service configuration, FrameworkBundle context) rather than Laravel-framed. Plan for an internal one-pager ("how we use OptionsResolver in this repo") and a Slack channel owner; budget roughly a day of one engineer's time to write it. Risk of abandonment is negligible for a component this widely depended upon; risk of documentation drift from our Laravel reality is the real support issue.

Scaling. Resolution cost is CPU-only, O(number of options) with a small constant, and happens once per resolver instance. Under steady traffic this is noise compared to a single database query — provided we honour the singleton binding and don't rebuild resolvers per request or per job. The scaling hazard is behavioural, not computational: closure defaults that hit env(), config() after cache warm, or (worst case) the database, evaluated at resolve time, can turn a cached-boot path into a per-request I/O path. Ban I/O in defaults by review rule.

Failure modes and mitigations.

  • Boot-time hard failure. A resolver running in a service provider throws MissingOptionsException → every request 500s. Mitigation: resolve in the provider, but wrap contract construction in a test-time assertion (php artisan about-style health check) rather than first-request; add a config:validate artisan command that resolves every registered schema and runs in CI and at deploy.
  • Silent drift via defaults. Defaults quietly absorb a missing key that used to be required, masking a deployment mistake. Mitigation: setRequired() for anything that has no safe default, and a test asserting the required set.
  • Normalizer side effects. A normalizer that trims, casts, or fetches data hides transformation from readers. Mitigation: normalizers stay pure and side-effect-free; anything with effects belongs in a service, not a schema.
  • Exception surface in production. An InvalidOptionsException reaching the user becomes an opaque 500 (or, worse, leaks config values). Mitigation: map these to a domain exception in the base class, log the option name but redact the value for keys tagged secret.
  • Deprecation log noise. setDeprecated() fires on every resolve; at scale that's log volume. Mitigation: sample or route to a metric, with a removal date attached.
  • Upgrade breakage at the v7→v8 boundary. Nested setDefault() stops working. Mitigation: lint rule or review checklist requiring setOptions() from day one.
  • Mistaking it for input validation. Someone routes $request->all() into a resolver. Mitigation: explicit convention in the base class docblock — configuration only; user input goes through Validator.

Ramp-up. A Laravel developer with the docs open is productive in about an hour; the API is small (setRequired, setDefaults, setAllowedTypes, setAllowedValues, setNormalizer, setOptions, resolve). Realistic ramp: half a day to the first reviewed schema, one to two days to internal fluency including our base class and conventions. The cost is almost entirely in knowing which options are required and what types are valid — the domain knowledge, not the library — and that knowledge is exactly what the schema captures permanently, which is the durable payback.

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.
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support