Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
77.78% |
7 / 9 |
CRAP | |
90.00% |
54 / 60 |
| IndirectExchange | |
0.00% |
0 / 1 |
|
77.78% |
7 / 9 |
21.44 | |
90.00% |
54 / 60 |
| __construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
| registerCalculator | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
| quote | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
| anonymous function | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| getConversions | |
100.00% |
1 / 1 |
7 | |
100.00% |
25 / 25 |
|||
| initializeNode | |
100.00% |
1 / 1 |
1 | |
100.00% |
5 / 5 |
|||
| reconstructConversionChain | |
100.00% |
1 / 1 |
2 | |
100.00% |
8 / 8 |
|||
| getCalculator | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
| initializeCalculator | |
0.00% |
0 / 1 |
3.33 | |
66.67% |
4 / 6 |
|||
| <?php | |
| namespace Money\Exchange; | |
| use Money\Calculator; | |
| use Money\Calculator\BcMathCalculator; | |
| use Money\Calculator\GmpCalculator; | |
| use Money\Calculator\PhpCalculator; | |
| use Money\Currencies; | |
| use Money\Currency; | |
| use Money\CurrencyPair; | |
| use Money\Exception\UnresolvableCurrencyPairException; | |
| use Money\Exchange; | |
| /** | |
| * Provides a way to get an exchange rate through a minimal set of intermediate conversions. | |
| * | |
| * @author Michael Cordingley <Michael.Cordingley@gmail.com> | |
| */ | |
| final class IndirectExchange implements Exchange | |
| { | |
| /** | |
| * @var Calculator | |
| */ | |
| private static $calculator; | |
| /** | |
| * @var array | |
| */ | |
| private static $calculators = [ | |
| BcMathCalculator::class, | |
| GmpCalculator::class, | |
| PhpCalculator::class, | |
| ]; | |
| /** | |
| * @var Currencies | |
| */ | |
| private $currencies; | |
| /** | |
| * @var Exchange | |
| */ | |
| private $exchange; | |
| /** | |
| * @param Exchange $exchange | |
| * @param Currencies $currencies | |
| */ | |
| public function __construct(Exchange $exchange, Currencies $currencies) | |
| { | |
| $this->exchange = $exchange; | |
| $this->currencies = $currencies; | |
| } | |
| /** | |
| * @param string $calculator | |
| */ | |
| public static function registerCalculator($calculator) | |
| { | |
| if (is_a($calculator, Calculator::class, true) === false) { | |
| throw new \InvalidArgumentException('Calculator must implement '.Calculator::class); | |
| } | |
| array_unshift(self::$calculators, $calculator); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function quote(Currency $baseCurrency, Currency $counterCurrency) | |
| { | |
| try { | |
| return $this->exchange->quote($baseCurrency, $counterCurrency); | |
| } catch (UnresolvableCurrencyPairException $exception) { | |
| $rate = array_reduce($this->getConversions($baseCurrency, $counterCurrency), function ($carry, CurrencyPair $pair) { | |
| return static::getCalculator()->multiply($carry, $pair->getConversionRatio()); | |
| }, '1.0'); | |
| return new CurrencyPair($baseCurrency, $counterCurrency, $rate); | |
| } | |
| } | |
| /** | |
| * @param Currency $baseCurrency | |
| * @param Currency $counterCurrency | |
| * | |
| * @return CurrencyPair[] | |
| * | |
| * @throws UnresolvableCurrencyPairException | |
| */ | |
| private function getConversions(Currency $baseCurrency, Currency $counterCurrency) | |
| { | |
| $startNode = $this->initializeNode($baseCurrency); | |
| $startNode->discovered = true; | |
| $nodes = [$baseCurrency->getCode() => $startNode]; | |
| $frontier = new \SplQueue(); | |
| $frontier->enqueue($startNode); | |
| while ($frontier->count()) { | |
| /** @var \stdClass $currentNode */ | |
| $currentNode = $frontier->dequeue(); | |
| /** @var Currency $currentCurrency */ | |
| $currentCurrency = $currentNode->currency; | |
| if ($currentCurrency->equals($counterCurrency)) { | |
| return $this->reconstructConversionChain($nodes, $currentNode); | |
| } | |
| /** @var Currency $candidateCurrency */ | |
| foreach ($this->currencies as $candidateCurrency) { | |
| if (!isset($nodes[$candidateCurrency->getCode()])) { | |
| $nodes[$candidateCurrency->getCode()] = $this->initializeNode($candidateCurrency); | |
| } | |
| /** @var \stdClass $node */ | |
| $node = $nodes[$candidateCurrency->getCode()]; | |
| if (!$node->discovered) { | |
| try { | |
| // Check if the candidate is a neighbor. This will throw an exception if it isn't. | |
| $this->exchange->quote($currentCurrency, $candidateCurrency); | |
| $node->discovered = true; | |
| $node->parent = $currentNode; | |
| $frontier->enqueue($node); | |
| } catch (UnresolvableCurrencyPairException $exception) { | |
| // Not a neighbor. Move on. | |
| } | |
| } | |
| } | |
| } | |
| throw UnresolvableCurrencyPairException::createFromCurrencies($baseCurrency, $counterCurrency); | |
| } | |
| /** | |
| * @param Currency $currency | |
| * | |
| * @return \stdClass | |
| */ | |
| private function initializeNode(Currency $currency) | |
| { | |
| $node = new \stdClass(); | |
| $node->currency = $currency; | |
| $node->discovered = false; | |
| $node->parent = null; | |
| return $node; | |
| } | |
| /** | |
| * @param array $currencies | |
| * @param \stdClass $goalNode | |
| * | |
| * @return CurrencyPair[] | |
| */ | |
| private function reconstructConversionChain(array $currencies, \stdClass $goalNode) | |
| { | |
| $current = $goalNode; | |
| $conversions = []; | |
| while ($current->parent) { | |
| $previous = $currencies[$current->parent->currency->getCode()]; | |
| $conversions[] = $this->exchange->quote($previous->currency, $current->currency); | |
| $current = $previous; | |
| } | |
| return array_reverse($conversions); | |
| } | |
| /** | |
| * @return Calculator | |
| */ | |
| private function getCalculator() | |
| { | |
| if (null === self::$calculator) { | |
| self::$calculator = self::initializeCalculator(); | |
| } | |
| return self::$calculator; | |
| } | |
| /** | |
| * @return Calculator | |
| * | |
| * @throws \RuntimeException If cannot find calculator for money calculations | |
| */ | |
| private static function initializeCalculator() | |
| { | |
| $calculators = self::$calculators; | |
| foreach ($calculators as $calculator) { | |
| /** @var Calculator $calculator */ | |
| if ($calculator::supported()) { | |
| return new $calculator(); | |
| } | |
| } | |
| throw new \RuntimeException('Cannot find calculator for money calculations'); | |
| } | |
| } |