Completed
Push — master ( e3081b...b3412c )
by Nikola
03:22
created

FileRepository::initStorage()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 12.096

Importance

Changes 2
Bugs 2 Features 0
Metric Value
c 2
b 2
f 0
dl 0
loc 19
ccs 6
cts 10
cp 0.6
rs 7.7778
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) 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\CurrencyCodeUtil;
16
use RunOpenCode\ExchangeRate\Utils\RateFilterUtil;
17
use RunOpenCode\ExchangeRate\Model\Rate;
18
19
/**
20
 * Class FileRepository
21
 *
22
 * File repository is simple file based repository for storing rates.
23
 * Rates are serialized into JSON and stored in plain text file, row by row.
24
 *
25
 * File repository can be used as repository for small number of rates.
26
 *
27
 * @package RunOpenCode\ExchangeRate\Repository
28
 */
29
class FileRepository implements RepositoryInterface
30
{
31
    const RATE_KEY_FORMAT = '%currency_code%_%date%_%rate_type%_%source_name%';
32
33
    /**
34
     * File where all rates are persisted.
35
     *
36
     * @var string
37
     */
38
    protected $pathToFile;
39
40
    /**
41
     * Collection of loaded rates.
42
     *
43
     * @var array
44
     */
45
    protected $rates;
46
47
    /**
48
     * Collection of latest rates (to speed up search process).
49
     *
50
     * @var array
51
     */
52
    protected $latest;
53
54 2
    public function __construct($pathToFile)
55
    {
56 2
        $this->pathToFile = $pathToFile;
57 2
        $this->initStorage();
58 2
        $this->load();
59 2
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64 2
    public function save(array $rates)
65
    {
66
        /**
67
         * @var RateInterface $rate
68
         */
69 2
        foreach ($rates as $rate) {
70 2
            $this->rates[$this->getRateKey($rate)] = $rate;
71 2
        }
72
73 2
        usort($this->rates, function(RateInterface $rate1, RateInterface $rate2) {
74
            return ($rate1->getDate() > $rate2->getDate()) ? -1 : 1;
75 2
        });
76
77 2
        $data = '';
78
79
        /**
80
         * @var RateInterface $rate
81
         */
82 2
        foreach ($this->rates as $rate) {
83 2
            $data .= json_encode(array(
84 2
                    'sourceName' => $rate->getSourceName(),
85 2
                    'value' => $rate->getValue(),
86 2
                    'currencyCode' => $rate->getCurrencyCode(),
87 2
                    'rateType' => $rate->getRateType(),
88 2
                    'date' => $rate->getDate()->format('Y-m-d H:i:s'),
89 2
                    'baseCurrencyCode' => $rate->getBaseCurrencyCode(),
90 2
                    'createdAt' => $rate->getCreatedAt()->format('Y-m-d H:i:s'),
91 2
                    'modifiedAt' => $rate->getModifiedAt()->format('Y-m-d H:i:s')
92 2
                )) . "\n";
93 2
        }
94
95 2
        file_put_contents($this->pathToFile, $data, LOCK_EX);
96
97 2
        $this->load();
98 2
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function delete(array $rates)
104
    {
105
        /**
106
         * @var RateInterface $rate
107
         */
108
        foreach ($rates as $rate) {
109
            unset($this->rates[$this->getRateKey($rate)]);
110
        }
111
112
        $this->save(array());
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118 2
    public function has($sourceName, $currencyCode, \DateTime $date = null, $rateType = 'default')
119
    {
120 2
        if ($date === null) {
121 2
            $date = new \DateTime('now');
122 2
        }
123
124 2
        return array_key_exists(
125 2
            str_replace(
126 2
                array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
127 2
                array(CurrencyCodeUtil::clean($currencyCode), $date->format('Y-m-d'), $rateType, $sourceName),
128
                self::RATE_KEY_FORMAT
129 2
            ),
130 2
            $this->rates
131 2
        );
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137
    public function get($sourceName, $currencyCode, \DateTime $date = null, $rateType = 'default')
138
    {
139
        if ($date === null) {
140
            $date = new \DateTime('now');
141
        }
142
143
        if ($this->has($currencyCode, $date, $rateType)) {
0 ignored issues
show
Documentation introduced by
$date is of type object<DateTime>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$rateType is of type string, but the function expects a null|object<DateTime>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
144
            return $this->rates[
145
                str_replace(
146
                    array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
147
                    array(CurrencyCodeUtil::clean($currencyCode), $date->format('Y-m-d'), $rateType, $sourceName),
148
                    self::RATE_KEY_FORMAT
149
                )
150
            ];
151
        }
152
153
        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')));
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159
    public function latest($sourceName, $currencyCode, $rateType = 'default')
160
    {
161
        $currencyCode = CurrencyCodeUtil::clean($currencyCode);
162
        /**
163
         * @var RateInterface $rate
164
         */
165
        foreach ($this->rates as $rate) {
166
167
            if (
168
                $rate->getSourceName() === $sourceName
169
                &&
170
                $rate->getCurrencyCode() === $currencyCode
171
                &&
172
                $rate->getRateType() === $rateType
173
            ) {
174
                return $rate;
175
            }
176
        }
177
178
        throw new ExchangeRateException(sprintf('Could not fetch latest rate for rate currency code "%s" and rate type "%s" from source "%s".', $currencyCode, $rateType, $sourceName));
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184
    public function all(array $criteria = array())
185
    {
186
        if (count($criteria) == 0) {
187
            return $this->rates;
188
        } else {
189
            $result = array();
190
191
            /**
192
             * @var RateInterface $rate
193
             */
194
            foreach ($this->rates as $rate) {
195
196
                if (RateFilterUtil::matches($rate, $criteria)) {
197
                    $result[] = $rate;
198
                }
199
            }
200
201
            return $result;
202
        }
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208
    public function count()
209
    {
210
        return count($this->rates);
211
    }
212
213
    /**
214
     * Load all rates from file.
215
     *
216
     * @return RateInterface[]
217
     */
218 2
    protected function load()
219
    {
220 2
        $this->rates = array();
221 2
        $this->latest = array();
222
223 2
        $handle = fopen($this->pathToFile, 'r');
224
225 2
        if ($handle) {
226
227 2
            while (($line = fgets($handle)) !== false) {
228 2
                $data = json_decode($line, true);
229
230 2
                $rate = new Rate(
231 2
                    $data['sourceName'],
232 2
                    $data['value'],
233 2
                    $data['currencyCode'],
234 2
                    $data['rateType'],
235 2
                    \DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
236 2
                    $data['baseCurrencyCode'],
237 2
                    \DateTime::createFromFormat('Y-m-d H:i:s', $data['createdAt']),
238 2
                    \DateTime::createFromFormat('Y-m-d H:i:s', $data['modifiedAt'])
239 2
                );
240
241 2
                $this->rates[$this->getRateKey($rate)] = $rate;
242
243 2
                $latestKey = sprintf('%s_%s_%s', $rate->getCurrencyCode(), $rate->getRateType(), $rate->getSourceName());
244
245 2
                if (!isset($this->latest[$latestKey]) || ($this->latest[$latestKey]->getDate() < $rate->getDate())) {
246 2
                    $this->latest[$latestKey] = $rate;
247 2
                }
248 2
            }
249
250 2
            fclose($handle);
251
252 2
        } else {
253
            throw new \RuntimeException(sprintf('Error opening file on path "%s".', $this->pathToFile));
254
        }
255
256 2
        return $this->rates;
257
    }
258
259
    /**
260
     * Builds rate key to speed up search.
261
     *
262
     * @param RateInterface $rate
263
     * @return string
264
     */
265 2
    protected function getRateKey(RateInterface $rate)
266
    {
267 2
        return str_replace(
268 2
            array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
269 2
            array($rate->getCurrencyCode(), $rate->getDate()->format('Y-m-d'), $rate->getRateType(), $rate->getSourceName()),
270
            self::RATE_KEY_FORMAT
271 2
        );
272
    }
273
274
    /**
275
     * Initializes file storage.
276
     */
277 2
    protected function initStorage()
278
    {
279
        /** @noinspection MkdirRaceConditionInspection */
280 2
        if (!file_exists(dirname($this->pathToFile)) && !mkdir(dirname($this->pathToFile), 0777, true)) {
281
            throw new \RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
282
        }
283
284 2
        if (!file_exists($this->pathToFile) && !(touch($this->pathToFile) && chmod($this->pathToFile, 0777))) {
285
            throw new \RuntimeException(sprintf('Could not create storage file on path "%s".', $this->pathToFile));
286
        }
287
288 2
        if (!is_readable($this->pathToFile)) {
289
            throw new \RuntimeException(sprintf('File on path "%s" for storing rates must be readable.', $this->pathToFile));
290
        }
291
292 2
        if (!is_writable($this->pathToFile)) {
293
            throw new \RuntimeException(sprintf('File on path "%s" for storing rates must be writeable.', $this->pathToFile));
294
        }
295 2
    }
296
}
297