Skip to content

Commit 7c25f37

Browse files
committed
Fix qlty review findings
1 parent 08b9912 commit 7c25f37

17 files changed

Lines changed: 327 additions & 90 deletions

.editorconfig

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ trim_trailing_whitespace = true
1313
charset = utf-8
1414

1515
[*.md]
16-
indent_size = 4
16+
indent_size = 1
17+
18+
[*.php]
19+
indent_size = 1
20+
21+
[*.toml]
22+
indent_size = 2
1723

1824
[*.{yml,yaml,json,neon}]
1925
indent_size = 2

.github/workflows/ci.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,20 @@ jobs:
2121

2222
steps:
2323
- name: Checkout
24-
uses: actions/checkout@v4
24+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
25+
with:
26+
persist-credentials: false
2527

2628
- name: Setup PHP
27-
uses: shivammathur/setup-php@v2
29+
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f
2830
with:
2931
php-version: ${{ matrix.php }}
3032
extensions: json, mbstring
3133
coverage: ${{ matrix.php == '8.3' && 'xdebug' || 'none' }}
3234
tools: composer:v2
3335

3436
- name: Cache Composer dependencies
35-
uses: actions/cache@v4
37+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
3638
with:
3739
path: ~/.composer/cache
3840
key: composer-${{ matrix.php }}-${{ hashFiles('composer.json') }}

.qlty/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*
2+
!configs
3+
!configs/**
4+
!hooks
5+
!hooks/**
6+
!qlty.toml
7+
!.gitignore

.qlty/qlty.toml

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# This file was automatically generated by `qlty init`.
2+
# You can modify it to suit your needs.
3+
# We recommend you to commit this file to your repository.
4+
#
5+
# This configuration is used by both Qlty CLI and Qlty Cloud.
6+
#
7+
# Qlty CLI -- Code quality toolkit for developers
8+
# Qlty Cloud -- Fully automated Code Health Platform
9+
#
10+
# Try Qlty Cloud: https://qlty.sh
11+
#
12+
# For a guide to configuration, visit https://qlty.sh/d/config
13+
# Or for a full reference, visit https://qlty.sh/d/qlty-toml
14+
config_version = "0"
15+
16+
exclude_patterns = [
17+
"rector.php",
18+
"*_min.*",
19+
"*-min.*",
20+
"*.min.*",
21+
"**/.yarn/**",
22+
"**/*.d.ts",
23+
"**/assets/**",
24+
"**/bower_components/**",
25+
"**/build/**",
26+
"**/cache/**",
27+
"**/config/**",
28+
"**/db/**",
29+
"**/deps/**",
30+
"**/dist/**",
31+
"**/extern/**",
32+
"**/external/**",
33+
"**/generated/**",
34+
"**/Godeps/**",
35+
"**/gradlew/**",
36+
"**/mvnw/**",
37+
"**/node_modules/**",
38+
"**/protos/**",
39+
"**/seed/**",
40+
"**/target/**",
41+
"**/templates/**",
42+
"**/testdata/**",
43+
"**/vendor/**",
44+
]
45+
46+
test_patterns = [
47+
"**/test/**",
48+
"**/spec/**",
49+
"**/*.test.*",
50+
"**/*.spec.*",
51+
"**/*_test.*",
52+
"**/*_spec.*",
53+
"**/test_*.*",
54+
"**/spec_*.*",
55+
]
56+
57+
[smells]
58+
mode = "comment"
59+
60+
[[source]]
61+
name = "default"
62+
default = true
63+
64+
65+
[[plugin]]
66+
name = "actionlint"
67+
68+
[[plugin]]
69+
name = "editorconfig-checker"
70+
mode = "comment"
71+
72+
[[plugin]]
73+
name = "osv-scanner"
74+
75+
[[plugin]]
76+
name = "php-codesniffer"
77+
mode = "comment"
78+
79+
[[plugin]]
80+
name = "phpstan"
81+
mode = "comment"
82+
83+
[[plugin]]
84+
name = "radarlint-php"
85+
mode = "comment"
86+
87+
[[plugin]]
88+
name = "ripgrep"
89+
mode = "comment"
90+
91+
[[plugin]]
92+
name = "trufflehog"
93+
94+
[[plugin]]
95+
name = "zizmor"

