1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* This file is part of the Exchange Rate package, an RunOpenCode project. |
4
|
|
|
* |
5
|
|
|
* (c) 2017 RunOpenCode |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
*/ |
10
|
|
|
namespace RunOpenCode\ExchangeRate\Repository; |
11
|
|
|
|
12
|
|
|
use RunOpenCode\ExchangeRate\Contract\RateInterface; |
13
|
|
|
use RunOpenCode\ExchangeRate\Contract\RepositoryInterface; |
14
|
|
|
use RunOpenCode\ExchangeRate\Enum\RateType; |
15
|
|
|
use RunOpenCode\ExchangeRate\Exception\ExchangeRateException; |
16
|
|
|
use RunOpenCode\ExchangeRate\Exception\RuntimeException; |
17
|
|
|
use RunOpenCode\ExchangeRate\Utils\RateFilterUtil; |
18
|
|
|
use RunOpenCode\ExchangeRate\Model\Rate; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Class FileRepository |
22
|
|
|
* |
23
|
|
|
* File repository is simple file based repository for storing rates. |
24
|
|
|
* Rates are serialized into JSON and stored in plain text file, row by row. |
25
|
|
|
* |
26
|
|
|
* File repository can be used as repository for small number of rates. |
27
|
|
|
* |
28
|
|
|
* @package RunOpenCode\ExchangeRate\Repository |
29
|
|
|
*/ |
30
|
|
|
class FileRepository implements RepositoryInterface |
31
|
|
|
{ |
32
|
|
|
/** |
33
|
|
|
* File where all rates are persisted. |
34
|
|
|
* |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
protected $pathToFile; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Collection of loaded rates. |
41
|
|
|
* |
42
|
|
|
* @var array |
43
|
|
|
*/ |
44
|
|
|
protected $rates; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Collection of latest rates (to speed up search process). |
48
|
|
|
* |
49
|
|
|
* @var array |
50
|
|
|
*/ |
51
|
|
|
protected $latest; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* FileRepository constructor. |
55
|
|
|
* |
56
|
|
|
* @param string $pathToFile |
57
|
|
|
*/ |
58
|
5 |
|
public function __construct($pathToFile) |
59
|
|
|
{ |
60
|
5 |
|
$this->pathToFile = $pathToFile; |
61
|
5 |
|
$this->initialize(); |
62
|
5 |
|
$this->load(); |
63
|
5 |
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* {@inheritdoc} |
67
|
|
|
*/ |
68
|
5 |
|
public function save(array $rates) |
69
|
|
|
{ |
70
|
|
|
/** |
71
|
|
|
* @var RateInterface $rate |
72
|
|
|
*/ |
73
|
5 |
View Code Duplication |
foreach ($rates as $rate) { |
74
|
5 |
|
$this->rates[$this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName())] = $rate; |
75
|
|
|
} |
76
|
|
|
|
77
|
5 |
|
usort($this->rates, function (RateInterface $rate1, RateInterface $rate2) { |
78
|
3 |
|
return ($rate1->getDate() > $rate2->getDate()) ? -1 : 1; |
79
|
5 |
|
}); |
80
|
|
|
|
81
|
5 |
|
$data = ''; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @var RateInterface $rate |
85
|
|
|
*/ |
86
|
5 |
|
foreach ($this->rates as $rate) { |
87
|
5 |
|
$data .= $this->toJson($rate)."\n"; |
88
|
|
|
} |
89
|
|
|
|
90
|
5 |
|
file_put_contents($this->pathToFile, $data, LOCK_EX); |
91
|
|
|
|
92
|
5 |
|
$this->load(); |
93
|
5 |
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* {@inheritdoc} |
97
|
|
|
*/ |
98
|
1 |
|
public function delete(array $rates) |
99
|
|
|
{ |
100
|
|
|
/** |
101
|
|
|
* @var RateInterface $rate |
102
|
|
|
*/ |
103
|
1 |
View Code Duplication |
foreach ($rates as $rate) { |
104
|
1 |
|
unset($this->rates[$this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName())]); |
105
|
|
|
} |
106
|
|
|
|
107
|
1 |
|
$this->save(array()); |
108
|
1 |
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* {@inheritdoc} |
112
|
|
|
*/ |
113
|
2 |
|
public function has($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN) |
114
|
|
|
{ |
115
|
2 |
|
if ($date === null) { |
116
|
2 |
|
$date = new \DateTime('now'); |
117
|
|
|
} |
118
|
|
|
|
119
|
2 |
|
return array_key_exists($this->getRateKey($currencyCode, $date, $rateType, $sourceName), $this->rates); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* {@inheritdoc} |
124
|
|
|
*/ |
125
|
1 |
|
public function get($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN) |
126
|
|
|
{ |
127
|
1 |
|
if ($date === null) { |
128
|
1 |
|
$date = new \DateTime('now'); |
129
|
|
|
} |
130
|
|
|
|
131
|
1 |
|
if ($this->has($sourceName, $currencyCode, $date, $rateType)) { |
132
|
1 |
|
return $this->rates[$this->getRateKey($currencyCode, $date, $rateType, $sourceName)]; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
throw new ExchangeRateException(sprintf('Could not fetch rate for rate currency code "%s" and rate type "%s" on date "%s".', $currencyCode, $rateType, $date->format('Y-m-d'))); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* {@inheritdoc} |
140
|
|
|
*/ |
141
|
1 |
|
public function latest($sourceName, $currencyCode, $rateType = RateType::MEDIAN) |
142
|
|
|
{ |
143
|
|
|
/** |
144
|
|
|
* @var RateInterface $rate |
145
|
|
|
*/ |
146
|
1 |
|
foreach ($this->rates as $rate) { |
147
|
|
|
|
148
|
|
|
if ( |
149
|
1 |
|
$rate->getSourceName() === $sourceName |
150
|
|
|
&& |
151
|
1 |
|
$rate->getCurrencyCode() === $currencyCode |
152
|
|
|
&& |
153
|
1 |
|
$rate->getRateType() === $rateType |
154
|
|
|
) { |
155
|
1 |
|
return $rate; |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
throw new ExchangeRateException(sprintf('Could not fetch latest rate for rate currency code "%s" and rate type "%s" from source "%s".', $currencyCode, $rateType, $sourceName)); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* {@inheritdoc} |
164
|
|
|
*/ |
165
|
2 |
|
public function all(array $criteria = array()) |
166
|
|
|
{ |
167
|
2 |
|
if (count($criteria) == 0) { |
168
|
|
|
return $this->rates; |
169
|
|
|
} |
170
|
|
|
|
171
|
2 |
|
$result = array(); |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* @var RateInterface $rate |
175
|
|
|
*/ |
176
|
2 |
|
foreach ($this->rates as $rate) { |
177
|
|
|
|
178
|
2 |
|
if (RateFilterUtil::matches($rate, $criteria)) { |
179
|
2 |
|
$result[] = $rate; |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
|
183
|
2 |
|
return $this->paginate($result, $criteria); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* {@inheritdoc} |
188
|
|
|
*/ |
189
|
1 |
|
public function count() |
190
|
|
|
{ |
191
|
1 |
|
return count($this->rates); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Load all rates from file. |
196
|
|
|
* |
197
|
|
|
* @return RateInterface[] |
198
|
|
|
*/ |
199
|
5 |
|
protected function load() |
200
|
|
|
{ |
201
|
5 |
|
$this->rates = array(); |
202
|
5 |
|
$this->latest = array(); |
203
|
|
|
|
204
|
5 |
|
$handle = fopen($this->pathToFile, 'rb'); |
205
|
|
|
|
206
|
5 |
|
if (!$handle) { |
207
|
|
|
throw new RuntimeException(sprintf('Error opening file on path "%s".', $this->pathToFile)); |
208
|
|
|
} |
209
|
|
|
|
210
|
5 |
|
while (($line = fgets($handle)) !== false) { |
211
|
|
|
|
212
|
5 |
|
$rate = $this->fromJson($line); |
213
|
|
|
|
214
|
5 |
|
$this->rates[$this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName())] = $rate; |
215
|
|
|
|
216
|
5 |
|
$latestKey = sprintf('%s_%s_%s', $rate->getCurrencyCode(), $rate->getRateType(), $rate->getSourceName()); |
217
|
|
|
|
218
|
5 |
|
if (!isset($this->latest[$latestKey]) || ($this->latest[$latestKey]->getDate() < $rate->getDate())) { |
219
|
5 |
|
$this->latest[$latestKey] = $rate; |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
|
223
|
5 |
|
fclose($handle); |
224
|
|
|
|
225
|
5 |
|
return $this->rates; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* Builds rate key to speed up search. |
230
|
|
|
* |
231
|
|
|
* @param string $currencyCode |
232
|
|
|
* @param \DateTime $date |
233
|
|
|
* @param string $rateType |
234
|
|
|
* @param string $sourceName |
235
|
|
|
* @return string |
236
|
|
|
*/ |
237
|
5 |
View Code Duplication |
protected function getRateKey($currencyCode, $date, $rateType, $sourceName) |
|
|
|
|
238
|
|
|
{ |
239
|
5 |
|
return str_replace( |
240
|
5 |
|
array('%currency_code%', '%date%', '%rate_type%', '%source_name%'), |
241
|
5 |
|
array($currencyCode, $date->format('Y-m-d'), $rateType, $sourceName), |
242
|
5 |
|
'%currency_code%_%date%_%rate_type%_%source_name%' |
243
|
|
|
); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Initializes file storage. |
248
|
|
|
*/ |
249
|
5 |
|
protected function initialize() |
250
|
|
|
{ |
251
|
|
|
/** @noinspection MkdirRaceConditionInspection */ |
252
|
5 |
View Code Duplication |
if (!file_exists(dirname($this->pathToFile)) && !mkdir(dirname($this->pathToFile), 0777, true)) { |
|
|
|
|
253
|
|
|
throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile)); |
254
|
|
|
} |
255
|
|
|
|
256
|
5 |
View Code Duplication |
if (!file_exists($this->pathToFile) && !(touch($this->pathToFile) && chmod($this->pathToFile, 0777))) { |
|
|
|
|
257
|
|
|
throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile)); |
258
|
|
|
} |
259
|
|
|
|
260
|
5 |
|
if (!is_readable($this->pathToFile)) { |
261
|
|
|
throw new RuntimeException(sprintf('File on path "%s" for storing rates must be readable.', $this->pathToFile)); |
262
|
|
|
} |
263
|
|
|
|
264
|
5 |
|
if (!is_writable($this->pathToFile)) { |
265
|
|
|
throw new RuntimeException(sprintf('File on path "%s" for storing rates must be writeable.', $this->pathToFile)); |
266
|
|
|
} |
267
|
5 |
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Serialize rate to JSON string. |
271
|
|
|
* |
272
|
|
|
* @param RateInterface $rate Rate to serialize. |
273
|
|
|
* @return string JSON representation of rate. |
274
|
|
|
*/ |
275
|
5 |
|
protected function toJson(RateInterface $rate) |
276
|
|
|
{ |
277
|
5 |
|
return json_encode(array( |
278
|
5 |
|
'sourceName' => $rate->getSourceName(), |
279
|
5 |
|
'value' => $rate->getValue(), |
280
|
5 |
|
'currencyCode' => $rate->getCurrencyCode(), |
281
|
5 |
|
'rateType' => $rate->getRateType(), |
282
|
5 |
|
'date' => $rate->getDate()->format(\DateTime::ATOM), |
283
|
5 |
|
'baseCurrencyCode' => $rate->getBaseCurrencyCode(), |
284
|
5 |
|
'createdAt' => $rate->getCreatedAt()->format(\DateTime::ATOM), |
285
|
5 |
|
'modifiedAt' => $rate->getModifiedAt()->format(\DateTime::ATOM), |
286
|
|
|
)); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* Deserialize JSON string to Rate |
291
|
|
|
* |
292
|
|
|
* @param string $json Serialized rate. |
293
|
|
|
* @return Rate Deserialized rate. |
294
|
|
|
*/ |
295
|
5 |
|
protected function fromJson($json) |
296
|
|
|
{ |
297
|
5 |
|
$data = json_decode($json, true); |
298
|
|
|
|
299
|
5 |
|
return new Rate( |
300
|
5 |
|
$data['sourceName'], |
301
|
5 |
|
(float) $data['value'], |
302
|
5 |
|
$data['currencyCode'], |
303
|
5 |
|
$data['rateType'], |
304
|
5 |
|
\DateTime::createFromFormat(\DateTime::ATOM, $data['date']), |
305
|
5 |
|
$data['baseCurrencyCode'], |
306
|
5 |
|
\DateTime::createFromFormat(\DateTime::ATOM, $data['createdAt']), |
307
|
5 |
|
\DateTime::createFromFormat(\DateTime::ATOM, $data['modifiedAt']) |
308
|
|
|
); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Extract requested page from filter criteria. |
313
|
|
|
* |
314
|
|
|
* @param array $rates Rates to filter for pagination. |
315
|
|
|
* @param array $criteria Filter criteria. |
316
|
|
|
* @return RateInterface[] Paginated rates. |
317
|
|
|
*/ |
318
|
2 |
|
protected function paginate(array $rates, $criteria) |
319
|
|
|
{ |
320
|
2 |
|
if (!array_key_exists('offset', $criteria) && !array_key_exists('limit', $criteria)) { |
321
|
1 |
|
return $rates; |
322
|
|
|
} |
323
|
|
|
|
324
|
1 |
|
$range = array(); |
325
|
1 |
|
$offset = array_key_exists('offset', $criteria) ? $criteria['offset'] : 0; |
326
|
1 |
|
$limit = min((array_key_exists('limit', $criteria) ? $criteria['limit'] : count($rates)) + $offset, count($rates)); |
327
|
|
|
|
328
|
1 |
|
for ($i = $offset; $i < $limit; $i++) { |
329
|
1 |
|
$range[] = $rates[$i]; |
330
|
|
|
} |
331
|
|
|
|
332
|
1 |
|
return $range; |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.