Completed
Push — master ( 6a43ea...7edb4c )
by Nikola
45:00
created

NationalBankOfSerbiaDomCrawlerSource   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 262
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 87.92%

Importance

Changes 5
Bugs 0 Features 1
Metric Value
wmc 37
c 5
b 0
f 1
lcom 1
cbo 12
dl 0
loc 262
ccs 131
cts 149
cp 0.8792
rs 8.6

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getName() 0 4 1
B fetch() 0 31 6
D parseXml() 0 85 14
B getPostParams() 0 27 5
B extractCsrfToken() 0 25 4
A load() 0 21 2
A validateRateType() 0 16 2
A validateCurrencyCode() 0 18 2
1
<?php
2
3
namespace RunOpenCode\ExchangeRate\Source;
4
5
use GuzzleHttp\Client as GuzzleClient;
6
use GuzzleHttp\Cookie\CookieJar;
7
use Psr\Log\LoggerAwareTrait;
8
use Psr\Log\LoggerInterface;
9
use RunOpenCode\ExchangeRate\Contract\RateInterface;
10
use RunOpenCode\ExchangeRate\Contract\SourceInterface;
11
use RunOpenCode\ExchangeRate\Exception\ConfigurationException;
12
use RunOpenCode\ExchangeRate\Exception\SourceNotAvailableException;
13
use RunOpenCode\ExchangeRate\Exception\UnknownCurrencyCodeException;
14
use RunOpenCode\ExchangeRate\Exception\UnknownRateTypeException;
15
use RunOpenCode\ExchangeRate\Model\Rate;
16
use Symfony\Component\DomCrawler\Crawler;
17
18
class NationalBankOfSerbiaDomCrawlerSource implements SourceInterface
19
{
20
    const SOURCE = 'http://www.nbs.rs/kursnaListaModul/naZeljeniDan.faces';
21
22
    use LoggerAwareTrait;
23
24
    /**
25
     * @var array
26
     */
27
    private $cache;
28
29 6
    public function __construct()
30
    {
31 6
        $this->cache = array();
32 6
    }
33
34
    /**
35
     * {@inheritdoc}
36
     */
37 6
    public function getName()
38
    {
39 6
        return 'national_bank_of_serbia';
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45 6
    public function fetch($currencyCode, $rateType = 'default', $date = null)
46
    {
47 3
        $this
48 6
            ->validateRateType($rateType)
49 6
            ->validateCurrencyCode($currencyCode, $rateType);
50
51 4
        if (is_null($date)) {
52 2
            $date = new \DateTime('now');
53 1
        }
54
55 4
        if (!array_key_exists($rateType, $this->cache)) {
56
57
            try {
58 4
                $this->load($date, $rateType);
59 3
            } catch (\Exception $e) {
60 2
                $exception = new SourceNotAvailableException(sprintf('Unable to load data from "%s" for "%s" of rate type "%s".', $this->getName(), $currencyCode, $rateType), 0, $e);
61
62 2
                if ($this->logger) {
63
                    $this->logger->emergency($exception->getMessage());
64
                }
65
66 2
                throw $exception;
67
            }
68 1
        }
69
70 2
        if (array_key_exists($currencyCode, $this->cache[$rateType])) {
71 2
            return $this->cache[$rateType][$currencyCode];
72
        }
73
74
        throw new ConfigurationException(sprintf('Source "%s" does not provide currency code "%s" for rate type "%s".', $this->getName(), $currencyCode, $rateType));
75
    }
76
77 6
    protected function validateRateType($rateType)
78
    {
79
        $knownTypes = array(
80 6
            'default', // It is actually a middle exchange rate
81 3
            'foreign_cache_buying',
82 3
            'foreign_cache_selling',
83 3
            'foreign_exchange_buying',
84
            'foreign_exchange_selling'
85 3
        );
86
87 6
        if (!in_array($rateType, $knownTypes, true)) {
88
            throw new UnknownRateTypeException(sprintf('Unknown rate type "%s" for source "%s", known types are: %s.', $rateType, $this->getName(), implode(', ', $knownTypes)));
89
        }
90
91 6
        return $this;
92
    }
93
94 6
    protected function validateCurrencyCode($currencyCode, $rateType)
95
    {
96
        $supports = array(
97
            'default' => array(
98 3
                'EUR', 'CHF'
99 6
            ),
100 3
            'foreign_cache_buying',
101 3
            'foreign_cache_selling',
102 3
            'foreign_exchange_buying',
103
            'foreign_exchange_selling'
104 3
        );
105
106 6
        if (!in_array($currencyCode, $supports[$rateType], true)) {
107 2
            throw new UnknownCurrencyCodeException(sprintf('Unknown currency code "%s" for source "%s" and rate type "%s".', $currencyCode, $this->getName(), $rateType));
108
        }
109
110 4
        return $this;
111
    }
112
113
    /**
114
     * @param \DateTime $date
115
     * @return RateInterface[]
116
     * @throws SourceNotAvailableException
117
     */
118 4
    protected function load(\DateTime $date, $rateType)
119
    {
120 4
        $guzzleClient = new GuzzleClient(array('cookies' => true));
121 4
        $jar = new CookieJar;
122
123 4
        $postParams = $this->getPostParams($date, $rateType, $this->extractCsrfToken($guzzleClient, $jar));
124
125 2
        $response = $guzzleClient->request('POST', self::SOURCE, array(
126 2
            'form_params' => $postParams,
127 1
            'cookies' => $jar
128 1
        ));
129
130 2
        $this->cache[$rateType] = array();
131
132
        /**
133
         * @var RateInterface $rate
134
         */
135 2
        foreach ($this->parseXml($response->getBody()->getContents(), $rateType) as $rate) {
136 2
            $this->cache[$rate->getRateType()][$rate->getCurrencyCode()] = $rate;
137 1
        }
138 2
    }
139
140 2
    protected function parseXml($xml, $rateType)
141
    {
142 2
        $rates = array();
143 2
        $stack = new \SplStack();
144 2
        $currentRate = array();
145 2
        $date = new \DateTime('now');
146
147 2
        $parser = xml_parser_create();
148
149
        xml_set_element_handler($parser, function($parser, $name, $attributes) use (&$rates, &$stack, &$currentRate) { // Element tag start
0 ignored issues
show
Unused Code introduced by
The parameter $attributes is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
150
151 2
            if (!empty($name)) {
152
153 2
                $stack->push($name);
154
155 2
                if ($name === 'ITEM') {
156 2
                    $currentRate = array();
157 1
                }
158 1
            }
159
160
        }, function($parser, $name) use (&$rates, &$stack, &$currentRate, &$rateType, &$date) { // Element tag end
161
162 2
            if (!empty($name)) {
163
164 2
                $stack->pop();
165
166 2
                if ($name === 'ITEM') {
167
168 2
                    if (strpos($rateType, 'buying') !== false) {
169
                        $value = $currentRate['buyingRate'];
170 2
                    } elseif (strpos($rateType, 'selling') !== false) {
171
                        $value = $currentRate['sellingRate'];
172
                    } else {
173 2
                        $value = $currentRate['middleRate'];
174
                    }
175
176 2
                    $rates[] = new Rate(
177 2
                        $this->getName(),
178 2
                        ($value / $currentRate['unit']),
179 2
                        $currentRate['currencyCode'],
180 1
                        $rateType,
181 1
                        $date,
182 2
                        'RSD',
183 2
                        new \DateTime('now'),
184 2
                        new \DateTime('now')
185 1
                    );
186 2
                    $currentRate = array();
187 1
                }
188 1
            }
189 2
        });
190
191
        xml_set_character_data_handler($parser, function($parser, $data) use (&$rates, &$stack, &$currentRate, &$date) { // Element tag data
192
193 2
            if (!empty($data)) {
194
195
196 2
                switch ($stack->top()) {
197 1
                    case 'DATE':
198 2
                        $date = \DateTime::createFromFormat('d.m.Y', $data);
199 2
                        break;
200 1
                    case 'CURRENCY':
201 2
                        $currentRate['currencyCode'] = trim($data);
202 2
                        break;
203 1
                    case 'UNIT':
204 2
                        $currentRate['unit'] = (int) trim($data);
205 2
                        break;
206 1
                    case 'BUYING_RATE':
207
                        $currentRate['buyingRate'] = (float) trim($data);
208
                        break;
209 1
                    case 'SELLING_RATE':
210
                        $currentRate['sellingRate'] = (float) trim($data);
211
                        break;
212 2
                    case 'MIDDLE_RATE':
213 2
                        $currentRate['middleRate'] = (float) trim($data);
214 2
                        break;
215 1
                }
216
217 1
            }
218 2
        });
219
220 2
        xml_parse($parser, $xml);
221 2
        xml_parser_free($parser);
222
223 2
        return $rates;
224
    }
225
226 2
    protected function getPostParams(\DateTime $date, $rateType, $csrfToken)
0 ignored issues
show
Unused Code introduced by
The parameter $csrfToken is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
227
    {
228
        return  array(
229 2
            'index:brKursneListe:' => '',
230 2
            'index:year' => $date->format('Y'),
231 2
            'index:inputCalendar1' => $date->format('d/m/Y'),
232 2
            'index:vrsta' => call_user_func(function($rateType) {
233
                switch ($rateType) {
234 2
                    case 'foreign_cache_buying':        // FALL TROUGH
235 1
                    case 'foreign_cache_selling':
236
                        return 1;
237
                        break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
238 1
                    case 'foreign_exchange_buying':     // FALL TROUGH
239 2
                    case 'foreign_exchange_selling':
240
                        return 2;
241
                        break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
242 1
                    default:
243 2
                        return 3;
244
                        break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
245 1
                }
246 2
            }, $rateType),
247 2
            'index:prikaz' => 3, // XML
248 2
            'index:buttonShow' => 'Show',
249 2
            'index' => 'index',
250
            'com.sun.faces.VIEW' => null
251 1
        );
252
    }
253
254 4
    protected function extractCsrfToken(GuzzleClient $guzzleClient, CookieJar $jar)
255
    {
256 4
        $response = $guzzleClient->request('GET', self::SOURCE, array('cookies' => $jar));
257 2
        $crawler = new Crawler($response->getBody()->getContents());
258
259 2
        $hiddens = $crawler->filter('input[type="hidden"]');
260
261
        /**
262
         * @var \DOMElement $hidden
263
         */
264 2
        foreach ($hiddens as $hidden) {
265
266 2
            if ($hidden->getAttribute('id') === 'com.sun.faces.VIEW') {
267 2
                return $hidden->getAttribute('value');
268
            }
269 1
        }
270
271
        $exception = new \RuntimeException('FATAL ERROR: National Bank of Serbia changed it\'s API, unable to extract token.');
272
273
        if ($this->logger) {
274
            $this->logger->emergency($exception->getMessage());
275
        }
276
277
        throw $exception;
278
    }
279
}
280