Completed
Push — master ( 11cbba...433ba8 )
by Nikola
02:35
created

FileRepository::count()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
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
        foreach ($rates as $rate) {
69 10
            $this->rates[$this->getRateKey($rate)] = $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
        foreach ($rates as $rate) {
99 2
            unset($this->rates[$this->getRateKey($rate)]);
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)] = $rate;
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|null|RateInterface $currencyCode
229
     * @param \DateTime|null $date
230
     * @param string|null $rateType
231
     * @param string|null $sourceName
232
     * @return string
233
     */
234 10
    protected function getRateKey($currencyCode = null, $date = null, $rateType = null, $sourceName = null)
235
    {
236 10
        if ($currencyCode instanceof RateInterface) {
237 10
            $date = $currencyCode->getDate();
238 10
            $rateType = $currencyCode->getRateType();
239 10
            $sourceName = $currencyCode->getSourceName();
240 10
            $currencyCode = $currencyCode->getCurrencyCode();
241
        }
242
243 10
        return str_replace(
244 10
            array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
245 10
            array($currencyCode, $date->format('Y-m-d'), $rateType, $sourceName),
246 10
            '%currency_code%_%date%_%rate_type%_%source_name%'
247
        );
248
    }
249
250
    /**
251
     * Initializes file storage.
252
     */
253 10
    protected function initStorage()
254
    {
255
        /** @noinspection MkdirRaceConditionInspection */
256 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...
257
            throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
258
        }
259
260 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...
261
            throw new RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
262
        }
263
264 10
        if (!is_readable($this->pathToFile)) {
265
            throw new RuntimeException(sprintf('File on path "%s" for storing rates must be readable.', $this->pathToFile));
266
        }
267
268 10
        if (!is_writable($this->pathToFile)) {
269
            throw new RuntimeException(sprintf('File on path "%s" for storing rates must be writeable.', $this->pathToFile));
270
        }
271 10
    }
272
273
    /**
274
     * Serialize rate to JSON string.
275
     *
276
     * @param RateInterface $rate Rate to serialize.
277
     * @return string JSON representation of rate.
278
     */
279 10
    protected function toJson(RateInterface $rate)
280
    {
281 10
        return json_encode(array(
282 10
            'sourceName' => $rate->getSourceName(),
283 10
            'value' => $rate->getValue(),
284 10
            'currencyCode' => $rate->getCurrencyCode(),
285 10
            'rateType' => $rate->getRateType(),
286 10
            'date' => $rate->getDate()->format(\DateTime::ATOM),
287 10
            'baseCurrencyCode' => $rate->getBaseCurrencyCode(),
288 10
            'createdAt' => $rate->getCreatedAt()->format(\DateTime::ATOM),
289 10
            'modifiedAt' => $rate->getModifiedAt()->format(\DateTime::ATOM)
290
        ));
291
    }
292
293
    /**
294
     * Deserialize JSON string to Rate
295
     *
296
     * @param string $json Serialized rate.
297
     * @return Rate Deserialized rate.
298
     */
299 10
    protected function fromJson($json)
300
    {
301 10
        $data = json_decode($json, true);
302
303 10
        return new Rate(
304 10
            $data['sourceName'],
305 10
            $data['value'],
306 10
            $data['currencyCode'],
307 10
            $data['rateType'],
308 10
            \DateTime::createFromFormat(\DateTime::ATOM, $data['date']),
309 10
            $data['baseCurrencyCode'],
310 10
            \DateTime::createFromFormat(\DateTime::ATOM, $data['createdAt']),
311 10
            \DateTime::createFromFormat(\DateTime::ATOM, $data['modifiedAt'])
312
        );
313
    }
314
315
    /**
316
     * Extract requested page from filter criteria.
317
     *
318
     * @param array $rates Rates to filter for pagination.
319
     * @param array $criteria Filter criteria.
320
     * @return RateInterface[] Paginated rates.
321
     */
322 4
    protected function paginate(array $rates, $criteria)
323
    {
324 4
        if (!array_key_exists('offset', $criteria) && !array_key_exists('limit', $criteria)) {
325 2
            return $rates;
326
        }
327
328 2
        $range = array();
329 2
        $offset = array_key_exists('offset', $criteria) ? $criteria['offset'] : 0;
330 2
        $limit = min((array_key_exists('limit', $criteria) ? $criteria['limit'] : count($rates)) + $offset, count($rates));
331
332 2
        for ($i = $offset; $i < $limit; $i++) {
333 2
            $range[] = $rates[$i];
334
        }
335
336 2
        return $range;
337
    }
338
}
339