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

Exchanger Laravel Package

florianv/exchanger

PHP exchange rate provider layer exposing 30+ services (fastFOREX, ECB, national banks) through one ExchangeRateService interface, with chain fallback, PSR-16 caching and historical rates. Maintained since 2016; requires PHP 8.2+.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

Exchanger is a provider abstraction layer, not a Laravel package. It sits at the bottom of the florianv stack: laravel-swap → symfony-swap → swap → exchanger. Choosing it directly means we deliberately take ownership of what Swap normally gives us for free: facade/API shape, DI bindings, config surface, cache policy, and chain composition.

That trade is only justified if at least one of these is true: we need non-default chain ordering, we need custom cache-key or TTL strategy per currency pair, we need to wrap providers in our own PSR-18 middleware (rate limiting, request signing, tracing), or we're building an FX surface that other teams consume. If the requirement is simply "convert an amount at today's rate," laravel-swap is the cheaper decision and it pulls Exchanger in transitively — we get the same providers with less code we own.

Positively, the abstraction surface is small and stable: an ExchangeRateService contract, ExchangeRateQueryBuilder for constructing queries, and an ExchangeRate value object exposing getValue(), getDate(), getProviderName(), getCurrencyPair(). That is easy to hide behind our own FxRateProvider interface, which keeps vendor swap-out cheap and keeps the library out of domain code.

The PSR-18 / PSR-17 / PSR-16 posture is the strongest architectural argument. No hard coupling to Guzzle, no hard coupling to a specific cache backend, trivially fakeable in tests. The cost is that nothing is wired up for us in Laravel.

Integration feasibility

composer require florianv/exchanger symfony/http-client nyholm/psr7 resolves cleanly in a modern Laravel app; php-http/discovery (now via Psr18ClientDiscovery as of 2.9.0) finds an installed PSR-18 client, and Laravel ships Guzzle by default. Provider credentials go in config/services.php and .env. No service provider is published, so we write one — roughly 30–60 lines to bind a configured Chain instance.

The one genuine friction point is PSR-16 caching. Laravel's cache repository is not PSR-16. Options are: (a) symfony/cache adapters pointed at Redis/filesystem, which duplicates our cache infrastructure and splits operational tooling; (b) php-cache/simple-cache-bridge, which is the wrong direction (PSR-6 → PSR-16); or (c) a small adapter class implementing Psr\SimpleCache\CacheInterface over Illuminate\Contracts\Cache\Repository. Option (c) is the right call — it keeps TTL, prefixing, and eviction under config/cache.php and keeps our existing Redis/Horizon observability intact.

Technical risk

  • Bus factor. Single primary maintainer, modest adoption footprint, and the scoring data reports zero dependents — which should be verified against Packagist, since Swap is expected to depend on it. Either way, this is not a widely-forked dependency. Mitigation: our FxRateProvider wrapper means an exit costs days, not weeks.
  • Provider churn is the dominant risk, and it is structural. 2.0.0 removed Yahoo and Google outright; 2.9.0 patched National Bank of Romania endpoints that "may have stopped working or could stop at any time." Our FX availability is therefore partly outsourced to third parties, several with no contract and no SLA. This is an availability risk, not a code-quality risk.
  • Versioning discipline. 2.9.0 raised the PHP floor to 8.2 and swapped the HTTP discovery class in a minor release. Both are defensible but operationally breaking for a dependent app. Pin to a caret range, but treat every minor as a change requiring a test-suite run.
  • Correctness responsibility is ours. The library returns a rate; it does not convert, round, or enforce precision. getValue() returns a float by design. That is fine for display and catalog pricing, not for ledger-grade accounting. We must convert and round with BCMath or brick/money at our boundary and never let a float reach a persisted monetary field.
  • Low security surface. Outbound HTTP only, no PII, no deserialization, MIT licensed. API keys are the only secret, and they belong in env/secret manager.
  • Data artifact to verify. The stated last release date (2026-08-06) is ahead of a plausible present; confirm the actual latest tag and its changelog before pinning.

Key questions

  1. Do we actually need provider-level control, or would laravel-swap defaults satisfy the requirement at lower cost of ownership?
  2. Which providers can we commercially contract for, and what is our fallback when a free tier disappears or changes shape?
  3. What is the maximum acceptable rate staleness for each use case — display, checkout, invoicing, settlement?
  4. Do we need historical rates, and if so which date ranges and which providers actually cover them?
  5. Who owns the decision to accept a single-maintainer dependency for a money-adjacent code path, and what is the review cadence?
  6. Where does the precision/rounding contract live, and who signs off on it?

Integration Approach

Stack fit

