1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Ksdev\NBPCurrencyConverter; |
4
|
|
|
|
5
|
|
|
class ExRatesTableFinder |
6
|
|
|
{ |
7
|
|
|
const NBP_XML_URL = 'http://www.nbp.pl/kursy/xml/'; |
8
|
|
|
const MIN_PUB_DATE = '2002-01-02'; |
9
|
|
|
const MAX_ONE_TIME_API_REQ = 7; |
10
|
|
|
|
11
|
|
|
/** @var \GuzzleHttp\Client */ |
12
|
|
|
private $guzzle; |
13
|
|
|
|
14
|
|
|
/** @var ExRatesTableFactory */ |
15
|
|
|
private $ratesTableFactory; |
16
|
|
|
|
17
|
|
|
/** @var string */ |
18
|
|
|
private $cachePath; |
19
|
|
|
|
20
|
|
|
/** @var \DateTime */ |
21
|
|
|
private $soughtPubDate; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @param \GuzzleHttp\Client $guzzle |
25
|
|
|
* @param ExRatesTableFactory $ratesTableFactory |
26
|
|
|
* @param string $cachePath Optional path to an existing folder where the cache files will be stored |
27
|
|
|
* |
28
|
|
|
* @throws \Exception |
29
|
|
|
*/ |
30
|
|
|
public function __construct( |
31
|
|
|
\GuzzleHttp\Client $guzzle, |
32
|
|
|
ExRatesTableFactory $ratesTableFactory, |
33
|
|
|
$cachePath = '' |
34
|
|
|
) { |
35
|
|
|
$this->guzzle = $guzzle; |
36
|
|
|
$this->ratesTableFactory = $ratesTableFactory; |
37
|
|
|
if ($cachePath) { |
38
|
|
|
if (!is_dir($cachePath)) { |
39
|
|
|
throw new \Exception('Invalid cache path'); |
40
|
|
|
} |
41
|
|
|
$this->cachePath = rtrim((string)$cachePath, '/') . '/'; |
42
|
|
|
} |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Get the ExRatesTable instance |
47
|
|
|
* |
48
|
|
|
* @param \DateTime $pubDate Optional rates table publication date |
49
|
|
|
* |
50
|
|
|
* @return ExRatesTable |
51
|
|
|
* |
52
|
|
|
* @throws \Exception |
53
|
|
|
*/ |
54
|
|
|
public function getExRatesTable(\DateTime $pubDate = null) |
55
|
|
|
{ |
56
|
|
|
$this->setSoughtPubDate($pubDate); |
57
|
|
|
|
58
|
|
|
$i = 0; |
59
|
|
|
do { |
60
|
|
|
// Limit the number of times the loop repeats |
61
|
|
|
if ($i === self::MAX_ONE_TIME_API_REQ) { |
62
|
|
|
throw new \Exception('Max requests to api limit has been reached'); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
// If user doesn't want a specific date, try to get the rates from the last working day |
66
|
|
|
if (!$pubDate) { |
67
|
|
|
$this->soughtPubDate = $this->soughtPubDate->sub(new \DateInterval('P1D')); |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
// Try to find the file in cache, otherwise download it |
71
|
|
|
if ($this->cachePath && ($cachedXml = $this->getCachedXml())) { |
72
|
|
|
$rawContent = $cachedXml; |
73
|
|
|
} else { |
74
|
|
|
$rawContent = $this->downloadXml(); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
// If a specific date is sought then break, otherwise continue |
78
|
|
|
if ($pubDate) { |
79
|
|
|
break; |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
$i++; |
83
|
|
|
} while (!$rawContent); |
84
|
|
|
|
85
|
|
|
if (!$rawContent) { |
86
|
|
|
throw new \Exception('Exchange rates file not found'); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
return $this->ratesTableFactory->getInstance($rawContent); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Set the sought publication date necessary for finder operation |
94
|
|
|
* |
95
|
|
|
* @param \DateTime|null $pubDate |
96
|
|
|
* |
97
|
|
|
* @throws \Exception |
98
|
|
|
*/ |
99
|
|
|
private function setSoughtPubDate($pubDate) |
100
|
|
|
{ |
101
|
|
|
if ($pubDate instanceof \DateTime) { |
102
|
|
|
if (!($pubDate >= new \DateTime(self::MIN_PUB_DATE) && $pubDate <= new \DateTime())) { |
103
|
|
|
throw new \Exception('Invalid publication date'); |
104
|
|
|
} |
105
|
|
|
} else { |
106
|
|
|
$pubDate = new \DateTime(); |
107
|
|
|
} |
108
|
|
|
$this->soughtPubDate = $pubDate; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Get the raw xml content from a cache file |
113
|
|
|
* |
114
|
|
|
* @return string|int Content string or 0 if the file doesn't exist |
115
|
|
|
*/ |
116
|
|
|
private function getCachedXml() |
117
|
|
|
{ |
118
|
|
|
$filesArray = scandir($this->cachePath); |
119
|
|
|
$filename = $this->matchFilename($filesArray); |
120
|
|
|
|
121
|
|
|
if ($filename) { |
122
|
|
|
$rawContent = file_get_contents($this->cachePath . $filename); |
123
|
|
|
return $rawContent; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
return 0; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Get the raw xml content from the NBP api |
131
|
|
|
* |
132
|
|
|
* @return string|int Content string or 0 if the file doesn't exist |
133
|
|
|
* |
134
|
|
|
* @throws \Exception |
135
|
|
|
*/ |
136
|
|
|
private function downloadXml() |
137
|
|
|
{ |
138
|
|
|
$filename = $this->findFileInRatesDir(); |
139
|
|
|
if ($filename) { |
140
|
|
|
$response = $this->guzzle->get(self::NBP_XML_URL . $filename); |
141
|
|
|
if ($response->getStatusCode() === 200) { |
142
|
|
|
$rawContent = (string)$response->getBody(); |
143
|
|
|
if ($this->cachePath) { |
144
|
|
|
file_put_contents($this->cachePath . $filename, $rawContent); |
145
|
|
|
} |
146
|
|
|
return $rawContent; |
147
|
|
|
} else { |
148
|
|
|
throw new \Exception( |
149
|
|
|
"Invalid response status code: {$response->getStatusCode()} {$response->getReasonPhrase()}" |
150
|
|
|
); |
151
|
|
|
} |
152
|
|
|
} else { |
153
|
|
|
return 0; |
154
|
|
|
} |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Find the file related to the publication date |
159
|
|
|
* |
160
|
|
|
* @return string|int Filename or 0 if the file was not found |
161
|
|
|
* |
162
|
|
|
* @throws \Exception |
163
|
|
|
*/ |
164
|
|
|
private function findFileInRatesDir() |
165
|
|
|
{ |
166
|
|
|
$dirname = $this->constructDirname(); |
167
|
|
|
|
168
|
|
|
$response = $this->guzzle->get(self::NBP_XML_URL . $dirname); |
169
|
|
|
if ($response->getStatusCode() === 200) { |
170
|
|
|
$rawContent = (string)$response->getBody(); |
171
|
|
|
} else { |
172
|
|
|
throw new \Exception( |
173
|
|
|
"Invalid response status code: {$response->getStatusCode()} {$response->getReasonPhrase()}" |
174
|
|
|
); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
$filesArray = explode("\r\n", $rawContent); |
178
|
|
|
$filename = $this->matchFilename($filesArray); |
179
|
|
|
|
180
|
|
|
return $filename; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Construct the name of directory containing the files |
185
|
|
|
* |
186
|
|
|
* @return string |
187
|
|
|
*/ |
188
|
|
|
private function constructDirname() |
189
|
|
|
{ |
190
|
|
|
if ($this->soughtPubDate->format('Y') !== (new \DateTime())->format('Y')) { |
191
|
|
|
$dirname = "dir{$this->soughtPubDate->format('Y')}.txt"; |
192
|
|
|
} else { |
193
|
|
|
$dirname = 'dir.txt'; |
194
|
|
|
} |
195
|
|
|
return $dirname; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* Searches files array for a match to the publication date |
200
|
|
|
* |
201
|
|
|
* @todo Optimize to avoid unnecessary regex |
202
|
|
|
* |
203
|
|
|
* @param array $filesArray |
204
|
|
|
* |
205
|
|
|
* @return string|int Filename or 0 if the file was not found |
206
|
|
|
*/ |
207
|
|
|
private function matchFilename(array $filesArray) |
208
|
|
|
{ |
209
|
|
|
foreach ($filesArray as $filename) { |
210
|
|
|
if (preg_match('/(a\d{3}z' . $this->soughtPubDate->format('ymd') . ')/', $filename, $matches)) { |
211
|
|
|
$filename = "{$matches[1]}.xml"; |
212
|
|
|
return $filename; |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
return 0; |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|