FileRepository::save()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 26
c 0
b 0
f 0
ccs 12
cts 12
cp 1
rs 9.504
cc 4
nc 4
nop 1
crap 4
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, \DateTimeInterface $date = null)
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 (null !== $date && $date < $rate->getDate()) {
149 1
                continue;
150
            }
151 1
152
            if (
153 1
                $rate->getSourceName() === $sourceName
154
                &&
155 1
                $rate->getCurrencyCode() === $currencyCode
156
                &&
157
                $rate->getRateType() === $rateType
158
            ) {
159 1
                return $rate;
160
            }
161
        }
162
163
        throw new ExchangeRateException(sprintf('Could not fetch latest rate for rate currency code "%s" and rate type "%s" from source "%s".', $currencyCode, $rateType, $sourceName));
164
    }
165 3
166
    /**
167 3
     * {@inheritdoc}
168 1
     */
169 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...
170
    {
171 2
        if (count($criteria) == 0) {
172
            return $this->rates;
173
        }
174
175
        $result = array();
176 2
177
        /**
178 2
         * @var RateInterface $rate
179 2
         */
180
        foreach ($this->rates as $rate) {
181
182
            if (RateFilterUtil::matches($rate, $criteria)) {
183 2
                $result[] = $rate;
184
            }
185
        }
186
187
        return $this->paginate($result, $criteria);
188
    }
189 1
190
    /**
191 1
     * {@inheritdoc}
192
     */
193
    public function count()
194
    {
195
        return count($this->rates);
196
    }
197
198
    /**
199 8
     * Load all rates from file.
200
     *
201 8
     * @return RateInterface[]
202 8
     */
203
    protected function load()
204 8
    {
205
        $this->rates = array();
206 8
        $this->latest = array();
207
208
        $handle = fopen($this->pathToFile, 'rb');
209
210 8
        if (!$handle) {
211
            throw new RuntimeException(sprintf('Error opening file on path "%s".', $this->pathToFile)); // @codeCoverageIgnore
212 7
        }
213
214 7
        while (($line = fgets($handle)) !== false) {
215
216 7
            $rate = $this->fromJson($line);
217
218 7
            $this->rates[$this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName())] = $rate;
219 7
220
            $latestKey = sprintf('%s_%s_%s', $rate->getCurrencyCode(), $rate->getRateType(), $rate->getSourceName());
221
222
            if (!isset($this->latest[$latestKey]) || ($this->latest[$latestKey]->getDate() < $rate->getDate())) {
223 8
                $this->latest[$latestKey] = $rate;
224
            }
225 8
        }
226
227
        fclose($handle);
228
229
        return $this->rates;
230
    }
231
232
    /**
233
     * Builds rate key to speed up search.
234
     *
235
     * @param string $currencyCode
236
     * @param \DateTime $date
237 7
     * @param string $rateType
238
     * @param string $sourceName
239 7
     * @return string
240 7
     */
241 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...
242 7
    {
243
        return str_replace(
244
            array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
245
            array($currencyCode, $date->format('Y-m-d'), $rateType, $sourceName),
246
            '%currency_code%_%date%_%rate_type%_%source_name%'
247
        );
248
    }
249
250
    /**
251
     * Initializes file storage.
252
     *
253
     * @codeCoverageIgnore
254
     */
255
    protected function initialize()
256
    {
257
        /** @noinspection MkdirRaceConditionInspection */
258 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...
259
            throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
260
        }
261
262 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...
263
            throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
264
        }
265
266
        if (!is_readable($this->pathToFile)) {
267
            throw new RuntimeException(sprintf('File on path "%s" for storing rates must be readable.', $this->pathToFile));
268
        }
269
270
        if (!is_writable($this->pathToFile)) {
271
            throw new RuntimeException(sprintf('File on path "%s" for storing rates must be writeable.', $this->pathToFile));
272
        }
273
    }
274
275
    /**
276
     * Serialize rate to JSON string.
277 7
     *
278
     * @param RateInterface $rate Rate to serialize.
279 7
     * @return string JSON representation of rate.
280 7
     */
281 7
    protected function toJson(RateInterface $rate)
282 7
    {
283 7
        return json_encode(array(
284 7
            'sourceName' => $rate->getSourceName(),
285 7
            'value' => $rate->getValue(),
286 7
            'currencyCode' => $rate->getCurrencyCode(),
287 7
            'rateType' => $rate->getRateType(),
288
            'date' => $rate->getDate()->format(\DateTime::ATOM),
289
            'baseCurrencyCode' => $rate->getBaseCurrencyCode(),
290
            'createdAt' => $rate->getCreatedAt()->format(\DateTime::ATOM),
291
            'modifiedAt' => $rate->getModifiedAt()->format(\DateTime::ATOM),
292
        ));
293
    }
294
295
    /**
296
     * Deserialize JSON string to Rate
297 7
     *
298
     * @param string $json Serialized rate.
299 7
     * @return Rate Deserialized rate.
300
     */
301 7
    protected function fromJson($json)
302 7
    {
303 7
        $data = json_decode($json, true);
304 7
305 7
        return new Rate(
306 7
            $data['sourceName'],
307 7
            (float) $data['value'],
308 7
            $data['currencyCode'],
309 7
            $data['rateType'],
310
            \DateTime::createFromFormat(\DateTime::ATOM, $data['date']),
311
            $data['baseCurrencyCode'],
312
            \DateTime::createFromFormat(\DateTime::ATOM, $data['createdAt']),
313
            \DateTime::createFromFormat(\DateTime::ATOM, $data['modifiedAt'])
314
        );
315
    }
316
317
    /**
318
     * Extract requested page from filter criteria.
319
     *
320 2
     * @param array $rates Rates to filter for pagination.
321
     * @param array $criteria Filter criteria.
322 2
     * @return RateInterface[] Paginated rates.
323 1
     */
324 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...
325
    {
326 1
        if (!array_key_exists('offset', $criteria) && !array_key_exists('limit', $criteria)) {
327 1
            return $rates;
328 1
        }
329
330 1
        $range = array();
331 1
        $offset = array_key_exists('offset', $criteria) ? $criteria['offset'] : 0;
332
        $limit = min((array_key_exists('limit', $criteria) ? $criteria['limit'] : count($rates)) + $offset, count($rates));
333
334 1
        for ($i = $offset; $i < $limit; $i++) {
335
            $range[] = $rates[$i];
336
        }
337
338
        return $range;
339
    }
340
}
341