Skip to content

Modernize currency API#123

Merged
otherguy merged 10 commits intomainfrom
v2-modernization
Apr 27, 2026
Merged

Modernize currency API#123
otherguy merged 10 commits intomainfrom
v2-modernization

Conversation

@otherguy
Copy link
Copy Markdown
Owner

Modernizes php-currency-api for v2 with a stricter PHP 8.3+ baseline, PSR HTTP abstractions, precise decimal math, updated provider support, and a refreshed test/tooling/docs surface.

  • Require PHP 8.3+ and add strict types throughout.
  • Replace hard-wired Guzzle usage with PSR-18 client and PSR-17 request factory support.
  • Add Currency backed enum while keeping Symbol as a deprecation shim.
  • Switch conversion rates from floats to brick/math BigDecimal.
  • Add new built-in drivers:
    • frankfurter
    • currencyapi
    • fastforex
  • Rewrite exchangeratesapi for APILayer’s current endpoint and implement conversion support.
  • Add instance-based DriverFactory registration while preserving DriverFactory::make().
  • Add modern CI/tooling:
    • GitHub Actions
    • Pint
    • PHPStan
    • Rector
    • PHPUnit 12
  • Refresh README, Changelog, contributor docs, upgrade notes, and examples.

@qltysh
Copy link
Copy Markdown

qltysh Bot commented Apr 27, 2026

2 new issues

Tool Category Rule Count
qlty Duplication Found 17 lines of similar code in 2 locations (mass = 163) 2

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml Outdated
Comment thread CONTRIBUTING.md
Comment thread CONTRIBUTING.md
Comment thread CONTRIBUTING.md
Comment thread src/DriverFactory.php
Comment thread src/DriverFactory.php
Comment thread src/DriverFactory.php
Comment thread src/DriverFactory.php
Comment thread src/DriverFactory.php Outdated
Comment thread src/DriverFactory.php
Comment thread src/DriverFactory.php
Comment thread src/DriverFactory.php
Comment thread src/DriverFactory.php
Comment thread src/DriverFactory.php
Comment thread src/DriverFactory.php Outdated
Comment thread src/DriverFactory.php Outdated
Comment thread src/DriverFactory.php Outdated
Comment thread src/DriverFactory.php Outdated
Comment thread src/Drivers/BaseCurrencyDriver.php
Comment thread rector.php
Comment thread rector.php
Comment thread rector.php
Comment thread rector.php
Comment thread src/Currency.php
Comment thread src/Drivers/ExchangeRatesApi.php Outdated
}

