Completed
Push — master ( 7a3ddd...0e6359 )
by Nikola
03:22
created

FileRepository::initialize()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 19
Code Lines 9

Duplication

Lines 6
Ratio 31.58 %

Code Coverage

Tests 6
CRAP Score 12.096

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 6
loc 19
ccs 6
cts 10
cp 0.6
rs 7.7777
cc 8
eloc 9
nc 5
nop 0
crap 12.096
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)
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 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)) {
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...
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))) {
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...
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