Completed
Push — master ( fd6b18...202a78 )
by Nikola
03:09
created

FileRepository   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 309
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 93.13%

Importance

Changes 13
Bugs 3 Features 0
Metric Value
wmc 45
c 13
b 3
f 0
lcom 1
cbo 4
dl 0
loc 309
ccs 122
cts 131
cp 0.9313
rs 8.3673

14 Methods

Rating   Name   Duplication   Size   Complexity  
A all() 0 20 4
A count() 0 4 1
A __construct() 0 6 1
B save() 0 26 4
A delete() 0 11 2
A has() 0 8 2
A get() 0 12 3
B latest() 0 20 5
B load() 0 30 5
B initStorage() 0 19 8
A toJson() 0 13 1
A fromJson() 0 15 1
B paginate() 0 16 6
A getRateKey() 0 15 2

How to fix   Complexity   

Complex Class

Complex classes like FileRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FileRepository, and based on these observations, apply Extract Interface, too.

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
    /**
31
     * File where all rates are persisted.
32
     *
33
     * @var string
34
     */
35
    protected $pathToFile;
36
37
    /**
38
     * Collection of loaded rates.
39
     *
40
     * @var array
41
     */
42
    protected $rates;
43
44
    /**
45
     * Collection of latest rates (to speed up search process).
46
     *
47
     * @var array
48
     */
49
    protected $latest;
50
51 10
    public function __construct($pathToFile)
