Completed
Push — master ( 1f59c8...e3081b )
by Nikola
04:10 queued 14s
created

FileRepository   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 261
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 64.1%

Importance

Changes 11
Bugs 3 Features 0
Metric Value
wmc 35
c 11
b 3
f 0
lcom 1
cbo 4
dl 0
loc 261
ccs 75
cts 117
cp 0.641
rs 9

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B save() 0 35 4
A delete() 0 11 2
A has() 0 15 2
A get() 0 18 3
A latest() 0 14 4
A all() 0 20 4
A count() 0 4 1
B load() 0 40 5
A getRateKey() 0 8 1
B initStorage() 0 19 8
1
<?php
2
/*
3
 * This file is part of the Exchange Rate package, an RunOpenCode project.
4
 *
5
 * (c) 2016 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\Exception\ExchangeRateException;
15
use RunOpenCode\ExchangeRate\Utils\RateFilterUtil;
16
use RunOpenCode\ExchangeRate\Model\Rate;
17
18
/**
19
 * Class FileRepository
20
 *
21
 * File repository is simple file based repository for storing rates.
22
 * Rates are serialized into JSON and stored in plain text file, row by row.
23
 *
24
 * File repository can be used as repository for small number of rates.
25
 *
26
 * @package RunOpenCode\ExchangeRate\Repository
27
 */
28
class FileRepository implements RepositoryInterface
29
{
30
    const RATE_KEY_FORMAT = '%currency_code%_%date%_%rate_type%';
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 2
    public function __construct($pathToFile)
54
    {
55 2
        $this->pathToFile = $pathToFile;
56 2
        $this->initStorage();
57 2
        $this->load();
58 2
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 2
    public function save(array $rates)
64
    {
65
        /**
66
         * @var RateInterface $rate
67
         */
68 2
        foreach ($rates as $rate) {
69 2
            $this->rates[$this->getRateKey($rate)] = $rate;
70 2
        }
71
72 2
        usort($this->rates, function(RateInterface $rate1, RateInterface $rate2) {
73
            return ($rate1->getDate() > $rate2->getDate()) ? -1 : 1;
74 2
        });
75
76 2
        $data = '';
77
78
        /**
79
         * @var RateInterface $rate
80
         */
81 2
        foreach ($this->rates as $rate) {
82 2
            $data .= json_encode(array(
83 2
                    'sourceName' => $rate->getSourceName(),
84 2
                    'value' => $rate->getValue(),
85 2
                    'currencyCode' => $rate->getCurrencyCode(),
86 2
                    'rateType' => $rate->getRateType(),
87 2
                    'date' => $rate->getDate()->format('Y-m-d H:i:s'),
88 2
                    'baseCurrencyCode' => $rate->getBaseCurrencyCode(),
89 2
                    'createdAt' => $rate->getCreatedAt()->format('Y-m-d H:i:s'),
90 2
                    'modifiedAt' => $rate->getModifiedAt()->format('Y-m-d H:i:s')
91 2
                )) . "\n";
92 2
        }
93
94 2
        file_put_contents($this->pathToFile, $data, LOCK_EX);
95
96 2
        $this->load();
97 2
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function delete(array $rates)
103
    {
104
        /**
105
         * @var RateInterface $rate
106
         */
107
        foreach ($rates as $rate) {
108
            unset($this->rates[$this->getRateKey($rate)]);
109
        }
110
111
        $this->save(array());
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117 2
    public function has($currencyCode, \DateTime $date = null, $rateType = 'default')
118
    {
119 2
        if ($date === null) {
120 2
            $date = new \DateTime('now');
121 2
        }
122
123 2
        return array_key_exists(
124 2
            str_replace(
125 2
                array('%currency_code%', '%date%', '%rate_type%'),
126 2
                array($currencyCode, $date->format('Y-m-d'), $rateType),
127
                self::RATE_KEY_FORMAT
128 2
            ),
129 2
            $this->rates
130 2
        );
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function get($currencyCode, \DateTime $date = null, $rateType = 'default')
137
    {
138
        if ($date === null) {
139
            $date = new \DateTime('now');
140
        }
141
142
        if ($this->has($currencyCode, $date, $rateType)) {
143
            return $this->rates[
144
                str_replace(
145
                    array('%currency_code%', '%date%', '%rate_type%'),
146
                    array($currencyCode, $date->format('Y-m-d'), $rateType),
147
                    self::RATE_KEY_FORMAT
148
                )
149
            ];
150
        }
151
152
        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')));
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function latest($currencyCode, $rateType = 'default')
159
    {
160
        /**
161
         * @var RateInterface $rate
162
         */
163
        foreach ($this->rates as $rate) {
164
165
            if ($rate->getCurrencyCode() === $currencyCode && $rate->getRateType() === $rateType) {
166
                return $rate;
167
            }
168
        }
169
170
        throw new ExchangeRateException(sprintf('Could not fetch latest rate for rate currency code "%s" and rate type "%s".', $currencyCode, $rateType));
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function all(array $criteria = array())
177
    {
178
        if (count($criteria) == 0) {
179
            return $this->rates;
180
        } else {
181
            $result = array();
182
183
            /**
184
             * @var RateInterface $rate
185
             */
186
            foreach ($this->rates as $rate) {
187
188
                if (RateFilterUtil::matches($rate, $criteria)) {
189
                    $result[] = $rate;
190
                }
191
            }
192
193
            return $result;
194
        }
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200
    public function count()
201
    {
202
        return count($this->rates);
203
    }
204
205
    /**
206
     * Load all rates from file.
207
     *
208
     * @return RateInterface[]
209
     */
210 2
    protected function load()
211
    {
212 2
        $this->rates = array();
213 2
        $this->latest = array();
214
215 2
        $handle = fopen($this->pathToFile, 'r');
216
217 2
        if ($handle) {
218
219 2
            while (($line = fgets($handle)) !== false) {
220 2
                $data = json_decode($line, true);
221
222 2
                $rate = new Rate(
223 2
                    $data['sourceName'],
224 2
                    $data['value'],
225 2
                    $data['currencyCode'],
226 2
                    $data['rateType'],
227 2
                    \DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
228 2
                    $data['baseCurrencyCode'],
229 2
                    \DateTime::createFromFormat('Y-m-d H:i:s', $data['createdAt']),
230 2
                    \DateTime::createFromFormat('Y-m-d H:i:s', $data['modifiedAt'])
231 2
                );
232
233 2
                $this->rates[$this->getRateKey($rate)] = $rate;
234
235 2
                $latestKey = sprintf('%s_%s', $rate->getCurrencyCode(), $rate->getRateType());
236
237 2
                if (!isset($this->latest[$latestKey]) || ($this->latest[$latestKey]->getDate() < $rate->getDate())) {
238 2
                    $this->latest[$latestKey] = $rate;
239 2
                }
240 2
            }
241
242 2
            fclose($handle);
243
244 2
        } else {
245
            throw new \RuntimeException(sprintf('Error opening file on path "%s".', $this->pathToFile));
246
        }
247
248 2
        return $this->rates;
249
    }
250
251
    /**
252
     * Builds rate key to speed up search.
253
     *
254
     * @param RateInterface $rate
255
     * @return string
256
     */
257 2
    protected function getRateKey(RateInterface $rate)
258
    {
259 2
        return str_replace(
260 2
            array('%currency_code%', '%date%', '%rate_type%'),
261 2
            array($rate->getCurrencyCode(), $rate->getDate()->format('Y-m-d'), $rate->getRateType()),
262
            self::RATE_KEY_FORMAT
263 2
        );
264
    }
265
266
    /**
267
     * Initializes file storage.
268
     */
269 2
    protected function initStorage()
270
    {
271
        /** @noinspection MkdirRaceConditionInspection */
272 2
        if (!file_exists(dirname($this->pathToFile)) && !mkdir(dirname($this->pathToFile), 0777, true)) {
273
            throw new \RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
274
        }
275
276 2
        if (!file_exists($this->pathToFile) && !(touch($this->pathToFile) && chmod($this->pathToFile, 0777))) {
277
            throw new \RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
278
        }
279
280 2
        if (!is_readable($this->pathToFile)) {
281
            throw new \RuntimeException(sprintf('File on path "%s" for storing rates must be readable.', $this->pathToFile));
282
        }
283
284 2
        if (!is_writable($this->pathToFile)) {
285
            throw new \RuntimeException(sprintf('File on path "%s" for storing rates must be writeable.', $this->pathToFile));
286
        }
287 2
    }
288
}
289