Completed
Push — master ( 794160...5bca01 )
by Nikola
03:58
created

FileRepository::load()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 8.439
c 0
b 0
f 0
ccs 13
cts 13
cp 1
cc 5
eloc 14
nc 4
nop 0
crap 5
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 8
    public function __construct($pathToFile)
59
    {
60 8
        $this->pathToFile = $pathToFile;
61 8
        $this->initialize();
62 8
        $this->load();
63 8
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68 7
    public function save(array $rates)
69
    {
70
        /**
71
         * @var RateInterface $rate
72
         */
73 7
        foreach ($rates as $rate) {
74 7
            $this->rates[$this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName())] = $rate;
75
        }
76
77 7
        usort($this->rates, function (RateInterface $rate1, RateInterface $rate2) {
78 5
            return ($rate1->getDate() > $rate2->getDate()) ? -1 : 1;
79 7
        });
80
81 7
        $data = '';
82
83
        /**
84
         * @var RateInterface $rate
85
         */
86 7
        foreach ($this->rates as $rate) {
87 7
            $data .= $this->toJson($rate)."\n";
88
        }
89
90 7
        file_put_contents($this->pathToFile, $data, LOCK_EX);
91
92 7
        $this->load();
93 7
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 1 View Code Duplication
    public function delete(array $rates)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
99
    {
100
        /**
101
         * @var RateInterface $rate
102
         */
103 1
        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 3 View Code Duplication
    public function has($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
114
    {
115 3
        if ($date === null) {
116 2
            $date = new \DateTime('now');
117
        }
118
119 3
        return array_key_exists($this->getRateKey($currencyCode, $date, $rateType, $sourceName), $this->rates);
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125 3 View Code Duplication
    public function get($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
126
    {
127 3
        if ($date === null) {
128 3
            $date = new \DateTime('now');
129
        }
130
131 3
        if ($this->has($sourceName, $currencyCode, $date, $rateType)) {
132 2
            return $this->rates[$this->getRateKey($currencyCode, $date, $rateType, $sourceName)];
133
        }
134
135 1
        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 2 View Code Duplication
    public function latest($sourceName, $currencyCode, $rateType = RateType::MEDIAN)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
142
    {
143
        /**
144
         * @var RateInterface $rate
145
         */
146 2
        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 1
        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 3 View Code Duplication
    public function all(array $criteria = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
166
    {
167 3
        if (count($criteria) == 0) {
168 1
            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 8
    protected function load()
200
    {
201 8
        $this->rates = array();
202 8
        $this->latest = array();
203
204 8
        $handle = fopen($this->pathToFile, 'rb');
205
206 8
        if (!$handle) {
207
            throw new RuntimeException(sprintf('Error opening file on path "%s".', $this->pathToFile)); // @codeCoverageIgnore
208
        }
209
210 8
        while (($line = fgets($handle)) !== false) {
211
212 7
            $rate = $this->fromJson($line);
213
214 7
            $this->rates[$this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName())] = $rate;
215
216 7
            $latestKey = sprintf('%s_%s_%s', $rate->getCurrencyCode(), $rate->getRateType(), $rate->getSourceName());
217
218 7
            if (!isset($this->latest[$latestKey]) || ($this->latest[$latestKey]->getDate() < $rate->getDate())) {
219 7
                $this->latest[$latestKey] = $rate;
220
            }
221
        }
222
223 8
        fclose($handle);
224
225 8
        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 7 View Code Duplication
    protected function getRateKey($currencyCode, $date, $rateType, $sourceName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
238
    {
239 7
        return str_replace(
240 7
            array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
241 7
            array($currencyCode, $date->format('Y-m-d'), $rateType, $sourceName),
242 7
            '%currency_code%_%date%_%rate_type%_%source_name%'
243
        );
244
    }
245
246
    /**
247
     * Initializes file storage.
248
     *
249
     * @codeCoverageIgnore
250
     */
251
    protected function initialize()
252
    {
253
        /** @noinspection MkdirRaceConditionInspection */
254 View Code Duplication
        if (!file_exists(dirname($this->pathToFile)) && !mkdir(dirname($this->pathToFile), 0777, true)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
255
            throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
256
        }
257
258 View Code Duplication
        if (!file_exists($this->pathToFile) && !(touch($this->pathToFile) && chmod($this->pathToFile, 0777))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
259
            throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
260
        }
261
262
        if (!is_readable($this->pathToFile)) {
263
            throw new RuntimeException(sprintf('File on path "%s" for storing rates must be readable.', $this->pathToFile));
264
        }
265
266
        if (!is_writable($this->pathToFile)) {
267
            throw new RuntimeException(sprintf('File on path "%s" for storing rates must be writeable.', $this->pathToFile));
268
        }
269
    }
270
271
    /**
272
     * Serialize rate to JSON string.
273
     *
274
     * @param RateInterface $rate Rate to serialize.
275
     * @return string JSON representation of rate.
276
     */
277 7
    protected function toJson(RateInterface $rate)
278
    {
279 7
        return json_encode(array(
280 7
            'sourceName' => $rate->getSourceName(),
281 7
            'value' => $rate->getValue(),
282 7
            'currencyCode' => $rate->getCurrencyCode(),
283 7
            'rateType' => $rate->getRateType(),
284 7
            'date' => $rate->getDate()->format(\DateTime::ATOM),
285 7
            'baseCurrencyCode' => $rate->getBaseCurrencyCode(),
286 7
            'createdAt' => $rate->getCreatedAt()->format(\DateTime::ATOM),
287 7
            'modifiedAt' => $rate->getModifiedAt()->format(\DateTime::ATOM),
288
        ));
289
    }
290
291
    /**
292
     * Deserialize JSON string to Rate
293
     *
294
     * @param string $json Serialized rate.
295
     * @return Rate Deserialized rate.
296
     */
297 7
    protected function fromJson($json)
298
    {
299 7
        $data = json_decode($json, true);
300
301 7
        return new Rate(
302 7
            $data['sourceName'],
303 7
            (float) $data['value'],
304 7
            $data['currencyCode'],
305 7
            $data['rateType'],
306 7
            \DateTime::createFromFormat(\DateTime::ATOM, $data['date']),
307 7
            $data['baseCurrencyCode'],
308 7
            \DateTime::createFromFormat(\DateTime::ATOM, $data['createdAt']),
309 7
            \DateTime::createFromFormat(\DateTime::ATOM, $data['modifiedAt'])
310
        );
311
    }
312
313
    /**
314
     * Extract requested page from filter criteria.
315
     *
316
     * @param array $rates Rates to filter for pagination.
317
     * @param array $criteria Filter criteria.
318
     * @return RateInterface[] Paginated rates.
319
     */
320 2 View Code Duplication
    protected function paginate(array $rates, $criteria)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
321
    {
322 2
        if (!array_key_exists('offset', $criteria) && !array_key_exists('limit', $criteria)) {
323 1
            return $rates;
324
        }
325
326 1
        $range = array();
327 1
        $offset = array_key_exists('offset', $criteria) ? $criteria['offset'] : 0;
328 1
        $limit = min((array_key_exists('limit', $criteria) ? $criteria['limit'] : count($rates)) + $offset, count($rates));
329
330 1
        for ($i = $offset; $i < $limit; $i++) {
331 1
            $range[] = $rates[$i];
332
        }
333
334 1
        return $range;
335
    }
336
}
337