52
    {
53 10
        $this->pathToFile = $pathToFile;
54 10
        $this->initStorage();
55 10
        $this->load();
56 10
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61 10
    public function save(array $rates)
62
    {
63
        /**
64
         * @var RateInterface $rate
65
         */
66 10
        foreach ($rates as $rate) {
67 10
            $this->rates[$this->getRateKey($rate)] = $rate;
68 10
        }
69
70 10
        usort($this->rates, function(RateInterface $rate1, RateInterface $rate2) {
71 6
            return ($rate1->getDate() > $rate2->getDate()) ? -1 : 1;
72 10
        });
73
74 10
        $data = '';
75
76
        /**
77
         * @var RateInterface $rate
78
         */
79 10
        foreach ($this->rates as $rate) {
80 10
            $data .= $this->toJson($rate) . "\n";
81 10
        }
82
83 10
        file_put_contents($this->pathToFile, $data, LOCK_EX);
84
85 10
        $this->load();
86 10
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91 2
    public function delete(array $rates)
92
    {
93
        /**
94
         * @var RateInterface $rate
95
         */
96 2
        foreach ($rates as $rate) {
97 2
            unset($this->rates[$this->getRateKey($rate)]);
98 2
        }
99
100 2
        $this->save(array());
101 2
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106 4
    public function has($sourceName, $currencyCode, \DateTime $date = null, $rateType = 'default')
107
    {
108 4
        if ($date === null) {
109 4
            $date = new \DateTime('now');
110 4
        }
111
112 4
        return array_key_exists($this->getRateKey($currencyCode, $date, $rateType, $sourceName), $this->rates);
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118 2
    public function get($sourceName, $currencyCode, \DateTime $date = null, $rateType = 'default')
119
    {
120 2
        if ($date === null) {
121 2
            $date = new \DateTime('now');
122 2
        }
123
124 2
        if ($this->has($sourceName, $currencyCode, $date, $rateType)) {
125 2
            return $this->rates[$this->getRateKey($currencyCode, $date, $rateType, $sourceName)];
126
        }
127
128
        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')));
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 2
    public function latest($sourceName, $currencyCode, $rateType = 'default')
135
    {
136
        /**
137
         * @var RateInterface $rate
138
         */
139 2
        foreach ($this->rates as $rate) {
140
141
            if (
142 2
                $rate->getSourceName() === $sourceName
143 2
                &&
144 2
                $rate->getCurrencyCode() === $currencyCode
145 2
                &&
146 2
                $rate->getRateType() === $rateType
147 2
            ) {
148 2
                return $rate;
149
            }
150
        }
151
152
        throw new ExchangeRateException(sprintf('Could not fetch latest rate for rate currency code "%s" and rate type "%s" from source "%s".', $currencyCode, $rateType, $sourceName));
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 4
    public function all(array $criteria = array())
159
    {
160 4
        if (count($criteria) == 0) {
161
            return $this->rates;
162
        } else {
163 4
            $result = array();
164
165
            /**
166
             * @var RateInterface $rate
167
             */
168 4
            foreach ($this->rates as $rate) {
169
170 4
                if (RateFilterUtil::matches($rate, $criteria)) {
171 4
                    $result[] = $rate;
172 4
                }
173 4
            }
174
175 4
            return $this->paginate($result, $criteria);
176
        }
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182 2
    public function count()
183
    {
184 2
        return count($this->rates);
185
    }
186
187
    /**
188
     * Load all rates from file.
189
     *
190
     * @return RateInterface[]
191
     */
192 10
    protected function load()
193
    {
194 10
        $this->rates = array();
195 10
        $this->latest = array();
196
197 10
        $handle = fopen($this->pathToFile, 'r');
198
199 10
        if ($handle) {
200
201 10
            while (($line = fgets($handle)) !== false) {
202
203 10
                $rate = $this->fromJson($line);
204
205 10
                $this->rates[$this->getRateKey($rate)] = $rate;
206
207 10
                $latestKey = sprintf('%s_%s_%s', $rate->getCurrencyCode(), $rate->getRateType(), $rate->getSourceName());
208
209 10
                if (!isset($this->latest[$latestKey]) || ($this->latest[$latestKey]->getDate() < $rate->getDate())) {
210 10
                    $this->latest[$latestKey] = $rate;
211 10
                }
212 10
            }
213
214 10
            fclose($handle);
215
216 10
        } else {
217
            throw new \RuntimeException(sprintf('Error opening file on path "%s".', $this->pathToFile));
218
        }
219
220 10
        return $this->rates;
221
    }
222
223
    /**
224
     * Builds rate key to speed up search.
225
     *
226
     * @param null $currencyCode
227
     * @param null $date
228
     * @param null $rateType
229
     * @param null $sourceName
230
     * @return string
231
     */
232 10
    protected function getRateKey($currencyCode = null, $date = null, $rateType = null, $sourceName = null)
233
    {
234 10
        if ($currencyCode instanceof RateInterface) {
235 10
            $date = $currencyCode->getDate();
236 10
            $rateType = $currencyCode->getRateType();
237 10
            $sourceName = $currencyCode->getSourceName();
238 10
            $currencyCode = $currencyCode->getCurrencyCode();
239 10
        }
240
241 10
        return str_replace(
242 10
            array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
243 10
            array($currencyCode, $date->format('Y-m-d'), $rateType, $sourceName),
244
            '%currency_code%_%date%_%rate_type%_%source_name%'
245 10
        );
246
    }
247
248
    /**
249
     * Initializes file storage.
250
     */
251 10
    protected function initStorage()
252
    {
253
        /** @noinspection MkdirRaceConditionInspection */
254 10
        if (!file_exists(dirname($this->pathToFile)) && !mkdir(dirname($this->pathToFile), 0777, true)) {
255
            throw new \RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
256
        }
257
258 10
        if (!file_exists($this->pathToFile) && !(touch($this->pathToFile) && chmod($this->pathToFile, 0777))) {
259
            throw new \RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
260
        }
261
262 10
        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 10
        if (!is_writable($this->pathToFile)) {
267
            throw new \RuntimeException(sprintf('File on path "%s" for storing rates must be writeable.', $this->pathToFile));
268
        }
269 10
    }
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 10
    protected function toJson(RateInterface $rate)
278
    {
279 10
        return json_encode(array(
280 10
            'sourceName' => $rate->getSourceName(),
281 10
            'value' => $rate->getValue(),
282 10
            'currencyCode' => $rate->getCurrencyCode(),
283 10
            'rateType' => $rate->getRateType(),
284 10
            'date' => $rate->getDate()->format('Y-m-d H:i:s'),
285 10
            'baseCurrencyCode' => $rate->getBaseCurrencyCode(),
286 10
            'createdAt' => $rate->getCreatedAt()->format('Y-m-d H:i:s'),
287 10
            'modifiedAt' => $rate->getModifiedAt()->format('Y-m-d H:i:s')
288 10
        ));
289
    }
290
291
    /**
292
     * Deserialize JSON string to Rate
293
     *
294
     * @param string $json Serialized rate.
295
     * @return Rate Deserialized rate.
296
     */
297 10
    protected function fromJson($json)
298
    {
299 10
        $data = json_decode($json, true);
300
301 10
        return new Rate(
302 10
            $data['sourceName'],
303 10
            $data['value'],
304 10
            $data['currencyCode'],
305 10
            $data['rateType'],
306 10
            \DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
307 10
            $data['baseCurrencyCode'],
308 10
            \DateTime::createFromFormat('Y-m-d H:i:s', $data['createdAt']),
309 10
            \DateTime::createFromFormat('Y-m-d H:i:s', $data['modifiedAt'])
310 10
        );
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 4
    protected function paginate(array $rates, $criteria)
321
    {
322 4
        if (!array_key_exists('offset', $criteria) && !array_key_exists('limit', $criteria)) {
323 2
            return $rates;
324
        }
325
326 2
        $range = array();
327 2
        $offset = array_key_exists('offset', $criteria) ? $criteria['offset'] : 0;
328 2
        $limit = min((array_key_exists('limit', $criteria) ? $criteria['limit'] : count($rates)) + $offset, count($rates));
329
330 2
        for ($i = $offset; $i < $limit; $i++) {
331 2
            $range[] = $rates[$i];
332 2
        }
333
334 2
        return $range;
335
    }
336
}
337