$response = $this->apiRequest('latest', [
'base' => $this->getBaseCurrency(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong amount of left-padding spaces(want multiple of 4) [editorconfig-checker:indent_size]

Comment thread src/Drivers/ExchangeRatesApi.php Outdated
Comment thread src/Drivers/ExchangeRatesApi.php Outdated
Comment thread src/Drivers/ExchangeRatesApi.php Outdated
Comment thread src/Drivers/ExchangeRatesApi.php Outdated
*
* @return array<string, BigDecimal|float|int|string>
*/
protected function responseRates(array $response, string $key, string $provider): array
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method Otherguy\Currency\Drivers\BaseCurrencyDriver::responseRates() has invalid return type Brick\Math\BigDecimal. [phpstan:class.notFound]


$normalised = [];
foreach ($rates as $currency => $rate) {
if (!$rate instanceof BigDecimal && !is_float($rate) && !is_int($rate) && !is_string($rate)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class Brick\Math\BigDecimal not found. [phpstan:class.notFound]

throw new ApiException('CurrencyAPI response did not contain a converted value.');
}

$rate = BigDecimal::of((string) $converted)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to static method of() on an unknown class Brick\Math\BigDecimal. [phpstan:class.notFound]

}

$rate = BigDecimal::of((string) $converted)
->dividedBy(BigDecimal::of((string) $this->amount), ConversionResult::DEFAULT_SCALE, RoundingMode::HALF_UP);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 issues:

1. Access to constant HALF_UP on an unknown class Brick\Math\RoundingMode. [phpstan:class.notFound]


2. Call to static method of() on an unknown class Brick\Math\BigDecimal. [phpstan:class.notFound]

*
* @return array<string, BigDecimal|float|int|string>
*/
private function ratesFromData(mixed $data): array
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method Otherguy\Currency\Drivers\CurrencyApi::ratesFromData() has invalid return type Brick\Math\BigDecimal. [phpstan:class.notFound]

Comment thread tests/CurrencyTest.php
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class CurrencyTest extends TestCase
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class Otherguy\Currency\Tests\CurrencyTest extends unknown class PHPUnit\Framework\TestCase. [phpstan:class.notFound]

Comment thread tests/CurrencyTest.php

class CurrencyTest extends TestCase
{
#[Test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attribute class PHPUnit\Framework\Attributes\Test does not exist. [phpstan:attribute.notFound]

Comment thread tests/CurrencyTest.php
#[Test]
public function exposes_all_supported_currency_cases(): void
{
$this->assertCount(167, Currency::cases());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method Otherguy\Currency\Tests\CurrencyTest::assertCount(). [phpstan:method.notFound]

Comment thread tests/CurrencyTest.php
$this->assertCount(167, Currency::cases());
}

#[Test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attribute class PHPUnit\Framework\Attributes\Test does not exist. [phpstan:attribute.notFound]

Comment thread tests/CurrencyTest.php
#[Test]
public function backed_value_matches_iso_code(): void
{
$this->assertSame('USD', Currency::USD->value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method Otherguy\Currency\Tests\CurrencyTest::assertSame(). [phpstan:method.notFound]

@otherguy otherguy self-assigned this Apr 27, 2026
*
* @return array<string, BigDecimal|float|int|string>
*/
protected function responseRates(array $response, string $key, string $provider): array
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method Otherguy\Currency\Drivers\BaseCurrencyDriver::responseRates() has invalid return type Brick\Math\BigDecimal. [phpstan:class.notFound]


$normalised = [];
foreach ($rates as $currency => $rate) {
if (!$rate instanceof BigDecimal && !is_float($rate) && !is_int($rate) && !is_string($rate)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class Brick\Math\BigDecimal not found. [phpstan:class.notFound]

throw new ApiException('CurrencyAPI response did not contain a converted value.');
}

$rate = BigDecimal::of((string) $converted)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to static method of() on an unknown class Brick\Math\BigDecimal. [phpstan:class.notFound]

}

$rate = BigDecimal::of((string) $converted)
->dividedBy(BigDecimal::of((string) $this->amount), ConversionResult::DEFAULT_SCALE, RoundingMode::HALF_UP);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 issues:

1. Access to constant HALF_UP on an unknown class Brick\Math\RoundingMode. [phpstan:class.notFound]


2. Call to static method of() on an unknown class Brick\Math\BigDecimal. [phpstan:class.notFound]

*
* @return array<string, BigDecimal|float|int|string>
*/
private function ratesFromData(mixed $data): array
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method Otherguy\Currency\Drivers\CurrencyApi::ratesFromData() has invalid return type Brick\Math\BigDecimal. [phpstan:class.notFound]

Comment thread tests/CurrencyTest.php
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class CurrencyTest extends TestCase
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class Otherguy\Currency\Tests\CurrencyTest extends unknown class PHPUnit\Framework\TestCase. [phpstan:class.notFound]

Comment thread tests/CurrencyTest.php

class CurrencyTest extends TestCase
{
#[Test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attribute class PHPUnit\Framework\Attributes\Test does not exist. [phpstan:attribute.notFound]

Comment thread tests/CurrencyTest.php
#[Test]
public function exposes_all_supported_currency_cases(): void
{
$this->assertCount(167, Currency::cases());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method Otherguy\Currency\Tests\CurrencyTest::assertCount(). [phpstan:method.notFound]

Comment thread tests/CurrencyTest.php
$this->assertCount(167, Currency::cases());
}

#[Test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attribute class PHPUnit\Framework\Attributes\Test does not exist. [phpstan:attribute.notFound]

Comment thread tests/CurrencyTest.php
#[Test]
public function backed_value_matches_iso_code(): void
{
$this->assertSame('USD', Currency::USD->value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method Otherguy\Currency\Tests\CurrencyTest::assertSame(). [phpstan:method.notFound]

- Add open graph preview image in PNG and SVG formats.
- Create `resources/README.md` to document resource folder contents.
- Remove obsolete PSD file.
- Update README to reflect new API wrapper description and upgrade notes.
- Add UPGRADING.md file with detailed upgrade instructions from 1.x to 2.x.
- Update CI workflow to trigger on `main` branch only.
- Change push instructions in CONTRIBUTING.md to use `main` branch.
- Update README badges to reflect `main` branch for CI and coverage.
- Update GitHub Actions workflow to include linting jobs.
- Modify dependabot configuration for improved dependency updates.
- Add linting configuration files for Markdown and YAML.
Comment thread .github/workflows/ci.yml
with:
persist-credentials: false
- name: Markdown Lint
uses: DavidAnson/markdownlint-cli2-action@v23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unpinned action reference [zizmor:zizmor/unpinned-uses]

Comment thread .github/workflows/ci.yml
config: ".markdownlint.yaml"

- name: YAML Lint
uses: karancode/yamllint-github-action@master
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unpinned action reference [zizmor:zizmor/unpinned-uses]

*
* @return array<string, BigDecimal|float|int|string>
*/
protected function responseRates(array $response, string $key, string $provider): array
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method Otherguy\Currency\Drivers\BaseCurrencyDriver::responseRates() has invalid return type Brick\Math\BigDecimal. [phpstan:class.notFound]


$normalised = [];
foreach ($rates as $currency => $rate) {
if (!$rate instanceof BigDecimal && !is_float($rate) && !is_int($rate) && !is_string($rate)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class Brick\Math\BigDecimal not found. [phpstan:class.notFound]

throw new ApiException('CurrencyAPI response did not contain a converted value.');
}

$rate = BigDecimal::of((string) $converted)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to static method of() on an unknown class Brick\Math\BigDecimal. [phpstan:class.notFound]

/**
* @throws MathException
*/
private function toBigDecimal(BigDecimal|float|int|string $value): BigDecimal
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 3 issues:

1. Method Otherguy\Currency\Results\ConversionResult::toBigDecimal() has invalid return type Brick\Math\BigDecimal. [phpstan:class.notFound]


2. PHPDoc tag @throws with type Brick\Math\Exception\MathException is not subtype of Throwable [phpstan:throws.notThrowable]


3. Parameter $value of method Otherguy\Currency\Results\ConversionResult::toBigDecimal() has invalid type Brick\Math\BigDecimal. [phpstan:class.notFound]

*/
private function toBigDecimal(BigDecimal|float|int|string $value): BigDecimal
{
return $value instanceof BigDecimal ? $value : BigDecimal::of($value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 issues:

1. Call to static method of() on an unknown class Brick\Math\BigDecimal. [phpstan:class.notFound]


2. Class Brick\Math\BigDecimal not found. [phpstan:class.notFound]

Comment thread tests/CurrencyTest.php
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class CurrencyTest extends TestCase
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class Otherguy\Currency\Tests\CurrencyTest extends unknown class PHPUnit\Framework\TestCase. [phpstan:class.notFound]

Comment thread tests/CurrencyTest.php

class CurrencyTest extends TestCase
{
#[Test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attribute class PHPUnit\Framework\Attributes\Test does not exist. [phpstan:attribute.notFound]

Comment thread tests/CurrencyTest.php
#[Test]
public function exposes_all_supported_currency_cases(): void
{
$this->assertCount(167, Currency::cases());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method Otherguy\Currency\Tests\CurrencyTest::assertCount(). [phpstan:method.notFound]

…rivers

- Add triage rules to Qlty configuration.
- Update `CONTRIBUTING.md` with prerequisites and setup instructions.
- Add `phpstan/phpstan-phpunit` dependency to `composer.json`.
- Create Lefthook configuration for automated checks.
- Refactor currency driver methods to improve structure and usage.
- Extend currency enum with comprehensive currency names.
- Introduce `MockHttpClientException` for better error handling in tests.
- Update various tests for new and modified functionality.

$response = $this->apiRequest('convert', $params);

$rate = BigDecimal::of($this->responseString($response, 'result', static::class))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to static method of() on an unknown class Brick\Math\BigDecimal. [phpstan:class.notFound]

$response = $this->apiRequest('convert', $params);

$rate = BigDecimal::of($this->responseString($response, 'result', static::class))
->dividedBy(BigDecimal::of((string) $this->amount), ConversionResult::DEFAULT_SCALE, RoundingMode::HALF_UP);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 issues:

1. Access to constant HALF_UP on an unknown class Brick\Math\RoundingMode. [phpstan:class.notFound]


2. Call to static method of() on an unknown class Brick\Math\BigDecimal. [phpstan:class.notFound]

*
* @return array<string, BigDecimal|float|int|string>
*/
protected function responseRates(array $response, string $key, string $provider): array
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method Otherguy\Currency\Drivers\BaseCurrencyDriver::responseRates() has invalid return type Brick\Math\BigDecimal. [phpstan:class.notFound]


$normalised = [];
foreach ($rates as $currency => $rate) {
if (!$rate instanceof BigDecimal && !is_float($rate) && !is_int($rate) && !is_string($rate)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class Brick\Math\BigDecimal not found. [phpstan:class.notFound]

throw new ApiException('CurrencyAPI response did not contain a converted value.');
}

$rate = BigDecimal::of((string) $converted)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to static method of() on an unknown class Brick\Math\BigDecimal. [phpstan:class.notFound]

Comment thread tests/CurrencyTest.php
$this->assertSame('United States Dollar', Currency::USD->displayName());
}

#[Test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attribute class PHPUnit\Framework\Attributes\Test does not exist. [phpstan:attribute.notFound]

Comment thread tests/CurrencyTest.php
#[Test]
public function try_from_code_returns_matching_case(): void
{
$this->assertSame(Currency::USD, Currency::tryFromCode('USD'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method Otherguy\Currency\Tests\CurrencyTest::assertSame(). [phpstan:method.notFound]

Comment thread tests/CurrencyTest.php
public function try_from_code_returns_matching_case(): void
{
$this->assertSame(Currency::USD, Currency::tryFromCode('USD'));
$this->assertNull(Currency::tryFromCode('XYZ'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method Otherguy\Currency\Tests\CurrencyTest::assertNull(). [phpstan:method.notFound]

Comment thread tests/CurrencyTest.php
$this->assertNull(Currency::tryFromCode('XYZ'));
}

#[Test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attribute class PHPUnit\Framework\Attributes\Test does not exist. [phpstan:attribute.notFound]

Comment thread tests/CurrencyTest.php
#[Test]
public function code_helper_coerces_strings_and_enum_values(): void
{
$this->assertSame('USD', Currency::code('USD'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method Otherguy\Currency\Tests\CurrencyTest::assertSame(). [phpstan:method.notFound]

…stan

- Apply #[Override] to driver methods so Rector's dry-run gate passes on PHP 8.3+
- Switch CI coverage driver from xdebug to pcov (matches local docs, faster)
- Disable qlty's phpstan plugin: it runs without composer autoload and produces
  ~100 false positives for PSR-18 / PHPUnit attribute classes; CI's phpstan run
  already covers this with the project's autoloader
- Document local pcov/xdebug install in CONTRIBUTING.md so phpunit's
  failOnWarning config doesn't surprise contributors
$this->assertSame('2013-12-24', $result->getDate());
$this->assertSame('1.636492', (string) $result->rate(Currency::USD));
$this->assertSame('1.196476', (string) $result->rate(Currency::EUR));
$this->assertSame('1.739516', (string) $result->rate(Currency::CAD));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 17 lines of similar code in 2 locations (mass = 163) [qlty:similar-code]

$this->assertSame('2001-02-16', $result->getDate());
$this->assertSame('3.67246', (string) $result->rate(Currency::AED));
$this->assertSame('144.529793', (string) $result->rate(Currency::ALL));
$this->assertSame('1.79', (string) $result->rate(Currency::ANG));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 16 lines of similar code in 2 locations (mass = 163) [qlty:similar-code]

Override the lint driver to install composer deps (so PSR-18, PHPUnit
attributes, and project classes resolve), point phpstan at the project's
autoloader, and keep phpstan.neon at the project root so its relative
ignoreErrors paths still match. Restores PR-comment mode without false
positives — verified locally via `qlty check --filter=phpstan`.
$this->assertSame('2013-12-24', $result->getDate());
$this->assertSame('1.636492', (string) $result->rate(Currency::USD));
$this->assertSame('1.196476', (string) $result->rate(Currency::EUR));
$this->assertSame('1.739516', (string) $result->rate(Currency::CAD));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 17 lines of similar code in 2 locations (mass = 163) [qlty:similar-code]

$this->assertSame('2001-02-16', $result->getDate());
$this->assertSame('3.67246', (string) $result->rate(Currency::AED));
$this->assertSame('144.529793', (string) $result->rate(Currency::ALL));
$this->assertSame('1.79', (string) $result->rate(Currency::ANG));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 16 lines of similar code in 2 locations (mass = 163) [qlty:similar-code]

@otherguy otherguy merged commit 7fd19f8 into main Apr 27, 2026
6 checks passed
@otherguy otherguy deleted the v2-modernization branch April 27, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant