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 Doctrine\DBAL\Connection; |
13
|
|
|
use Doctrine\DBAL\Driver\Statement; |
14
|
|
|
use Doctrine\DBAL\Query\QueryBuilder; |
15
|
|
|
use Doctrine\DBAL\Schema\Schema; |
16
|
|
|
use RunOpenCode\ExchangeRate\Contract\RateInterface; |
17
|
|
|
use RunOpenCode\ExchangeRate\Contract\RepositoryInterface; |
18
|
|
|
use RunOpenCode\ExchangeRate\Enum\RateType; |
19
|
|
|
use RunOpenCode\ExchangeRate\Exception\ExchangeRateException; |
20
|
|
|
use RunOpenCode\ExchangeRate\Model\Rate; |
21
|
|
|
use RunOpenCode\ExchangeRate\Utils\FilterUtilHelper; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Class DoctrineDbalRepository |
25
|
|
|
* |
26
|
|
|
* Dbal repository uses http://www.doctrine-project.org/projects/dbal.html for rates persistance. |
27
|
|
|
* |
28
|
|
|
* @package RunOpenCode\ExchangeRate\Repository |
29
|
|
|
*/ |
30
|
|
|
class DoctrineDbalRepository implements RepositoryInterface |
31
|
|
|
{ |
32
|
|
|
use FilterUtilHelper; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var Connection |
36
|
|
|
*/ |
37
|
|
|
private $connection; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var string |
41
|
|
|
*/ |
42
|
|
|
private $tableName; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var RateInterface[] |
46
|
|
|
*/ |
47
|
|
|
private $identityMap = []; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* DoctrineDbalRepository constructor. |
51
|
|
|
* |
52
|
|
|
* @param Connection $connection Dbal connection. |
53
|
|
|
* @param string $tableName Table name in which rates will be stored. |
54
|
|
|
* @param bool $initialize Should repository initialize table. |
55
|
|
|
*/ |
56
|
5 |
|
public function __construct(Connection $connection, $tableName = 'runopencode_exchange_rate', $initialize = false) |
57
|
|
|
{ |
58
|
5 |
|
$this->connection = $connection; |
59
|
5 |
|
$this->tableName = $tableName; |
60
|
|
|
|
61
|
5 |
|
if ($initialize) { |
62
|
5 |
|
$this->initialize(); |
63
|
|
|
} |
64
|
5 |
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* {@inheritdoc} |
68
|
|
|
*/ |
69
|
4 |
|
public function save(array $rates) |
70
|
|
|
{ |
71
|
4 |
|
$this->connection->beginTransaction(); |
72
|
|
|
|
73
|
|
|
try { |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @var RateInterface $rate |
77
|
|
|
*/ |
78
|
4 |
|
foreach ($rates as $rate) { |
79
|
|
|
|
80
|
4 |
|
if ($this->has($rate->getSourceName(), $rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType())) { |
81
|
|
|
|
82
|
|
|
$this->connection->executeQuery(sprintf('UPDATE %s SET rate_value = :rate_value, modified_at = :modified_at WHERE source_name = :source_name AND currency_code = :currency_code AND rate_date = :rate_date AND rate_type = :rate_type;', $this->tableName), [ |
83
|
|
|
'rate_value' => (float) $rate->getValue(), |
84
|
|
|
'source_name' => $rate->getSourceName(), |
85
|
|
|
'currency_code' => $rate->getCurrencyCode(), |
86
|
|
|
'rate_date' => $rate->getDate()->format('Y-m-d'), |
87
|
|
|
'rate_type' => $rate->getRateType(), |
88
|
|
|
'modified_at' => date('Y-m-d H:i:s'), |
89
|
|
|
]); |
90
|
|
|
|
91
|
|
|
} else { |
92
|
|
|
|
93
|
4 |
|
$this->connection->executeQuery(sprintf('INSERT INTO %s (source_name, rate_value, currency_code, rate_type, rate_date, base_currency_code, created_at, modified_at) VALUES (:source_name, :rate_value, :currency_code, :rate_type, :rate_date, :base_currency_code, :created_at, :modified_at);', $this->tableName), [ |
94
|
4 |
|
'source_name' => $rate->getSourceName(), |
95
|
4 |
|
'rate_value' => (float) $rate->getValue(), |
96
|
4 |
|
'currency_code' => $rate->getCurrencyCode(), |
97
|
4 |
|
'rate_type' => $rate->getRateType(), |
98
|
4 |
|
'rate_date' => $rate->getDate()->format('Y-m-d'), |
99
|
4 |
|
'base_currency_code' => $rate->getBaseCurrencyCode(), |
100
|
4 |
|
'created_at' => date('Y-m-d H:i:s'), |
101
|
4 |
|
'modified_at' => date('Y-m-d H:i:s'), |
102
|
|
|
]); |
103
|
|
|
} |
104
|
|
|
} |
105
|
|
|
|
106
|
4 |
|
$this->connection->commit(); |
107
|
|
|
|
108
|
|
|
} catch (\Exception $e) { |
109
|
|
|
$this->connection->rollBack(); |
110
|
|
|
throw $e; |
111
|
|
|
} |
112
|
4 |
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* {@inheritdoc} |
116
|
|
|
*/ |
117
|
1 |
|
public function delete(array $rates) |
118
|
|
|
{ |
119
|
1 |
|
$this->connection->beginTransaction(); |
120
|
|
|
|
121
|
|
|
try { |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @var RateInterface $rate |
125
|
|
|
*/ |
126
|
1 |
|
foreach ($rates as $rate) { |
127
|
1 |
|
$key = $this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName()); |
128
|
|
|
|
129
|
1 |
|
if (isset($this->identityMap[$key])) { |
130
|
1 |
|
unset($this->identityMap[$key]); |
131
|
|
|
} |
132
|
|
|
|
133
|
1 |
|
$this->connection->executeQuery(sprintf('DELETE FROM %s WHERE source_name = :source_name AND currency_code = :currency_code AND rate_date = :rate_date AND rate_type = :rate_type;', $this->tableName), [ |
134
|
1 |
|
'source_name' => $rate->getSourceName(), |
135
|
1 |
|
'currency_code' => $rate->getCurrencyCode(), |
136
|
1 |
|
'rate_date' => $rate->getDate()->format('Y-m-d'), |
137
|
1 |
|
'rate_type' => $rate->getRateType(), |
138
|
|
|
]); |
139
|
|
|
} |
140
|
|
|
|
141
|
1 |
|
$this->connection->commit(); |
142
|
|
|
} catch (\Exception $e) { |
143
|
|
|
$this->connection->rollBack(); |
144
|
|
|
throw $e; |
145
|
|
|
} |
146
|
1 |
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* {@inheritdoc} |
150
|
|
|
*/ |
151
|
4 |
|
public function has($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN) |
152
|
|
|
{ |
153
|
4 |
|
if ($date === null) { |
154
|
1 |
|
$date = new \DateTime('now'); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
try { |
158
|
4 |
|
return (bool) $this->get($sourceName, $currencyCode, $date, $rateType); |
159
|
4 |
|
} catch (ExchangeRateException $e) { |
160
|
4 |
|
return false; |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* {@inheritdoc} |
166
|
|
|
*/ |
167
|
4 |
|
public function get($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN) |
168
|
|
|
{ |
169
|
4 |
|
if ($date === null) { |
170
|
1 |
|
$date = new \DateTime('now'); |
171
|
|
|
} |
172
|
|
|
|
173
|
4 |
|
$key = $this->getRateKey($currencyCode, $date, $rateType, $sourceName); |
174
|
|
|
|
175
|
4 |
|
if (!isset($this->identityMap[$key])) { |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* @var array $result |
179
|
|
|
*/ |
180
|
4 |
|
$result = $this->connection->fetchAll( |
181
|
4 |
|
sprintf('SELECT R.* FROM %s R WHERE R.source_name = :source_name AND R.currency_code = :currency_code AND R.rate_date = :rate_date AND R.rate_type = :rate_type;', $this->tableName), |
182
|
|
|
[ |
183
|
4 |
|
'source_name' => $sourceName, |
184
|
4 |
|
'currency_code' => $currencyCode, |
185
|
4 |
|
'rate_date' => $date->format('Y-m-d'), |
186
|
4 |
|
'rate_type' => $rateType, |
187
|
|
|
] |
188
|
|
|
); |
189
|
|
|
|
190
|
4 |
View Code Duplication |
if (0 === count($result)) { |
|
|
|
|
191
|
4 |
|
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'))); |
192
|
|
|
} |
193
|
|
|
|
194
|
1 |
|
$this->identityMap[$key] = $this->buildRateFromTableRowData($result[0]); |
195
|
|
|
} |
196
|
|
|
|
197
|
1 |
|
return $this->identityMap[$key]; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* {@inheritdoc} |
202
|
|
|
*/ |
203
|
1 |
|
public function latest($sourceName, $currencyCode, $rateType = RateType::MEDIAN) |
204
|
|
|
{ |
205
|
|
|
/** |
206
|
|
|
* @var array $result |
207
|
|
|
*/ |
208
|
1 |
|
$result = $this->connection->fetchAll( |
209
|
1 |
|
sprintf('SELECT R.* FROM %s R WHERE R.source_name = :source_name AND R.currency_code = :currency_code AND R.rate_type = :rate_type ORDER BY R.rate_date DESC;', $this->tableName), |
210
|
|
|
[ |
211
|
1 |
|
'source_name' => $sourceName, |
212
|
1 |
|
'currency_code' => $currencyCode, |
213
|
1 |
|
'rate_type' => $rateType, |
214
|
|
|
] |
215
|
|
|
); |
216
|
|
|
|
217
|
1 |
View Code Duplication |
if (0 === count($result)) { |
|
|
|
|
218
|
|
|
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'))); |
219
|
|
|
} |
220
|
|
|
|
221
|
1 |
|
$rate = $this->buildRateFromTableRowData($result[0]); |
222
|
1 |
|
$key = $this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName()); |
|
|
|
|
223
|
|
|
|
224
|
1 |
|
return ($this->identityMap[$key] = $rate); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* {@inheritdoc} |
229
|
|
|
*/ |
230
|
2 |
|
public function all(array $criteria = array()) |
231
|
|
|
{ |
232
|
|
|
/** |
233
|
|
|
* @var QueryBuilder $queryBuilder |
234
|
|
|
*/ |
235
|
2 |
|
$queryBuilder = $this->connection->createQueryBuilder(); |
236
|
|
|
|
237
|
|
|
$queryBuilder |
238
|
2 |
|
->select('R.*') |
239
|
2 |
|
->from($this->tableName, 'R') |
240
|
2 |
|
->addOrderBy('R.rate_date', 'DESC') |
241
|
2 |
|
->addOrderBy('R.source_name', 'ASC') |
242
|
2 |
|
->addOrderBy('R.currency_code', 'ASC') |
243
|
2 |
|
->addOrderBy('R.rate_type', 'ASC') |
244
|
|
|
; |
245
|
|
|
|
246
|
2 |
|
if (0 !== count($currencyCodes = self::extractArrayCriteria('currencyCode', $criteria))) { |
247
|
|
|
$queryBuilder |
248
|
|
|
->andWhere('R.currency_code IN (:currency_codes)') |
249
|
|
|
->setParameter(':currency_codes', $currencyCodes, Connection::PARAM_STR_ARRAY); |
250
|
|
|
} |
251
|
|
|
|
252
|
2 |
|
if (0 !== count($rateTypes = self::extractArrayCriteria('rateType', $criteria))) { |
253
|
|
|
$queryBuilder |
254
|
|
|
->andWhere('R.rate_type IN (:rate_types)') |
255
|
|
|
->setParameter(':rate_types', $rateTypes, Connection::PARAM_STR_ARRAY); |
256
|
|
|
} |
257
|
|
|
|
258
|
2 |
|
if (0 !== count($sourceNames = self::extractArrayCriteria('sourceName', $criteria))) { |
259
|
|
|
$queryBuilder |
260
|
1 |
|
->andWhere('R.source_name IN (:source_names)') |
261
|
1 |
|
->setParameter(':source_names', $sourceNames, Connection::PARAM_STR_ARRAY); |
262
|
|
|
} |
263
|
|
|
|
264
|
2 |
|
if (isset($criteria['dateFrom'])) { |
265
|
|
|
$queryBuilder |
266
|
|
|
->andWhere('R.rate_date >= :date_from') |
267
|
|
|
->setParameter('date_from', $criteria['dateFrom']->format('Y-m-d')); |
268
|
|
|
} |
269
|
|
|
|
270
|
2 |
|
if (isset($criteria['dateTo'])) { |
271
|
|
|
$queryBuilder |
272
|
|
|
->andWhere('R.rate_date <= :date_to') |
273
|
|
|
->setParameter('date_to', $criteria['dateTo']->format('Y-m-d')); |
274
|
|
|
} |
275
|
|
|
|
276
|
2 |
|
if (isset($criteria['onDate'])) { |
277
|
|
|
$queryBuilder |
278
|
|
|
->andWhere('R.rate_date = :on_date') |
279
|
|
|
->setParameter('on_date', $criteria['onDate']->format('Y-m-d')); |
280
|
|
|
} |
281
|
|
|
|
282
|
2 |
|
if (isset($criteria['limit'])) { |
283
|
1 |
|
$queryBuilder->setMaxResults($criteria['limit']); |
284
|
|
|
} |
285
|
|
|
|
286
|
2 |
|
if (isset($criteria['offset'])) { |
287
|
1 |
|
$queryBuilder->setFirstResult($criteria['offset']); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* @var Statement $statement |
292
|
|
|
*/ |
293
|
2 |
|
$statement = $queryBuilder->execute(); |
294
|
2 |
|
while (($row = $statement->fetch()) !== false) { |
295
|
2 |
|
$rate = $this->buildRateFromTableRowData($row); |
296
|
2 |
|
$key = $this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName()); |
|
|
|
|
297
|
2 |
|
$this->identityMap[$key] = $rate; |
298
|
|
|
} |
299
|
|
|
|
300
|
2 |
|
return array_values($this->identityMap); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* {@inheritdoc} |
305
|
|
|
*/ |
306
|
1 |
|
public function count() |
307
|
|
|
{ |
308
|
|
|
/** |
309
|
|
|
* @var Statement $statement |
310
|
|
|
*/ |
311
|
1 |
|
$statement = $this->connection->query(sprintf('SELECT count(*) as cnt FROM %s;', $this->tableName), \PDO::FETCH_ASSOC); |
312
|
1 |
|
return (int) $statement->fetchAll()['cnt']; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Builds rate key to speed up search. |
317
|
|
|
* |
318
|
|
|
* @param string $currencyCode |
319
|
|
|
* @param \DateTime $date |
320
|
|
|
* @param string $rateType |
321
|
|
|
* @param string $sourceName |
322
|
|
|
* @return string |
323
|
|
|
*/ |
324
|
4 |
View Code Duplication |
protected function getRateKey($currencyCode, $date, $rateType, $sourceName) |
|
|
|
|
325
|
|
|
{ |
326
|
4 |
|
return str_replace( |
327
|
4 |
|
array('%currency_code%', '%date%', '%rate_type%', '%source_name%'), |
328
|
4 |
|
array($currencyCode, $date->format('Y-m-d'), $rateType, $sourceName), |
329
|
4 |
|
'%currency_code%_%date%_%rate_type%_%source_name%' |
330
|
|
|
); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Initialize table schema where rates would be stored. |
335
|
|
|
*/ |
336
|
5 |
|
protected function initialize() |
337
|
|
|
{ |
338
|
5 |
|
$schema = new Schema(); |
339
|
|
|
|
340
|
5 |
|
$table = $schema->createTable($this->tableName); |
341
|
5 |
|
$table->addColumn('source_name', 'string', ['length' => 255]); |
342
|
5 |
|
$table->addColumn('rate_value', 'float', ['precision' => 10, 'scale' => 4]); |
343
|
5 |
|
$table->addColumn('currency_code', 'string', ['length' => 3]); |
344
|
5 |
|
$table->addColumn('rate_type', 'string', ['length' => 255]); |
345
|
5 |
|
$table->addColumn('rate_date', 'date', []); |
346
|
5 |
|
$table->addColumn('base_currency_code', 'string', ['length' => 3]); |
347
|
5 |
|
$table->addColumn('created_at', 'datetime', []); |
348
|
5 |
|
$table->addColumn('modified_at', 'datetime', []); |
349
|
|
|
|
350
|
5 |
|
$table->setPrimaryKey(['currency_code', 'rate_date', 'rate_type', 'source_name']); |
351
|
|
|
|
352
|
5 |
|
$this->connection->exec($schema->toSql($this->connection->getDatabasePlatform())[0]); |
353
|
5 |
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Build rate from table row data. |
357
|
|
|
* |
358
|
|
|
* @param array $row Row data. |
359
|
|
|
* @return Rate |
360
|
|
|
*/ |
361
|
4 |
|
private function buildRateFromTableRowData(array $row) |
362
|
|
|
{ |
363
|
4 |
|
return new Rate( |
364
|
4 |
|
$row['source_name'], |
365
|
4 |
|
(float) $row['rate_value'], |
366
|
4 |
|
$row['currency_code'], |
367
|
4 |
|
$row['rate_type'], |
368
|
4 |
|
\DateTime::createFromFormat('Y-m-d', $row['rate_date']), |
|
|
|
|
369
|
4 |
|
$row['base_currency_code'], |
370
|
4 |
|
\DateTime::createFromFormat('Y-m-d H:i:s', $row['created_at']), |
|
|
|
|
371
|
4 |
|
\DateTime::createFromFormat('Y-m-d H:i:s', $row['modified_at']) |
|
|
|
|
372
|
|
|
); |
373
|
|
|
} |
374
|
|
|
} |
375
|
|
|
|
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.