diff --git a/phpstan.neon b/phpstan.neon index 4319fff..0eeead5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,3 +7,5 @@ parameters: - *DEPRECATED_* parallel: maximumNumberOfProcesses: 4 + stubFiles: + - stubs/Psr/Cache/CacheItemInterface.stub diff --git a/src/CurrencyAverageRates/Infrastructure/Collection/RatesFlatCollection.php b/src/CurrencyAverageRates/Infrastructure/Collection/RatesFlatCollection.php index 046fc11..acad80d 100644 --- a/src/CurrencyAverageRates/Infrastructure/Collection/RatesFlatCollection.php +++ b/src/CurrencyAverageRates/Infrastructure/Collection/RatesFlatCollection.php @@ -8,6 +8,7 @@ use MaciejSz\Nbp\CurrencyAverageRates\Domain\CurrencyAveragesTable; /** + * @implements \IteratorAggregate * @internal */ final class RatesFlatCollection implements \IteratorAggregate diff --git a/src/CurrencyAverageRates/Infrastructure/Collection/TablesDictionary.php b/src/CurrencyAverageRates/Infrastructure/Collection/TablesDictionary.php index 634e24f..0b1ae8a 100644 --- a/src/CurrencyAverageRates/Infrastructure/Collection/TablesDictionary.php +++ b/src/CurrencyAverageRates/Infrastructure/Collection/TablesDictionary.php @@ -12,9 +12,12 @@ */ final class TablesDictionary { - /** @var array */ + /** @var array */ private $tables; + /** + * @param array $tables + */ public function __construct(array $tables) { $this->tables = $tables; diff --git a/src/CurrencyAverageRates/Infrastructure/Mapper/CurrencyAverageRatesMapper.php b/src/CurrencyAverageRates/Infrastructure/Mapper/CurrencyAverageRatesMapper.php index 876b5b5..94ae740 100644 --- a/src/CurrencyAverageRates/Infrastructure/Mapper/CurrencyAverageRatesMapper.php +++ b/src/CurrencyAverageRates/Infrastructure/Mapper/CurrencyAverageRatesMapper.php @@ -15,7 +15,7 @@ class CurrencyAverageRatesMapper private $rateValidator; /** - * @param ThrowableValidator|null $rateValidator + * @param ?ThrowableValidator $rateValidator */ public function __construct(?ThrowableValidator $rateValidator = null) { diff --git a/src/CurrencyAverageRates/Infrastructure/Mapper/CurrencyAveragesTableMapper.php b/src/CurrencyAverageRates/Infrastructure/Mapper/CurrencyAveragesTableMapper.php index 0c7e1dc..2af9359 100644 --- a/src/CurrencyAverageRates/Infrastructure/Mapper/CurrencyAveragesTableMapper.php +++ b/src/CurrencyAverageRates/Infrastructure/Mapper/CurrencyAveragesTableMapper.php @@ -33,6 +33,7 @@ public function rawDataToDomainObject(array $tableData): CurrencyAveragesTable $tableLetter = $dataAccess->extractString('table'); $tableNumber = $dataAccess->extractString('no'); $effectiveDate = $dataAccess->extractDateTime('effectiveDate'); + /** @var array $rates */ $rates = $dataAccess->extractArray('rates'); return new CurrencyAveragesTable( diff --git a/src/CurrencyTradingRates/Infrastructure/Collection/RatesFlatCollection.php b/src/CurrencyTradingRates/Infrastructure/Collection/RatesFlatCollection.php index af1e031..6721159 100644 --- a/src/CurrencyTradingRates/Infrastructure/Collection/RatesFlatCollection.php +++ b/src/CurrencyTradingRates/Infrastructure/Collection/RatesFlatCollection.php @@ -8,6 +8,7 @@ use MaciejSz\Nbp\CurrencyTradingRates\Domain\CurrencyTradingTable; /** + * @implements \IteratorAggregate * @internal */ final class RatesFlatCollection implements \IteratorAggregate diff --git a/src/CurrencyTradingRates/Infrastructure/Mapper/CurrencyTradingRatesMapper.php b/src/CurrencyTradingRates/Infrastructure/Mapper/CurrencyTradingRatesMapper.php index 1754d64..2a0e049 100644 --- a/src/CurrencyTradingRates/Infrastructure/Mapper/CurrencyTradingRatesMapper.php +++ b/src/CurrencyTradingRates/Infrastructure/Mapper/CurrencyTradingRatesMapper.php @@ -15,7 +15,7 @@ class CurrencyTradingRatesMapper private $rateValidator; /** - * @param ThrowableValidator|null $rateValidator + * @param ?ThrowableValidator $rateValidator */ public function __construct(?ThrowableValidator $rateValidator = null) { @@ -26,15 +26,28 @@ public function __construct(?ThrowableValidator $rateValidator = null) } /** - * @param array $tableData - * @param array> $ratesData + * @param array{ + * table: string, + * no: string, + * tradingDate: string, + * effectiveDate: string, + * rates: array, + * } $tableData + * @param array< + * array{ + * currency: string, + * code: string, + * bid: float, + * ask: float, + * } + * > $rates * @return array */ - public function rawDataToDomainObjectCollection(array $tableData, array $ratesData): array + public function rawDataToDomainObjectCollection(array $tableData, array $rates): array { $collection = []; - foreach ($ratesData as $rateData) { - $currencyTradingRate = $this->rawDataToDomainObject($tableData, $rateData); + foreach ($rates as $rate) { + $currencyTradingRate = $this->rawDataToDomainObject($tableData, $rate); $collection[$currencyTradingRate->getCurrencyCode()] = $currencyTradingRate; } @@ -42,8 +55,19 @@ public function rawDataToDomainObjectCollection(array $tableData, array $ratesDa } /** - * @param array $tableData - * @param array $ratesData + * @param array{ + * table: string, + * no: string, + * tradingDate: string, + * effectiveDate: string, + * rates: array, + * } $tableData + * @param array{ + * currency: string, + * code: string, + * bid: float, + * ask: float, + * } $ratesData */ public function rawDataToDomainObject(array $tableData, array $ratesData): CurrencyTradingRate { diff --git a/src/CurrencyTradingRates/Infrastructure/Mapper/CurrencyTradingTableMapper.php b/src/CurrencyTradingRates/Infrastructure/Mapper/CurrencyTradingTableMapper.php index 8a2aa35..9a43bd9 100644 --- a/src/CurrencyTradingRates/Infrastructure/Mapper/CurrencyTradingTableMapper.php +++ b/src/CurrencyTradingRates/Infrastructure/Mapper/CurrencyTradingTableMapper.php @@ -25,7 +25,13 @@ public function __construct(?CurrencyTradingRatesMapper $ratesMapper = null) } /** - * @param array $tableData + * @param array{ + * table: string, + * no: string, + * tradingDate: string, + * effectiveDate: string, + * rates: array, + * } $tableData */ public function rawDataToDomainObject(array $tableData): CurrencyTradingTable { @@ -35,6 +41,16 @@ public function rawDataToDomainObject(array $tableData): CurrencyTradingTable $tableNumber = $dataAccess->extractString('no'); $tradingDate = $dataAccess->extractDateTime('tradingDate'); $effectiveDate = $dataAccess->extractDateTime('effectiveDate'); + /** + * @var array< + * array{ + * currency: string, + * code: string, + * bid: float, + * ask: float, + * } + * > $rates + */ $rates = $dataAccess->extractArray('rates'); return new CurrencyTradingTable( diff --git a/src/GoldRates/Infrastructure/Collection/GoldRatesCollection.php b/src/GoldRates/Infrastructure/Collection/GoldRatesCollection.php index 4fd3ba9..f744ef1 100644 --- a/src/GoldRates/Infrastructure/Collection/GoldRatesCollection.php +++ b/src/GoldRates/Infrastructure/Collection/GoldRatesCollection.php @@ -6,6 +6,9 @@ use MaciejSz\Nbp\GoldRates\Domain\GoldRate; +/** + * @implements \IteratorAggregate + */ class GoldRatesCollection implements \IteratorAggregate { /** @var iterable */ diff --git a/src/GoldRates/Infrastructure/Mapper/GoldRatesMapper.php b/src/GoldRates/Infrastructure/Mapper/GoldRatesMapper.php index 148e2db..8534fdc 100644 --- a/src/GoldRates/Infrastructure/Mapper/GoldRatesMapper.php +++ b/src/GoldRates/Infrastructure/Mapper/GoldRatesMapper.php @@ -11,7 +11,7 @@ use MaciejSz\Nbp\Shared\Infrastructure\Validator\ThrowableValidator; /** - * @implements TableMapper> + * @implements TableMapper */ class GoldRatesMapper implements TableMapper { @@ -19,7 +19,7 @@ class GoldRatesMapper implements TableMapper private $rateValidator; /** - * @param ThrowableValidator|null $rateValidator + * @param ?ThrowableValidator $rateValidator */ public function __construct(?ThrowableValidator $rateValidator = null) { diff --git a/src/Shared/Domain/DateFormatter/functions.php b/src/Shared/Domain/DateFormatter/functions.php index 6d5e150..ddf213b 100644 --- a/src/Shared/Domain/DateFormatter/functions.php +++ b/src/Shared/Domain/DateFormatter/functions.php @@ -8,12 +8,22 @@ function first_day_of_month(int $year, int $month): string { - return date('Y-m-d', strtotime("{$year}-{$month}-01")); + return date('Y-m-d', safe_strtotime("{$year}-{$month}-01")); } function last_day_of_month(int $year, int $month): string { - return date('Y-m-t', strtotime("{$year}-{$month}")); + return date('Y-m-t', safe_strtotime("{$year}-{$month}")); +} + +function safe_strtotime(string $datetime): int +{ + $result = strtotime($datetime); + if (false === $result) { + throw new InvalidDateException("Cannot convert string to time"); + } + + return $result; } /** @@ -91,7 +101,10 @@ function next_month($date): \DateTimeInterface return middle_of_month($date)->add(new \DateInterval('P1M')); } -function middle_of_month($date): \DateTimeInterface +/** + * @param string|\DateTimeInterface $date + */ +function middle_of_month($date): \DateTimeImmutable { return new \DateTimeImmutable(ensure_date_obj($date)->format('Y-m-') . '15'); } diff --git a/src/Shared/Domain/DateTimeBuilder.php b/src/Shared/Domain/DateTimeBuilder.php index 4642b7d..be119b8 100644 --- a/src/Shared/Domain/DateTimeBuilder.php +++ b/src/Shared/Domain/DateTimeBuilder.php @@ -12,9 +12,12 @@ class DateTimeBuilder /** @var string */ private $timezone; - /** @var Validator|null */ + /** @var ?Validator */ private $validator; + /** + * @param ?Validator $validator + */ public function __construct( string $timezone = self::DEFAULT_TIMEZONE, ?Validator $validator = null @@ -23,7 +26,7 @@ public function __construct( $this->validator = $validator; } - public function build(?string $date = null): \DateTimeInterface + public function build(string $date): \DateTimeInterface { if ($this->validator) { $this->validator->validate($date); diff --git a/src/Shared/Infrastructure/Client/NbpClient.php b/src/Shared/Infrastructure/Client/NbpClient.php index 789e3d5..8c09832 100644 --- a/src/Shared/Infrastructure/Client/NbpClient.php +++ b/src/Shared/Infrastructure/Client/NbpClient.php @@ -9,7 +9,7 @@ interface NbpClient { /** - * @return array + * @return array> */ public function send(NbpClientRequest $request): array; } diff --git a/src/Shared/Infrastructure/Mapper/TableMapper.php b/src/Shared/Infrastructure/Mapper/TableMapper.php index c087258..acb984e 100644 --- a/src/Shared/Infrastructure/Mapper/TableMapper.php +++ b/src/Shared/Infrastructure/Mapper/TableMapper.php @@ -13,5 +13,5 @@ interface TableMapper * @param array $tableData * @return T */ - public function rawDataToDomainObject(array $tableData): object; + public function rawDataToDomainObject(array $tableData); } diff --git a/src/Shared/Infrastructure/Repository/NbpWebRepository.php b/src/Shared/Infrastructure/Repository/NbpWebRepository.php index ec20d71..59ce436 100644 --- a/src/Shared/Infrastructure/Repository/NbpWebRepository.php +++ b/src/Shared/Infrastructure/Repository/NbpWebRepository.php @@ -102,9 +102,10 @@ public function getGoldRates(int $year, int $month): iterable } /** + * @template T * @param iterable> $results - * @param TableMapper $mapper - * @return iterable + * @param TableMapper $mapper + * @return iterable */ private function hydrate(iterable $results, TableMapper $mapper): iterable { diff --git a/src/Shared/Infrastructure/Serializer/ArrayDataAccess.php b/src/Shared/Infrastructure/Serializer/ArrayDataAccess.php index 1fe2a2c..8cdd43b 100644 --- a/src/Shared/Infrastructure/Serializer/ArrayDataAccess.php +++ b/src/Shared/Infrastructure/Serializer/ArrayDataAccess.php @@ -8,11 +8,14 @@ class ArrayDataAccess implements DataAccess { - /** @var array */ + /** @var array */ private $data; - /** @var DateTimeBuilder|null */ + /** @var ?DateTimeBuilder */ private $dateTimeBuilder = null; + /** + * @param array $data + */ public function __construct(array $data) { $this->data = $data; @@ -65,6 +68,9 @@ public function extractFloat(string $key): float public function extractDateTime(string $key): \DateTimeInterface { $value = $this->extract($key); + if (!is_string($value)) { + throw new Exception\UnexpectedDataType('valid date', gettype($value)); + } try { return $this->getDateTimeBuilder()->build($value); } catch (\Exception $e) { diff --git a/src/Shared/Infrastructure/Serializer/DataAccess.php b/src/Shared/Infrastructure/Serializer/DataAccess.php index ade4bc6..3b256f0 100644 --- a/src/Shared/Infrastructure/Serializer/DataAccess.php +++ b/src/Shared/Infrastructure/Serializer/DataAccess.php @@ -6,6 +6,9 @@ interface DataAccess { + /** + * @return mixed + */ public function extract(string $key); /** @@ -25,6 +28,7 @@ public function extractDateTime(string $key): \DateTimeInterface; /** * @throws Exception\UnexpectedDataType + * @return array */ public function extractArray(string $key): array; } diff --git a/src/Shared/Infrastructure/Serializer/Exception/DataKeyDoesNotExist.php b/src/Shared/Infrastructure/Serializer/Exception/DataKeyDoesNotExist.php index bdc75fb..30043ad 100644 --- a/src/Shared/Infrastructure/Serializer/Exception/DataKeyDoesNotExist.php +++ b/src/Shared/Infrastructure/Serializer/Exception/DataKeyDoesNotExist.php @@ -6,7 +6,7 @@ class DataKeyDoesNotExist extends \RuntimeException { - public function __construct($key = '', $code = 0, \Throwable $previous = null) + public function __construct(string $key = '', int $code = 0, \Throwable $previous = null) { parent::__construct("Data key '{$key}' does not exist", $code, $previous); } diff --git a/src/Shared/Infrastructure/Transport/CachingTransport.php b/src/Shared/Infrastructure/Transport/CachingTransport.php index 2851b37..eae8257 100644 --- a/src/Shared/Infrastructure/Transport/CachingTransport.php +++ b/src/Shared/Infrastructure/Transport/CachingTransport.php @@ -4,6 +4,7 @@ namespace MaciejSz\Nbp\Shared\Infrastructure\Transport; +use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; class CachingTransport implements Transport @@ -29,6 +30,7 @@ public function __construct( public function get(string $path): array { + /** @var CacheItemInterface>> $item */ $item = $this->cache->getItem($path); if (!$item->isHit()) { $item->set($this->backend->get($path)); diff --git a/src/Shared/Infrastructure/Transport/FileContentsTransport.php b/src/Shared/Infrastructure/Transport/FileContentsTransport.php index 669890a..9c19ff3 100644 --- a/src/Shared/Infrastructure/Transport/FileContentsTransport.php +++ b/src/Shared/Infrastructure/Transport/FileContentsTransport.php @@ -23,7 +23,10 @@ public function get(string $path): array $path = trim($path, '/'); $uri = "{$baseUri}/{$path}?format=json"; $contents = file_get_contents($uri); - /** @var array|null $data */ + if (false === $contents) { + throw new TransportException("Cannot get contents from {$uri}"); + } + /** @var ?array> $data */ $data = json_decode($contents, true); if (null === $data) { throw new TransportException("Cannot decode JSON data from {$uri}"); diff --git a/src/Shared/Infrastructure/Transport/GuzzleTransport.php b/src/Shared/Infrastructure/Transport/GuzzleTransport.php index 579093a..af00a67 100644 --- a/src/Shared/Infrastructure/Transport/GuzzleTransport.php +++ b/src/Shared/Infrastructure/Transport/GuzzleTransport.php @@ -33,7 +33,7 @@ public function get(string $path): array ], ]); - /** @var array|null $data */ + /** @var ?array> $data */ $data = json_decode($response->getBody()->getContents(), true); if (null === $data) { /** @var Uri $uri */ diff --git a/src/Shared/Infrastructure/Transport/Transport.php b/src/Shared/Infrastructure/Transport/Transport.php index 683e4f9..bf0ed64 100644 --- a/src/Shared/Infrastructure/Transport/Transport.php +++ b/src/Shared/Infrastructure/Transport/Transport.php @@ -7,7 +7,7 @@ interface Transport { /** - * @return array + * @return array> */ public function get(string $path): array; } diff --git a/src/Shared/Infrastructure/Validator/NbpNumericRateValidator.php b/src/Shared/Infrastructure/Validator/NbpNumericRateValidator.php index 4204703..2376d0b 100644 --- a/src/Shared/Infrastructure/Validator/NbpNumericRateValidator.php +++ b/src/Shared/Infrastructure/Validator/NbpNumericRateValidator.php @@ -19,7 +19,8 @@ public function isValid($value): bool public function validate($value): void { if (!$this->isValid($value)) { - throw new ValidationException("Invalid numeric rate value: '{$value}'"); + $stringRepresentation = (is_string($value) || is_numeric($value))? $value : 'value'; + throw new ValidationException("Invalid numeric rate value: '{$stringRepresentation}'"); } } } diff --git a/stubs/Psr/Cache/CacheItemInterface.stub b/stubs/Psr/Cache/CacheItemInterface.stub new file mode 100644 index 0000000..72e4a65 --- /dev/null +++ b/stubs/Psr/Cache/CacheItemInterface.stub @@ -0,0 +1,22 @@ + + * @return array */ private function fetchFixtureTables(): array { $fixturesRepository = new FixturesRepository(); - return $fixturesRepository->fetchArray('/api/cenyzlota/2023-03-01/2023-03-03/data'); + // @phpstan-ignore-next-line + return $fixturesRepository->fetchArray('/api/cenyzlota/2023-03-01/2023-03-03/data');; } } diff --git a/tests/unit/Shared/Domain/DateBuilderTest.php b/tests/unit/Shared/Domain/DateBuilderTest.php index ee5b4b4..c01a260 100644 --- a/tests/unit/Shared/Domain/DateBuilderTest.php +++ b/tests/unit/Shared/Domain/DateBuilderTest.php @@ -10,7 +10,7 @@ class DateBuilderTest extends TestCase { - public function testBuild() + public function testBuild(): void { $validator = $this->createMock(Validator::class); $validator->method('validate'); @@ -21,7 +21,7 @@ public function testBuild() $this->assertEquals('2023-01-01T00:00:00+01:00', $date->format('c')); } - public function testBuildInvalid() + public function testBuildInvalid(): void { $validator = $this->createMock(Validator::class); $exception = new \Exception('Invalid date'); diff --git a/tests/unit/Shared/Domain/DateFormatterTest.php b/tests/unit/Shared/Domain/DateFormatterTest.php index bd983f2..5ca03e2 100644 --- a/tests/unit/Shared/Domain/DateFormatterTest.php +++ b/tests/unit/Shared/Domain/DateFormatterTest.php @@ -73,7 +73,7 @@ public function testIsSameDay($date1, $date2, bool $expected): void self::assertSame($expected, is_same_day($date1, $date2)); } - public function testCompareDays() + public function testCompareDays(): void { self::assertSame(-1, compare_days('2020-01-01', '2020-01-02')); self::assertSame(0, compare_days('2020-01-01', '2020-01-01')); diff --git a/tests/unit/Shared/Infrastructure/Transport/FileContentsTransportTest.php b/tests/unit/Shared/Infrastructure/Transport/FileContentsTransportTest.php index 2829c21..7689a63 100644 --- a/tests/unit/Shared/Infrastructure/Transport/FileContentsTransportTest.php +++ b/tests/unit/Shared/Infrastructure/Transport/FileContentsTransportTest.php @@ -33,4 +33,22 @@ public function testGetInvalidJson(): void $transport = new FileContentsTransport('/'); $transport->get($path); } + + public function testGetInvalidPath(): void + { + self::expectException(TransportException::class); + self::expectExceptionMessage("Cannot get contents from /bogus"); + + $transport = new FileContentsTransport('/'); + + set_error_handler( + function (int $errno, string $errstr, string $errfile, int $errline): bool { + return true; + } + ); + + $transport->get('bogus'); + + restore_error_handler(); + } }