Completed
Push — master ( 433ba8...064df3 )
by Nikola
02:41
created

FileRepository   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 302
Duplicated Lines 3.97 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 92.59%

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 5
dl 12
loc 302
ccs 100
cts 108
cp 0.9259
rs 8.3396
c 0
b 0
f 0

14 Methods

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

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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) 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 10
    public function __construct($pathToFile)
54
    {
55 10
        $this->pathToFile = $pathToFile;
56 10
        $this->initStorage();
57 10
        $this->load();
58 10
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 10
    public function save(array $rates)
64
    {
65
        /**
66
         * @var RateInterface $rate
67
         */
68 10 View Code Duplication
        foreach ($rates as $rate) {
1 ignored issue
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...
69 10
            $this->rates[$this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName())] = $rate;
70
        }
71
72 10
        usort($this->rates, function(RateInterface $rate1, RateInterface $rate2) {
73 6
            return ($rate1->getDate() > $rate2->getDate()) ? -1 : 1;
74 10
        });
75
76 10
        $data = '';
77
78
        /**
79
         * @var RateInterface $rate
80
         */
81 10
        foreach ($this->rates as $rate) {
82 10
            $data .= $this->toJson($rate) . "\n";
83
        }
84
85 10
        file_put_contents($this->pathToFile, $data, LOCK_EX);
86
87 10
        $this->load();
88 10
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93 2
    public function delete(array $rates)
94
    {
95
        /**
96
         * @var RateInterface $rate
97
         */
98 2 View Code Duplication
        foreach ($rates as $rate) {
1 ignored issue
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...
99 2
            unset($this->rates[$this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName())]);
100
        }
101
102 2
        $this->save(array());
103 2
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108 4
    public function has($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::DEFAULT)
0 ignored issues
show
Coding Style introduced by
Possible parse error: non-abstract method defined as abstract
Loading history...
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
109
    {
110 4
        if ($date === null) {
111 4
            $date = new \DateTime('now');
112
        }
113
114 4
        return array_key_exists($this->getRateKey($currencyCode, $date, $rateType, $sourceName), $this->rates);
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $this.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 2
    public function get($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::DEFAULT)
0 ignored issues
show
Coding Style introduced by
Possible parse error: non-abstract method defined as abstract
Loading history...
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
121
    {
122 2
        if ($date === null) {
123 2
            $date = new \DateTime('now');
124
        }
125
126 2
        if ($this->has($sourceName, $currencyCode, $date, $rateType)) {
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $this.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
127 2
            return $this->rates[$this->getRateKey($currencyCode, $date, $rateType, $sourceName)];
128
        }
129
130
        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')));
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $currencyCode.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 2
    public function latest($sourceName, $currencyCode, $rateType = RateType::DEFAULT)
0 ignored issues
show
Coding Style introduced by
Possible parse error: non-abstract method defined as abstract
Loading history...
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
137
    {
138
        /**
139
         * @var RateInterface $rate
140
         */
141 2
        foreach ($this->rates as $rate) {
142
143
            if (
144 2
                $rate->getSourceName() === $sourceName
145
                &&
146 2
                $rate->getCurrencyCode() === $currencyCode
147
                &&
148 2
                $rate->getRateType() === $rateType
149
            ) {
150 2
                return $rate;
151
            }
152
        }
153
154
        throw new ExchangeRateException(sprintf('Could not fetch latest rate for rate currency code "%s" and rate type "%s" from source "%s".', $currencyCode, $rateType, $sourceName));
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
Coding Style introduced by
The visibility should be declared for property $currencyCode.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 4
    public function all(array $criteria = array())
161
    {
162 4
        if (count($criteria) == 0) {
163
            return $this->rates;
164
        } else {
165 4
            $result = array();
166
167
            /**
168
             * @var RateInterface $rate
169
             */
170 4
            foreach ($this->rates as $rate) {
171
172 4
                if (RateFilterUtil::matches($rate, $criteria)) {
173 4
                    $result[] = $rate;
174
                }
175
            }
176
177 4
            return $this->paginate($result, $criteria);
178
        }
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184 2
    public function count()
185
    {
186 2
        return count($this->rates);
187
    }
188
189
    /**
190
     * Load all rates from file.
191
     *
192
     * @return RateInterface[]
193
     */
194 10
    protected function load()
195
    {
196 10
        $this->rates = array();
197 10
        $this->latest = array();
198
199 10
        $handle = fopen($this->pathToFile, 'r');
200
201 10
        if ($handle) {
202
203 10
            while (($line = fgets($handle)) !== false) {
204
205 10
                $rate = $this->fromJson($line);
206
207 10
                $this->rates[$this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName())] = $rate;
1 ignored issue
show
Bug introduced by
It seems like $rate->getDate() can be null; however, getRateKey() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
208
209 10
                $latestKey = sprintf('%s_%s_%s', $rate->getCurrencyCode(), $rate->getRateType(), $rate->getSourceName());
210
211 10
                if (!isset($this->latest[$latestKey]) || ($this->latest[$latestKey]->getDate() < $rate->getDate())) {
212 10
                    $this->latest[$latestKey] = $rate;
213
                }
214
            }
215
216 10
            fclose($handle);
217
218
        } else {
219
            throw new RuntimeException(sprintf('Error opening file on path "%s".', $this->pathToFile));
220
        }
221
222 10
        return $this->rates;
223
    }
224
225
    /**
226
     * Builds rate key to speed up search.
227
     *
228
     * @param string $currencyCode
229
     * @param \DateTime $date
230
     * @param string $rateType
231
     * @param string $sourceName
232
     * @return string
233
     */
234 10
    protected function getRateKey($currencyCode, $date, $rateType, $sourceName)
235
    {
236 10
        return str_replace(
237 10
            array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
238 10
            array($currencyCode, $date->format('Y-m-d'), $rateType, $sourceName),
239 10
            '%currency_code%_%date%_%rate_type%_%source_name%'
240
        );
241
    }
242
243
    /**
244
     * Initializes file storage.
245
     */
246 10
    protected function initStorage()
247
    {
248
        /** @noinspection MkdirRaceConditionInspection */
249 10 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...
250
            throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
251
        }
252
253 10 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...
254
            throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
255
        }
256
257 10
        if (!is_readable($this->pathToFile)) {
258
            throw new RuntimeException(sprintf('File on path "%s" for storing rates must be readable.', $this->pathToFile));
259
        }
260
261 10
        if (!is_writable($this->pathToFile)) {
262
            throw new RuntimeException(sprintf('File on path "%s" for storing rates must be writeable.', $this->pathToFile));
263
        }
264 10
    }
265
266
    /**
267
     * Serialize rate to JSON string.
268
     *
269
     * @param RateInterface $rate Rate to serialize.
270
     * @return string JSON representation of rate.
271
     */
272 10
    protected function toJson(RateInterface $rate)
273
    {
274 10
        return json_encode(array(
275 10
            'sourceName' => $rate->getSourceName(),
276 10
            'value' => $rate->getValue(),
277 10
            'currencyCode' => $rate->getCurrencyCode(),
278 10
            'rateType' => $rate->getRateType(),
279 10
            'date' => $rate->getDate()->format(\DateTime::ATOM),
280 10
            'baseCurrencyCode' => $rate->getBaseCurrencyCode(),
281 10
            'createdAt' => $rate->getCreatedAt()->format(\DateTime::ATOM),
282 10
            'modifiedAt' => $rate->getModifiedAt()->format(\DateTime::ATOM)
283
        ));
284
    }
285
286
    /**
287
     * Deserialize JSON string to Rate
288
     *
289
     * @param string $json Serialized rate.
290
     * @return Rate Deserialized rate.
291
     */
292 10
    protected function fromJson($json)
293
    {
294 10
        $data = json_decode($json, true);
295
296 10
        return new Rate(
297 10
            $data['sourceName'],
298 10
            $data['value'],
299 10
            $data['currencyCode'],
300 10
            $data['rateType'],
301 10
            \DateTime::createFromFormat(\DateTime::ATOM, $data['date']),
302 10
            $data['baseCurrencyCode'],
303 10
            \DateTime::createFromFormat(\DateTime::ATOM, $data['createdAt']),
304 10
            \DateTime::createFromFormat(\DateTime::ATOM, $data['modifiedAt'])
305
        );
306
    }
307
308
    /**
309
     * Extract requested page from filter criteria.
310
     *
311
     * @param array $rates Rates to filter for pagination.
312
     * @param array $criteria Filter criteria.
313
     * @return RateInterface[] Paginated rates.
314
     */
315 4
    protected function paginate(array $rates, $criteria)
316
    {
317 4
        if (!array_key_exists('offset', $criteria) && !array_key_exists('limit', $criteria)) {
318 2
            return $rates;
319
        }
320
321 2
        $range = array();
322 2
        $offset = array_key_exists('offset', $criteria) ? $criteria['offset'] : 0;
323 2
        $limit = min((array_key_exists('limit', $criteria) ? $criteria['limit'] : count($rates)) + $offset, count($rates));
324
325 2
        for ($i = $offset; $i < $limit; $i++) {
326 2
            $range[] = $rates[$i];
327
        }
328
329 2
        return $range;
330
    }
331
}
332