Code Monkey home page Code Monkey logo

nbp-php's Introduction

nbp-php

API for accessing Polish National Bank (NBP - Narodowy Bank Polski) currency and commodities exchange rates.

Latest Version on Packagist Build Status Build Status No dependencies MIT License

Usage

Installing via composer

composer require maciej-sz/nbp-php

Minimal setup

<?php
require_once 'vendor/autoload.php';

use MaciejSz\Nbp\Service\CurrencyAverageRatesService;

$currencyAverages = CurrencyAverageRatesService::new();
$rate = $currencyAverages->fromDay('2023-01-02')->fromTable('A')->getRate('USD');

printf('%s rate is %d', $rate->getCurrencyCode(), $rate->getValue());
USD rate is 4.381100

Examples

All working examples from this README are included in the examples/ directory of the repository.

Services

CurrencyAverageRatesService

This service provides API for accessing average rates published in NBP tables.

fromMonth method

Returns flat collection of rates from all NBP tables at given month

$averageRatesFromJanuary = $currencyAverages->fromMonth(2023, 1);

foreach ($averageRatesFromJanuary as $rate) {
    printf(
        '%s rate from %s is %F' . PHP_EOL,
        $rate->getCurrencyCode(),
        $rate->getEffectiveDate()->format('Y-m-d'),
        $rate->getValue()
    );
}
THB rate from 2023-01-02 is 0.126700
USD rate from 2023-01-02 is 4.381100
AUD rate from 2023-01-02 is 2.976700
...

fromDay method

Returns a dictionary with NBP tables from given day.

$eurRateFromApril4th = $currencyAverages
    ->fromDay('2023-04-04')
    ->fromTable('A')
    ->getRate('EUR');

echo $eurRateFromApril4th->getValue(); // 4.6785

fromDayBefore method

Returns a dictionary with NBP tables from day before given day. This method can be useful in some bookkeeping applications when there is a legislatory need to calculate transfer prices. The legislation requires for the prices to be calculated using currency rate applied in the business day before the actual transfer date. Which is exactly what this method exposes.

Example:

$eurRateFromBeforeJanuary2nd = $currencyAverages
    ->fromDayBefore('2023-01-02')
    ->fromTable('A')
    ->getRate('EUR')
;

printf(
    '%s rate from %s is %F',
    $eurRateFromBeforeJanuary2nd->getCurrencyCode(),
    $eurRateFromBeforeJanuary2nd->getEffectiveDate()->format('Y-m-d'),
    $eurRateFromBeforeJanuary2nd->getValue()
);
EUR rate from 2022-12-30 is 4.689900

getMonthTablesA method

Returns the A table iterator from a specific month. Rates here are grouped into tables, which represent the actual data structure provided by NBP. To get the rates there needs to be second iteration:

$aTablesFromMarch = $currencyAverages->getMonthTablesA(2023, 3);

foreach ($aTablesFromMarch as $table) {
    foreach ($table->getRates() as $rate) {
        printf(
            '%s rate from table %s is %F' . PHP_EOL,
            $rate->getCurrencyCode(),
            $table->getNo(),
            $rate->getValue()
        );
    }
}
THB rate from table 042/A/NBP/2023 is 0.126700
USD rate from table 042/A/NBP/2023 is 4.409400
AUD rate from table 042/A/NBP/2023 is 2.981900
...
THB rate from table 043/A/NBP/2023 is 0.126600
USD rate from table 043/A/NBP/2023 is 4.400200
AUD rate from table 043/A/NBP/2023 is 2.963800
...

Example getting specific rate:

$aTablesFromMarch = $currencyAverages->getMonthTablesA(2023, 3);

foreach ($aTablesFromMarch as $table) {
    $chfRate = $table->getRate('CHF');
    printf(
        '%s rate from table %s is %F' . PHP_EOL,
        $chfRate->getCurrencyCode(),
        $table->getNo(),
        $chfRate->getValue()
    );
}
CHF rate from table 042/A/NBP/2023 is 4.703100
CHF rate from table 043/A/NBP/2023 is 4.674300
CHF rate from table 044/A/NBP/2023 is 4.728000
// ...

getMonthTablesB method

Returns the B table iterator from a specific month.

$bTablesFromMarch = $currencyAverages->getMonthTablesB(2022, 3);