Target Laravel 10–12 on PHP 8.2+. Guzzle is present by default, so PSR-18 discovery resolves without extra packages; we can skip symfony/http-client and nyholm/psr7 entirely. Bind the configured Exchanger and Chain as singletons — the service objects carry no per-request mutable state, so this is Octane-safe, but resolve per-request if we ever add stateful middleware. Queue workers and scheduled jobs are fine; only outbound HTTP and the cache are involved.

Compatibility

Verify the CI matrix against our PHP floor. If any supported service still runs 8.1, Exchanger is off the table until that service is upgraded. Check PSR-16 package version constraints against whatever cache adapter we choose, and confirm php-http/discovery major version alignment if we add other HTTP-consuming packages.

Migration path

  • From raw cURL/Guzzle calls: introduce our FxRateProvider interface, implement it with Exchanger, and shadow-run both for one billing cycle, comparing outputs and logging deltas before cutover.
  • From laravel-swap: no migration needed — laravel-swap already depends on Exchanger. Moving "down" to Exchanger directly is only worth doing if we need chain/cache control; otherwise stay put and configure Swap.
  • From hardcoded single provider: this is the real win. We replace a brittle integration with a fallback chain and gain getProviderName() for attribution.

Sequencing

  1. Spike (2–3 days). One provider (ECB for EUR pairs, no key required) behind our interface, in a branch. Validate the adapter pattern and float handling.
  2. Config + binding (2 days). config/fx.php with provider list, per-provider credentials, cache TTL, timeout, and staleness policy. Service provider binds the chain.
  3. Cache adapter (2 days). PSR-16 adapter over Laravel's cache repository, with per-pair and per-date key strategy and an explicit TTL matrix.
  4. Chain + resilience (3 days). Primary commercial provider, ECB or a second commercial provider as fallback, per-provider timeouts, last-known-good persistence, and a negative cache to avoid hammering a down provider.
  5. Observability and failure drill (3 days). Emit provider name, latency, cache hit/miss, and fallback depth per lookup. Then deliberately break each provider in staging and confirm behavior.
  6. Rollout. Feature-flag by use case; start with display-only pricing, then checkout, then any settlement-adjacent path only after the precision contract is signed off.

Operational Impact

Maintenance

Ongoing cost is low but not zero — budget roughly 0.1 FTE: a monthly dependency review, a read of release notes on every minor (the 2.9.0 pattern suggests minor versions are not always operationally inert), and reactive fixes when a provider endpoint drifts. Provider breakage will be discovered by us, not by upstream, so our alerting on "conversion failed" or "fallback depth > 0" is the real maintenance mechanism.

Support

Community-only. No SLA, no vendor to escalate to, upstream GitHub issues as the sole channel. Our on-call owns integration failures; the maintainer owns provider parsing bugs. For paid providers, the provider's own support is a separate, better channel — another argument for at least one contracted provider in the chain.

Scaling

Lookups are cache-dominated: one entry per currency pair per date granularity. The ceiling is provider rate limits, not our application tier. Two scaling hazards: (a) the fallback path is sequential, so a fully-down chain costs N × timeout per request — set 1–2s per-provider timeouts and a short negative cache; (b) TTL expiry creates a stampede on hot pairs — use cache locks or probabilistic early refresh. For high-traffic checkout paths, warm known pairs on a schedule rather than fetching inline.

Failure modes to design for

  • Total provider failure → ChainException carrying all collected exceptions. Must degrade to a persisted last-known-good rate with an explicit staleness policy, not a 500.
  • Silent skip of unsupported pairs → the chain "succeeds" with a worse provider or a different rate granularity. Log provider identity on every lookup and alert on unexpected provider changes.
  • Base/quote inversion. ECB is EUR-base only; an inverted rate looks plausible and is catastrophically wrong. Assert direction in tests.
  • Stale intraday data. Central bank providers publish daily with weekend/holiday gaps; intraday volatility is invisible. Match provider choice to the use case.
  • Date semantics. getDate() is the provider's rate date, not our accounting date. Cross-border month-end close will expose this.
  • Precision drift. Float multiplication across line items accumulates. Round once, at the boundary, with an explicit rounding mode.
  • Key leakage/rotation. Rotate API keys without a deploy by reading from config at resolution time, not at construction.

Ramp-up

A competent Laravel developer ships a working EUR→USD conversion in under a day; production-grade caching, chaining, and observability take about a week. Documentation is adequate — a solid README, a deeper doc/readme.md covering caching/HTTP/error handling, plus a changelog and release notes — but there is nothing Laravel-specific, so most of the learning curve is provider quirks (base-currency restrictions, granularity, historical coverage, key handling) rather than the library API itself. Budget for that explicitly in the spike; it is where the surprises live.

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.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table