PHPackages                             otherguy/php-currency-api - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. otherguy/php-currency-api

ActiveLibrary[HTTP &amp; Networking](/categories/http)

otherguy/php-currency-api
=========================

A PHP API Wrapper to offer a unified programming interface for popular Currency Rate APIs.

2.0.0(2mo ago)2526.8k↓33.9%5[11 PRs](https://github.com/otherguy/php-currency-api/pulls)MITPHPPHP ^8.3CI passing

Since Jun 7Pushed 1w ago1 watchersCompare

[ Source](https://github.com/otherguy/php-currency-api)[ Packagist](https://packagist.org/packages/otherguy/php-currency-api)[ RSS](/packages/otherguy-php-currency-api/feed)WikiDiscussions main Synced 2d ago

READMEChangelog (10)Dependencies (17)Versions (35)Used By (0)

💱 PHP Currency API
==================

[](#-php-currency-api)

[![PHP Currency API](resources/open-graph-preview.png)](resources/open-graph-preview.png)

*A PHP API Wrapper offering a unified, fluent programming interface for popular currency exchange rate APIs.*

[![Version](https://camo.githubusercontent.com/370132ffcf6a2805a3d999e7d3d1b3d1da6b7e97838f2a8931aab386544e4d2a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f746865726775792f7068702d63757272656e63792d6170692e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/otherguy/php-currency-api)[![Installs](https://camo.githubusercontent.com/5ec6fd507f3a116abd702ca42337fccdf989e76d2179bee6d68afc90a5e58925/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6f746865726775792f7068702d63757272656e63792d6170693f636f6c6f723d626c7565266c6162656c3d696e7374616c6c73267374796c653d666c61742d737175617265)](https://packagist.org/packages/otherguy/php-currency-api)[![PHP version](https://camo.githubusercontent.com/85ef21809786df82a2b71b1a4aaac14dd1f3ad6a6ecd11f7981503aeb36854ba/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6f746865726775792f7068702d63757272656e63792d6170693f7374796c653d666c61742d737175617265)](https://packagist.org/packages/otherguy/php-currency-api)[![CI](https://camo.githubusercontent.com/132eddb4f22c7446f8ff2944bad0f7f234b02dce314962a480e85672fc558836/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6f746865726775792f7068702d63757272656e63792d6170692f63692e796d6c3f6272616e63683d6d61696e267374796c653d666c61742d737175617265)](https://github.com/otherguy/php-currency-api/actions)[![Coverage](https://camo.githubusercontent.com/20564a01d9519d2403270f0ca6d06cd98ae6a4cdd8f82a298317347de65ebd23/68747470733a2f2f696d672e736869656c64732e696f2f636f766572616c6c732f6f746865726775792f7068702d63757272656e63792d6170692e7376673f7374796c653d666c61742d737175617265)](https://coveralls.io/github/otherguy/php-currency-api?branch=main)[![License](https://camo.githubusercontent.com/06e9c05daf99c92aa7967f5a4e2bf86d2c3049a3c0910983de7d70ceff1411b8/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6f746865726775792f7068702d63757272656e63792d6170692e7376673f7374796c653d666c61742d73717561726526636f6c6f723d6f72616e6765)](LICENSE.md)

Don't worry about your favorite currency conversion service shutting down or changing plans. Switch providers without changing your code.

What's new in 2.0
-----------------

[](#whats-new-in-20)

- **PHP 8.3+** with strict types everywhere.
- **PSR-18 / PSR-17** HTTP layer — bring your own client (Guzzle, Symfony, anything PSR-compliant).
- **`brick/math` `BigDecimal`** for precise rate math instead of floats.
- **`Currency` backed enum** replaces the old `Symbol` constants class (which is kept as a deprecation shim).
- **New `frankfurter` driver** — free, no API key required.
- **New `currencyapi` and `fastforex` drivers** — provider parity with TripTally's backend FX stack.
- **Rewritten `exchangeratesapi` driver** — now points at the working `api.apilayer.com` endpoint with full `convert()` support.
- **Pluggable `DriverFactory`** — register your own provider at runtime.

You can find detailed instructions on how to upgrade from `1.x` to `2.x` in [UPGRADING.md](UPGRADING.md).

Features
--------

[](#features)

- Multiple drivers behind a single interface — switch providers by changing one string.
- Fluent setter chain (`source`, `to`, `amount`, `date`, …) on every driver.
- `ConversionResult` value object with lossless rebasing (`setBaseCurrency()`).
- Hermetic test surface — inject any PSR-18 client, including in-memory mocks.

Supported APIs
--------------

[](#supported-apis)

ServiceIdentifier[Frankfurter](https://www.frankfurter.dev)`frankfurter`[FixerIO](https://fixer.io)`fixerio`[CurrencyLayer](https://currencylayer.com)`currencylayer`[Open Exchange Rates](https://openexchangerates.org)`openexchangerates`[APILayer Exchange Rates](https://apilayer.com/marketplace/exchangerates_data-api)`exchangeratesapi`[CurrencyAPI](https://currencyapi.com)`currencyapi`[fastFOREX](https://fastforex.io)`fastforex`A `mock` driver is also bundled for testing without network access.

*Want another provider? [Open an issue](https://github.com/otherguy/php-currency-api/issues) — or register a custom driver at runtime (see below).*

Requirements
------------

[](#requirements)

- PHP **8.3** or higher.
- A PSR-18 HTTP client and PSR-17 request factory of your choice.
- An API account with the chosen provider, except for `frankfurter`.

Installation
------------

[](#installation)

```
composer require otherguy/php-currency-api
```

You also need a PSR-18 client and PSR-17 factory. The most common choice is Guzzle:

```
composer require guzzlehttp/guzzle http-interop/http-factory-guzzle
```

Alternatively, with Symfony HttpClient:

```
composer require symfony/http-client nyholm/psr7
```

Quickstart
----------

[](#quickstart)

```
use Otherguy\Currency\Currency;
use Otherguy\Currency\DriverFactory;

$result = DriverFactory::make('frankfurter')
    ->from(Currency::USD)
    ->to([Currency::EUR, Currency::GBP])
    ->get();

echo $result->rate(Currency::EUR);     // BigDecimal '0.92'
echo $result->convert(100, Currency::USD, Currency::EUR); // BigDecimal '92.00'
```

`DriverFactory::make()` auto-discovers Guzzle if it's installed and wires up a default PSR-18 client. To inject your own:

```
use GuzzleHttp\Client;
use Http\Factory\Guzzle\RequestFactory;
use Otherguy\Currency\DriverFactory;

$factory = new DriverFactory();
$driver  = $factory->build('fixerio', new Client(), new RequestFactory());

$result = $driver->accessKey('YOUR_KEY')
    ->from(Currency::EUR)
    ->to(Currency::USD)
    ->get();
```

### Bring your own HTTP client (Symfony + nyholm/psr7)

[](#bring-your-own-http-client-symfony--nyholmpsr7)

```
use Nyholm\Psr7\Factory\Psr17Factory;
use Otherguy\Currency\DriverFactory;
use Symfony\Component\HttpClient\Psr18Client;

$psr17  = new Psr17Factory();
$client = new Psr18Client();

$driver = (new DriverFactory())->build('frankfurter', $client, $psr17);
```

Usage
-----

[](#usage)

### The `Currency` enum

[](#the-currency-enum)

`Otherguy\Currency\Currency` is a backed enum with one case per ISO-4217 code (plus a few common crypto/precious-metal codes).

```
use Otherguy\Currency\Currency;

Currency::USD->value;          // 'USD'
Currency::USD->displayName();  // 'United States Dollar'
Currency::tryFromCode('EUR');  // Currency::EUR
Currency::tryFromCode('XYZ');  // null
Currency::cases();             // every supported currency
```

Every method that takes a currency accepts either the enum or its string code, so plain `'USD'` keeps working.

### Setting the access key

[](#setting-the-access-key)

Most providers require authentication. `accessKey()` is sugar for `config('access_key', …)` and is wired per-driver to the right query-string parameter.

```
$driver->accessKey('YOUR_KEY');
```

Frankfurter has no API key — calling `accessKey()` on it throws `ApiException`. CurrencyAPI is the exception to the query-string rule: its driver sends the key in the `apikey` request header.

Provider-specific key mapping:

Driver`accessKey()` mapping`fixerio``access_key` query parameter`currencylayer``access_key` query parameter`openexchangerates``app_id` query parameter`exchangeratesapi``apikey` query parameter`currencyapi``apikey` request header`fastforex``api_key` query parameter`frankfurter`no key; throws `ApiException`### Configuration options

[](#configuration-options)

For provider-specific options use `config()`:

```
$driver->config('format', '1'); // CurrencyLayer pretty-printed JSON
```

### Base currency

[](#base-currency)

`from()` and `source()` are aliases.

```
$driver->from(Currency::USD);
$driver->source('USD');
```

Each driver has its own default base currency: `EUR` for FixerIO, APILayer Exchange Rates, and Frankfurter; `USD` for CurrencyLayer, Open Exchange Rates, CurrencyAPI, fastFOREX, and the mock driver. Most providers only allow base-currency changes on paid plans — they'll respond with an error envelope which the driver translates into `ApiException`.

### Target currencies

[](#target-currencies)

`to()` and `currencies()` are aliases. Pass a single currency or an array. Pass nothing (or an empty array) to ask for every currency the provider supports.

```
$driver->to(Currency::BTC);
$driver->currencies([Currency::BTC, Currency::EUR, Currency::USD]);
$driver->to([Currency::EUR, Currency::GBP]);
```

### Latest rates

[](#latest-rates)

```
$driver->get();              // current rates for the configured target currencies
$driver->get(Currency::DKK); // current rate for DKK
```

### Historical rates

[](#historical-rates)

Dates must be `DateTimeInterface` (or `null`).

```
use DateTimeImmutable;

$driver->date(new DateTimeImmutable('2010-01-01'))->historical();
$driver->historical(new DateTimeImmutable('2018-07-01'));
```

### Convert amount

[](#convert-amount)

```
$driver->convert(10.00, Currency::USD, Currency::THB);
$driver->convert(122.50, Currency::NPR, Currency::EUR, new DateTimeImmutable('2019-01-01'));
```

For providers without a native `/convert` endpoint (e.g. Frankfurter), the driver fetches the rate via `get()` / `historical()` and returns a `ConversionResult` for the requested pair. Use `ConversionResult::convert()` when you need the converted amount as a `BigDecimal`.

CurrencyAPI and fastFOREX both expose native latest conversion endpoints. For dated conversions, their drivers fetch historical rates and return a `ConversionResult` for the requested pair.

### Fluent chain

[](#fluent-chain)

```
DriverFactory::make('fixerio')->from(Currency::USD)->to(Currency::EUR)->get();
DriverFactory::make('fixerio')->from(Currency::USD)->to(Currency::NPR)->date(new DateTimeImmutable('2013-03-02'))->historical();
DriverFactory::make('fixerio')->from(Currency::USD)->to(Currency::NPR)->amount(12.10)->convert();
```

### `ConversionResult`

[](#conversionresult)

`get()` and `historical()` return a [`ConversionResult`](src/Results/ConversionResult.php). Rates are stored as `BigDecimal` and rebasing is lossless (default scale: 8 decimals).

```
use Brick\Math\BigDecimal;

$result = DriverFactory::make('frankfurter')
    ->from(Currency::USD)
    ->to([Currency::EUR, Currency::GBP])
    ->get();

$result->all();                     // ['USD' => BigDecimal '1', 'EUR' => BigDecimal '0.89', 'GBP' => BigDecimal '0.79']
$result->allAsFloats();             // legacy float view
$result->getBaseCurrency();         // 'USD'
$result->getDate();                 // '2026-04-25'
$result->rate(Currency::EUR);       // BigDecimal '0.89'
$result->rateAsFloat(Currency::EUR);// 0.89

$result->convert(5.0, Currency::EUR, Currency::USD); // BigDecimal '5.618...'

$rebased = $result->setBaseCurrency(Currency::EUR);
$rebased->getBaseCurrency();        // 'EUR'
$rebased->originalBaseCurrency;     // 'USD' — readonly, never mutated
```

`rate()` on a code that wasn't fetched throws `Otherguy\Currency\Exceptions\CurrencyException`. To convert between two arbitrary currencies, request both in the original `get()` / `historical()` call.

Registering custom drivers
--------------------------

[](#registering-custom-drivers)

The factory is instance-based. Bring your own driver class (extending `BaseCurrencyDriver`) and register it:

```
use Otherguy\Currency\DriverFactory;

$factory = new DriverFactory();
$factory->register('mybank', \Acme\MyBankDriver::class);

$driver = $factory->build('mybank');
```

The static `DriverFactory::make($name)` continues to work via a process-wide default factory — `DriverFactory::setDefault($factory)` lets you swap it for tests.

### Adding a new driver

[](#adding-a-new-driver)

A driver is the bridge between this library's fluent interface and a specific upstream rate provider. Every driver implements [`CurrencyDriverContract`](src/Drivers/CurrencyDriverContract.php) by extending [`BaseCurrencyDriver`](src/Drivers/BaseCurrencyDriver.php).

The base class supplies:

- All fluent setters (`source`, `from`, `currencies`, `to`, `amount`, `date`, `config`, `accessKey`, `secure`).
- A PSR-18 / PSR-17 HTTP layer in `apiRequest()` that builds the URI, merges `$httpParams` with per-call params, decodes JSON with `JSON_THROW_ON_ERROR`, and wraps every failure mode in `ApiException`.

You only need to:

1. Set the right defaults for `$apiURL`, `$protocol`, and `$baseCurrency`.
2. Implement `get()`, `historical()`, and `convert()`.
3. Override `apiRequest()` only if the provider's successful HTTP response can still carry an error envelope, such as `success: false`.

### Driver skeleton

[](#driver-skeleton)

```