foreach ($bTablesFromMarch as $table) {
    try {
        $rate = $table->getRate('MNT');
    } catch (CurrencyCodeNotFoundException $e) {
        continue;
    }
    printf(
        '%s rate from table %s is %F' . PHP_EOL,
        $rate->getCurrencyName(),
        $table->getNo(),
        $rate->getValue()
    );
}
tugrik (Mongolia) rate from table 009/B/NBP/2022 is 0.001500
tugrik (Mongolia) rate from table 010/B/NBP/2022 is 0.001529
tugrik (Mongolia) rate from table 011/B/NBP/2022 is 0.001469
tugrik (Mongolia) rate from table 012/B/NBP/2022 is 0.001457
tugrik (Mongolia) rate from table 013/B/NBP/2022 is 0.001417
Warning about missing currencies in table B

In table B there can be multiple currencies with the same code.

It is also possible, that a specific currency is present in table from one day, but is not present in table from the next day.

In such case you should not use the getRate($rate) method but rather iterate over all currencies returned by getRates().

Currency trading rates service

This service is used to get buy and sell currency rates from NBP tables.

fromMonth method

Returns trading rates from entire month.

$tradingRatesFromApril = $currencyTrading->fromMonth(2023, 4);

foreach ($tradingRatesFromApril as $rate) {
    printf(
        "%s rate from %s effective day traded on %s ask price is %s, bid price is %s\n",
        $rate->getCurrencyCode(),
        $rate->getEffectiveDate()->format('Y-m-d'),
        $rate->getTradingDate()->format('Y-m-d'),
        $rate->getAsk(),
        $rate->getBid()
    );
}
USD rate from 2023-04-03 effective day traded on 2023-03-31 ask price is 4.3338, bid price is 4.248
AUD rate from 2023-04-03 effective day traded on 2023-03-31 ask price is 2.9072, bid price is 2.8496
CAD rate from 2023-04-03 effective day traded on 2023-03-31 ask price is 3.2033, bid price is 3.1399
EUR rate from 2023-04-03 effective day traded on 2023-03-31 ask price is 4.7208, bid price is 4.6274
...

fromEffectiveDay method

Return rates from effective date.

$gbpFromApril4th = $currencyTrading->fromEffectiveDay('2023-04-04')->getRate('GBP');

printf(
    '%s rate from %s effective day traded on %s ask price is %s, bid price is %s',
    $gbpFromApril4th->getCurrencyCode(),
    $gbpFromApril4th->getEffectiveDate()->format('Y-m-d'),
    $gbpFromApril4th->getTradingDate()->format('Y-m-d'),
    $gbpFromApril4th->getAsk(),
    $gbpFromApril4th->getBid()
);
GBP rate from 2023-04-04 effective day traded on 2023-04-03 ask price is 5.3691, bid price is 5.2627

fromTradingDay method

Return rates from trading date.

$gbpFromApril4th = $currencyTrading->fromTradingDay('2023-04-04')->getRate('GBP');

printf(
    '%s rate from %s effective day traded on %s ask price is %s, bid price is %s',
    $gbpFromApril4th->getCurrencyCode(),
    $gbpFromApril4th->getEffectiveDate()->format('Y-m-d'),
    $gbpFromApril4th->getTradingDate()->format('Y-m-d'),
    $gbpFromApril4th->getAsk(),
    $gbpFromApril4th->getBid()
);
GBP rate from 2023-04-05 effective day traded on 2023-04-04 ask price is 5.4035, bid price is 5.2965

Gold rates service

This service is used to get gold commodity rates from NBP tables.

fromMonth method

Gets all rates from specific month.

$jan2013rates = $goldRates->fromMonth(2013, 1);

foreach ($jan2013rates as $rate) {
    printf(
        'Gold rate from %s is %F' . PHP_EOL,
        $rate->getDate()->format('Y-m-d'),
        $rate->getValue()
    );
}
Gold rate from 2013-01-02 is 165.830000
Gold rate from 2013-01-03 is 166.970000
Gold rate from 2013-01-04 is 167.430000
...

fromDay method

Returns a gold rate from specific date.

$goldRateFromJan2nd2014 = $goldRates->fromDay('2014-01-02');

printf(
    'Gold rate from %s is %F',
    $goldRateFromJan2nd2014->getDate()->format('Y-m-d'),
    $goldRateFromJan2nd2014->getValue()
);
Gold rate from 2014-01-02 is 116.350000

fromDayBefore method

Returns a gold rate from before a specific date.

$goldRateBeforeJan2nd = $goldRates->fromDayBefore('2014-01-02');

printf(
    'Gold rate from %s is %F',
    $goldRateBeforeJan2nd->getDate()->format('Y-m-d'),
    $goldRateBeforeJan2nd->getValue()
);
Gold rate from 2013-12-31 is 116.890000

Using cache

Note that a library implementing PSR-6 has to be provided in order to use the caching abilities.