phpcs.xml.dist

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="php-currency-api">
3+
<description>PSR-12 checks for qlty/php-codesniffer.</description>
4+
5+
<file>src</file>
6+
<file>examples</file>
7+
<file>rector.php</file>
8+
9+
<exclude-pattern>build/*</exclude-pattern>
10+
<exclude-pattern>tests/*</exclude-pattern>
11+
<exclude-pattern>vendor/*</exclude-pattern>
12+
13+
<rule ref="PSR12">
14+
<exclude name="Generic.Files.LineLength"/>
15+
</rule>
16+
</ruleset>

phpstan.neon

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,3 @@ parameters:
2020
-
2121
identifier: offsetAccess.nonOffsetAccessible
2222
path: src/Drivers/*.php
23-
-
24-
identifier: argument.type
25-
path: src/Drivers/*.php

rector.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
use Rector\Set\ValueObject\SetList;
99

1010
return RectorConfig::configure()
11-
->withPaths([
12-
__DIR__ . '/src',
13-
__DIR__ . '/tests',
14-
])
15-
->withSets([
16-
LevelSetList::UP_TO_PHP_83,
17-
SetList::CODE_QUALITY,
18-
SetList::TYPE_DECLARATION,
19-
PHPUnitSetList::PHPUNIT_100,
20-
])
21-
->withImportNames(removeUnusedImports: true);
11+
->withPaths([
12+
__DIR__ . '/src',
13+
__DIR__ . '/tests',
14+
])
15+
->withSets([
16+
LevelSetList::UP_TO_PHP_83,
17+
SetList::CODE_QUALITY,
18+
SetList::TYPE_DECLARATION,
19+
PHPUnitSetList::PHPUNIT_100,
20+
])
21+
->withImportNames(removeUnusedImports: true);

src/DriverFactory.php

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
use Otherguy\Currency\Drivers\MockCurrencyDriver;
1717
use Otherguy\Currency\Drivers\OpenExchangeRates;
1818
use Otherguy\Currency\Exceptions\DriverNotFoundException;
19+
use Otherguy\Currency\Exceptions\MissingDependencyException;
1920
use Psr\Http\Client\ClientInterface;
2021
use Psr\Http\Message\RequestFactoryInterface;
21-
use RuntimeException;
2222

2323
class DriverFactory
2424
{
@@ -35,14 +35,14 @@ class DriverFactory
3535
public function __construct(?array $drivers = null)
3636
{
3737
$this->drivers = $drivers ?? [
38-
'mock' => MockCurrencyDriver::class,
39-
'fixerio' => FixerIo::class,
40-
'currencylayer' => CurrencyLayer::class,
41-
'openexchangerates' => OpenExchangeRates::class,
42-
'exchangeratesapi' => ExchangeRatesApi::class,
43-
'frankfurter' => Frankfurter::class,
44-
'currencyapi' => CurrencyApi::class,
45-
'fastforex' => FastForex::class,
38+
'mock' => MockCurrencyDriver::class,
39+
'fixerio' => FixerIo::class,
40+
'currencylayer' => CurrencyLayer::class,
41+
'openexchangerates' => OpenExchangeRates::class,
42+
'exchangeratesapi' => ExchangeRatesApi::class,
43+
'frankfurter' => Frankfurter::class,
44+
'currencyapi' => CurrencyApi::class,
45+
'fastforex' => FastForex::class,
4646
];
4747
}
4848

@@ -124,24 +124,57 @@ public static function setDefault(?self $instance): void
124124
private function defaultClient(): ClientInterface
125125
{
126126
if (!class_exists(GuzzleClient::class)) {
127-
throw new RuntimeException(
128-
'No PSR-18 HTTP client supplied and guzzlehttp/guzzle is not installed. '
129-
. 'Either install guzzlehttp/guzzle, or pass a ClientInterface to DriverFactory::make().',
127+
throw new MissingDependencyException(
128+
'No PSR-18 HTTP client supplied and guzzlehttp/guzzle is '
129+
. 'not installed. Either install guzzlehttp/guzzle, or pass '
130+
. 'a ClientInterface to DriverFactory::make().',
130131
);
131132
}
132133

133-
return new GuzzleClient();
134+
$client = $this->buildDefaultClient();
135+
if (!$client instanceof ClientInterface) {
136+
throw new MissingDependencyException(
137+
'The installed guzzlehttp/guzzle package does not provide a PSR-18 '
138+
. 'ClientInterface implementation.',
139+
);
140+
}
141+
142+
return $client;
134143
}
135144

136145
private function defaultRequestFactory(): RequestFactoryInterface
137146
{
138147
if (!class_exists(GuzzleRequestFactory::class)) {
139-
throw new RuntimeException(
140-
'No PSR-17 RequestFactory supplied and http-interop/http-factory-guzzle is not installed. '
141-
. 'Either install http-interop/http-factory-guzzle, or pass a RequestFactoryInterface to DriverFactory::make().',
148+
throw new MissingDependencyException(
149+
'No PSR-17 RequestFactory supplied and '
150+
. 'http-interop/http-factory-guzzle is not installed. '
151+
. 'Either install http-interop/http-factory-guzzle, or pass '
152+
. 'a RequestFactoryInterface to DriverFactory::make().',
142153
);
143154
}
144155

145-
return new GuzzleRequestFactory();
156+
$requestFactory = $this->buildDefaultRequestFactory();
157+
if (!$requestFactory instanceof RequestFactoryInterface) {
158+
throw new MissingDependencyException(
159+
'The installed http-interop/http-factory-guzzle package does not '
160+
. 'provide a PSR-17 RequestFactoryInterface implementation.',
161+
);
162+
}
163+
164+
return $requestFactory;
165+
}
166+
167+
private function buildDefaultClient(): object
168+
{
169+
$class = GuzzleClient::class;
170+
171+
return new $class();
172+
}
173+
174+
private function buildDefaultRequestFactory(): object
175+
{
176+
$class = GuzzleRequestFactory::class;
177+
178+
return new $class();
146179
}
147180
}

src/Drivers/BaseCurrencyDriver.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Otherguy\Currency\Drivers;
66

7+
use Brick\Math\BigDecimal;
78
use DateTimeInterface;
89
use JsonException;
910
use Otherguy\Currency\Currency;
@@ -178,4 +179,64 @@ protected function apiRequest(string $endpoint, array $params = []): array
178179

179180
return $data;
180181
}
182+
183+
/**
184+
* @param array<array-key, mixed> $response
185+
*/
186+
protected function responseString(array $response, string $key, string $provider): string
187+
{
188+
$value = $response[$key] ?? null;
189+
if (!is_scalar($value)) {
190+
throw new ApiException("{$provider} response did not contain {$key}.");
191+
}
192+
193+
return (string) $value;
194+
}
195+
196+
/**
197+
* @param array<array-key, mixed> $response
198+
*/
199+
protected function optionalResponseString(array $response, string $key): ?string
200+
{
201+
$value = $response[$key] ?? null;
202+
203+
return is_scalar($value) ? (string) $value : null;
204+
}
205+
206+
/**
207+
* @param array<array-key, mixed> $response
208+
*/
209+
protected function responseInt(array $response, string $key, string $provider): int
210+
{
211+
$value = $response[$key] ?? null;
212+
if (!is_scalar($value)) {
213+
throw new ApiException("{$provider} response did not contain {$key}.");
214+
}
215+
216+
return (int) $value;
217+
}
218+
219+
/**
220+
* @param array<array-key, mixed> $response
221+
*
222+
* @return array<string, BigDecimal|float|int|string>
223+
*/
224+
protected function responseRates(array $response, string $key, string $provider): array
225+
{
226+
$rates = $response[$key] ?? null;
227+
if (!is_array($rates)) {
228+
throw new ApiException("{$provider} response did not contain {$key}.");
229+
}
230+
231+
$normalised = [];
232+
foreach ($rates as $currency => $rate) {
233+
if (!$rate instanceof BigDecimal && !is_float($rate) && !is_int($rate) && !is_string($rate)) {
234+
throw new ApiException("{$provider} response did not contain a numeric rate for {$currency}.");
235+
}
236+
237+
$normalised[(string) $currency] = $rate;
238+
}
239+
240+
return $normalised;
241+
}
181242
}

0 commit comments

Comments
 (0)