Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
77.78% covered (warning)
77.78%
7 / 9
CRAP
90.00% covered (success)
90.00%
54 / 60
IndirectExchange
0.00% covered (danger)
0.00%
0 / 1
77.78% covered (warning)
77.78%
7 / 9
21.44
90.00% covered (success)
90.00%
54 / 60
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 registerCalculator
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 quote
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
2 / 2
 anonymous function
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getConversions
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
25 / 25
 initializeNode
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 reconstructConversionChain
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
8 / 8
 getCalculator
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 initializeCalculator
0.00% covered (danger)
0.00%
0 / 1
3.33
66.67% covered (warning)
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');
    }
}