The CachedTransport class is a proxy for all other transport implementations. This transport has to be backed by another transport, as it relies on it to make the actual requests that have not been cached yet.

Example

use Symfony\Component\Cache\Adapter\FilesystemAdapter as CachePoolAdapter;

// 1) create repository backed by caching transport
$cachePool = new CachePoolAdapter();
$cachingTransportFactory = CachingTransportFactory::new($cachePool);
$client = NbpWebClient::new(transportFactory: $cachingTransportFactory);
$nbpRepository = NbpWebRepository::new($client);

// 2) create needed services using cache-backed repository:
$goldRates = new GoldRatesService($nbpRepository);

// 3) run multiple times to check the effect of caching:
$start = microtime(true);
$goldRates->fromDayBefore('2013-05-15')->getValue();
$end = microtime(true);
$took = $end - $start;
printf('Getting the rate took %F ms', $took * 1000);

Using custom transport

The library uses Symfony HTTP Client and Guzzle as default transports. If those packages are not available then it falls back to the file_get_contents method. This may not be ideal in some situations. Especially when there is no access to HTTP client packages as may be the case when using PHP version prior to 8.0.

In such cases it is suggested to use different transport. It can be achieved by replacing the TransportFactory of the NbpClient with your own implementation.

$customTransportFactory = new class() implements TransportFactory {
    public function create(string $baseUri): Transport
    {
        return new class() implements Transport {
            public function get(string $path): array
            {
                echo "Requesting resource: {$path}" . PHP_EOL;

                $ch = curl_init();
                $url = NbpWebClient::BASE_URL . $path;
                curl_setopt($ch, CURLOPT_URL, $url);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
                try {
                    $output = curl_exec($ch);
                } finally {
                    curl_close($ch);
                }

                echo 'Request successful' . PHP_EOL;

                return json_decode($output, true);
            }
        };
    }
};

$client = NbpWebClient::new(transportFactory: $customTransportFactory);
$nbpRepository = NbpWebRepository::new($client);
$goldRates = GoldRatesService::new($nbpRepository);

$rate = $goldRates->fromDay('2022-01-03');

printf(
    'Gold rate from %s is %F',
    $rate->getDate()->format('Y-m-d'),
    $rate->getValue()
);
Requesting resource: /api/cenyzlota/2022-01-01/2022-01-31
Request successful
Gold rate from 2022-01-03 is 235.720000

Layers

Service layer

The service consists of facades for the package. Classes here are named services instead of facades due to common understanding of this word as something through which you access the internals of a system.

This layer provides a high level business-oriented methods for interacting with the NBP API. It exposes most common use cases of the nbp-php package and is the likely starting point for all applications using this package. One needs to interact with other layers only for more complex tasks.

The service layer is structured in the way that it directly communicates with the repository layer.

Repository layer

Repository layer allows getting data from NBP API by providing methods that closely reflect the NBP Web API. This layer operates on higher level, hydrated domain objects.

The only constraint here is that the repository layer operates on month-based dates. So for example you can get trading rates from entire month, but in order to retrieve a specific date you have to iterate through the retrieved month (you can use service layer for that). This is by design for the purpose of reducing network traffic to the NBP servers. It also allows simplifying caching on the transport layer, because there is no need for any cache-pooling logic.

Client layer

Client layer is a bridge between the repository and the transport layer. It processes request objects on the input, and then it uses the transport layer to fulfill those requests.

The requests thet client layer uses are higher-level requests then those on the transport layer. They implement NbpClientRequest interface.

Transport layer

The transport layer is responsible for directly interacting with the NBP API. A few independent transport implementations are provided for serving connections to the NBP API.

It is also equipped with a convenient factories which pick the most appropriate implementation depending on installed libraries and configuration.

nbp-php's People

Contributors

jarjak avatar maciej-sz avatar pjona avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

nbp-php's Issues

Change all date formatting functions to use OO interface

  • rewrite all methods from functions to \MaciejSz\Nbp\Shared\Domain\NbpDate class
  • get rid of the src/Shared/Domain/DateFormatter/functions.php file
  • ensure all date comparisons are beforehand normalized to the Europe/Warsaw timezone

Reverse `fromDay` and `fromTable` call order in CurrencyAverageRatesService

Currently the api requires to first call fromDay and then fromTable:

$averageRatesService->fromDay('2023-01-01')->fromTable('A');

This causes the need to iterate month tables on the spot. All tables must be iterated in order to build the TablesDictionary. This diminishes all advantages of using generator, because even if no data is actually used from those tables the full iteration of those tables takes place.

Switching the api to first call fromTable would allow to produce generators which will be called only on actual usage. Also it would not cause triggering the generator of other tables if only one table is picked.

Add support for returning NULL-s from service methods

Service methods that return null would allow using PHP 8 nullsafe operator, without the need to providing correct dates.

Example (current behavior)

$currencyAverages = CurrencyAverageRatesService::create();
// throws exeption, because there is no trading data available for this day:
$currencyAverages->formDay('2023-01-01');

Example (new behavior)

$currencyAverages = CurrencyAverageRatesService::create();
// returns null instead of throwing exception, so can be safely chained:
$currencyAverages->formDay('2023-01-01')?->fromTable('A')->getRate('USD');

Todo

  • Decide whether or not to implement this in old API (causes BC-break), or add new API

Possible aproaches for the new API solution

  1. Introduce methods in existing services (for example tryFromDay('2023-01-01')
  2. Introduce new api classes corresponding to existing ones (something like CurrencyAverageRatesService -> CurrencyAverageRatesNullableService)
  3. Introduce some kind of switch (config variable) in existing services to change the behavior of existing methods. No BC-break, but breaks SOLID.

Add support for invalid dates on higher layer

Currently invalid dates are thrown on the transport level.

Example 1

    public function testFromMonthInTheFuture()
    {
        $inTwoMonths = (new \DateTimeImmutable())->add(new \DateInterval('P2M'));
        $currencyAverages = CurrencyAverageRatesService::create();
        $rates = $currencyAverages->fromMonth(...extract_ym($inTwoMonths));
        var_dump($rates->fromTable('A')->toArray());
    }

Causes: ClientException : HTTP/1.1 400 B��dny zakres dat / Invalid date range returned for...

Example 2

    public function testFromMonthBeforeRecords()
    {
        $currencyAverages = CurrencyAverageRatesService::create();
        $rates = $currencyAverages->fromMonth(1970, 1);
        var_dump($rates->fromTable('A')->toArray());
    }

Causes:
ClientException : HTTP/1.1 404 Bark danych / No data available returned for...

Example 3

    public function testFromDayBeforeInvalid()
    {
        $inTwoMonths = (new \DateTimeImmutable())->add(new \DateInterval('P2M'));
        self::expectException(RateNotFoundException::class);
        self::expectExceptionMessage("Gold rate from day before {$inTwoMonths->format('Y-m-d')} has not been found");

        $goldRates = GoldRatesService::create();
        $goldRates->fromDayBefore($inTwoMonths);
    }

Causes:
ClientException : HTTP/1.1 400 B��dny zakres dat / Invalid date range returned for ...

Todo

  • Move exceptions to higher layer
  • Handle caching of invalid requests (?)
  • Tests for average rates
  • Tests for trading rates
  • Tests for gold rates

Requesting rate from current month causes invalid request

When requesting date from current month the request is build with entire month range.

Reproduce steps

  1. Issue request from current month (for example 2023-05-05 assuming it is may of 2023 now)
  2. The client will build request form entire month range - form 2023-05-01 to 2023-05-31
  3. This results in NBP request from entire month: GET https://api.nbp.pl/api/exchangerates/tables/A/2023-05-01/2023-05-31.
  4. Return status from serfer is 400 BadRequest

Expected result

  1. Request is issued with date range capped at today
  2. The requested rate is returned without errors

Technical debt

This issue tracks the technical debt todos.

Features

  • add __set_state methods on all domain objects

Enhancements

  • Improve TransportFactory - use better system for detecting 3rd party libraries along with versions (not only class_exists)
  • decide whether or not to rely on sorting provided by API, or add additional sorting on the repository layer

Refactor

  • change creating default instances for optional (nullable) parameters to use the elvis operator - throughout the project
  • #12
  • Move caching to from Transport layer to Repository layer

Tests

  • refactor all calls from $this->expectException to self::expectException and from $this->assert* to self::assert*
  • move calls to expectException just before the exception occurs
  • add e2e tests using var_export (requires the __set_state feature)
  • refactor tests to use provide specific data sets, example: function fetchApiExxhangeRates($date, $date2) { return $this->fetchArray("/api/exchangerates/tables/$date1/$date2" ...; move array shapes there
  • Change all non-mock objects to be created by createStub method instead of createMock
  • Replace willReturnOnConsecutiveCalls and willReturnMap with expectations (according to the latest PhpUnit 10 guidelines)

CI/CD

  • Add Deptrac
  • Run tests on multiple PHP versions

Documentation

  • add array shapes to all typehints in dockblocks instead of array
  • write documentation using https://www.mkdocs.org/
  • add a diagram of the package architecture

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.