Completed
Push — master ( 5bca01...565929 )
by Nikola
03:22
created

DoctrineDbalRepository::all()   F

Complexity

Conditions 10
Paths 512

Size

Total Lines 76
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 10

Importance

Changes 0
Metric Value
dl 0
loc 76
c 0
b 0
f 0
ccs 38
cts 38
cp 1
rs 3.9615
cc 10
eloc 45
nc 512
nop 1
crap 10

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Doctrine\DBAL\Connection;
13
use Doctrine\DBAL\Driver\Statement;
14
use Doctrine\DBAL\Query\QueryBuilder;
15
use Doctrine\DBAL\Schema\Schema;
16
use RunOpenCode\ExchangeRate\Contract\RateInterface;
17
use RunOpenCode\ExchangeRate\Contract\RepositoryInterface;
18
use RunOpenCode\ExchangeRate\Enum\RateType;
19
use RunOpenCode\ExchangeRate\Exception\ExchangeRateException;
20
use RunOpenCode\ExchangeRate\Model\Rate;
21
use RunOpenCode\ExchangeRate\Utils\FilterUtilHelper;
22
23
/**
24
 * Class DoctrineDbalRepository
25
 *
26
 * Dbal repository uses http://www.doctrine-project.org/projects/dbal.html for rates persistance.
27
 *
28
 * @package RunOpenCode\ExchangeRate\Repository
29
 */
30
class DoctrineDbalRepository implements RepositoryInterface
31
{
32
    use FilterUtilHelper;
33
34
    /**
35
     * @var Connection
36
     */
37
    private $connection;
38
39
    /**
40
     * @var string
41
     */
42
    private $tableName;
43
44
    /**
45
     * @var RateInterface[]
46
     */
47
    private $identityMap = [];
48
49
    /**
50
     * DoctrineDbalRepository constructor.
51
     *
52
     * @param Connection $connection Dbal connection.
53
     * @param string|null $tableName Table name in which rates will be stored, or NULL for 'runopencode_exchange_rate'.
54
     */
55 10
    public function __construct(Connection $connection, $tableName = null)
56
    {
57 10
        $this->connection = $connection;
58 10
        $this->tableName = ($tableName = trim($tableName)) ? $tableName : 'runopencode_exchange_rate';
59
60 10
        $this->initialize();
61 10
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66 8
    public function save(array $rates)
67
    {
68 8
        $this->connection->beginTransaction();
69
70
        try {
71
72
            /**
73
             * @var RateInterface $rate
74
             */
75 8
            foreach ($rates as $rate) {
76
77 8
                if ($this->has($rate->getSourceName(), $rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType())) {
78
79 1
                    $this->connection->executeQuery(sprintf('UPDATE %s SET rate_value = :rate_value, modified_at = :modified_at WHERE source_name = :source_name AND currency_code = :currency_code AND rate_date = :rate_date AND rate_type = :rate_type;', $this->tableName), [
80 1
                        'rate_value' => (float) $rate->getValue(),
81 1
                        'source_name' => $rate->getSourceName(),
82 1
                        'currency_code' => $rate->getCurrencyCode(),
83 1
                        'rate_date' => $rate->getDate()->format('Y-m-d'),
84 1
                        'rate_type' => $rate->getRateType(),
85 1
                        'modified_at' => date('Y-m-d H:i:s'),
86
                    ]);
87
88 1
                    continue;
89
                }
90
91 8
                $this->connection->executeQuery(sprintf('INSERT INTO %s (source_name, rate_value, currency_code, rate_type, rate_date, base_currency_code, created_at, modified_at) VALUES (:source_name, :rate_value, :currency_code, :rate_type, :rate_date, :base_currency_code, :created_at, :modified_at);', $this->tableName), [
92 8
                    'source_name' => $rate->getSourceName(),
93 8
                    'rate_value' => (float) $rate->getValue(),
94 8
                    'currency_code' => $rate->getCurrencyCode(),
95 8
                    'rate_type' => $rate->getRateType(),
96 8
                    'rate_date' => $rate->getDate()->format('Y-m-d'),
97 8
                    'base_currency_code' => $rate->getBaseCurrencyCode(),
98 8
                    'created_at' => date('Y-m-d H:i:s'),
99 8
                    'modified_at' => date('Y-m-d H:i:s'),
100
                ]);
101
            }
102
103 7
            $this->connection->commit();
104 7
            $this->identityMap = [];
105 1
        } catch (\Exception $e) {
106 1
            $this->connection->rollBack();
107 1
            throw new ExchangeRateException('Unable to save rates.', 0, $e);
108
        }
109 7
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114 2
    public function delete(array $rates)
115
    {
116 2
        $this->connection->beginTransaction();
117
118
        try {
119
120
            /**
121
             * @var RateInterface $rate
122
             */
123 2
            foreach ($rates as $rate) {
124 2
                $key = $this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName());
125
126 2
                if (isset($this->identityMap[$key])) {
127 1
                    unset($this->identityMap[$key]);
128
                }
129
130 2
                $this->connection->executeQuery(sprintf('DELETE FROM %s WHERE source_name = :source_name AND currency_code = :currency_code AND rate_date = :rate_date AND rate_type = :rate_type;', $this->tableName), [
131 2
                    'source_name' => $rate->getSourceName(),
132 2
                    'currency_code' => $rate->getCurrencyCode(),
133 2
                    'rate_date' => $rate->getDate()->format('Y-m-d'),
134 2
                    'rate_type' => $rate->getRateType(),
135
                ]);
136
            }
137
138 1
            $this->connection->commit();
139 1
        } catch (\Exception $e) {
140 1
            $this->connection->rollBack();
141 1
            throw new ExchangeRateException('Unable to delete rates.', 0, $e);
142
        }
143 1
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148 8
    public function has($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN)
149
    {
150 8
        if ($date === null) {
151 2
            $date = new \DateTime('now');
152
        }
153
154
        try {
155 8
            return (bool) $this->get($sourceName, $currencyCode, $date, $rateType);
156 8
        } catch (ExchangeRateException $e) {
157 8
            return false;
158
        }
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 8
    public function get($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN)
165
    {
166 8
        if ($date === null) {
167 3
            $date = new \DateTime('now');
168
        }
169
170 8
        $key = $this->getRateKey($currencyCode, $date, $rateType, $sourceName);
171
172 8
        if (!isset($this->identityMap[$key])) {
173
174
            /**
175
             * @var array $result
176
             */
177 8
            $result = $this->connection->fetchAll(
178 8
                sprintf('SELECT R.* FROM %s R WHERE R.source_name = :source_name AND R.currency_code = :currency_code AND R.rate_date = :rate_date AND R.rate_type = :rate_type;', $this->tableName),
179
                [
180 8
                    'source_name' => $sourceName,
181 8
                    'currency_code' => $currencyCode,
182 8
                    'rate_date' => $date->format('Y-m-d'),
183 8
                    'rate_type' => $rateType,
184
                ]
185
            );
186
187 8
            if (0 === count($result)) {
188 8
                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')));
189
            }
190
191 2
            $this->identityMap[$key] = $this->buildRateFromTableRowData($result[0]);
192
        }
193
194 2
        return $this->identityMap[$key];
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 2
    public function latest($sourceName, $currencyCode, $rateType = RateType::MEDIAN)
201
    {
202
        /**
203
         * @var array $result
204
         */
205 2
        $result = $this->connection->fetchAll(
206 2
            sprintf('SELECT R.* FROM %s R WHERE R.source_name = :source_name AND R.currency_code = :currency_code AND R.rate_type = :rate_type ORDER BY R.rate_date DESC;', $this->tableName),
207
            [
208 2
                'source_name' => $sourceName,
209 2
                'currency_code' => $currencyCode,
210 2
                'rate_type' => $rateType,
211
            ]
212
        );
213
214 2
        if (0 === count($result)) {
215 1
            throw new ExchangeRateException(sprintf('Could not fetch latest rate for rate currency code "%s" and rate type "%s".', $currencyCode, $rateType));
216
        }
217
218 1
        $rate = $this->buildRateFromTableRowData($result[0]);
219 1
        $key = $this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName());
0 ignored issues
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...
220
221 1
        return ($this->identityMap[$key] = $rate);
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227 3
    public function all(array $criteria = array())
228
    {
229
        /**
230
         * @var QueryBuilder $queryBuilder
231
         */
232 3
        $queryBuilder = $this->connection->createQueryBuilder();
233
234
        $queryBuilder
235 3
            ->select('R.*')
236 3
            ->from($this->tableName, 'R')
237 3
            ->addOrderBy('R.rate_date', 'DESC')
238 3
            ->addOrderBy('R.source_name', 'ASC')
239 3
            ->addOrderBy('R.currency_code', 'ASC')
240 3
            ->addOrderBy('R.rate_type', 'ASC')
241
        ;
242
243 3
        if (0 !== count($currencyCodes = self::extractArrayCriteria('currencyCode', $criteria))) {
244
            $queryBuilder
245 1
                ->andWhere('R.currency_code IN (:currency_codes)')
246 1
                ->setParameter(':currency_codes', $currencyCodes, Connection::PARAM_STR_ARRAY);
247
        }
248
249 3
        if (0 !== count($rateTypes = self::extractArrayCriteria('rateType', $criteria))) {
250
            $queryBuilder
251 1
                ->andWhere('R.rate_type IN (:rate_types)')
252 1
                ->setParameter(':rate_types', $rateTypes, Connection::PARAM_STR_ARRAY);
253
        }
254
255 3
        if (0 !== count($sourceNames = self::extractArrayCriteria('sourceName', $criteria))) {
256
            $queryBuilder
257 1
                ->andWhere('R.source_name IN (:source_names)')
258 1
                ->setParameter(':source_names', $sourceNames, Connection::PARAM_STR_ARRAY);
259
        }
260
261 3
        if (isset($criteria['dateFrom'])) {
262
            $queryBuilder
263 1
                ->andWhere('R.rate_date >= :date_from')
264 1
                ->setParameter('date_from', self::extractDateCriteria('dateFrom', $criteria)->format('Y-m-d'));
265
        }
266
267 3
        if (isset($criteria['dateTo'])) {
268
            $queryBuilder
269 1
                ->andWhere('R.rate_date <= :date_to')
270 1
                ->setParameter('date_to', self::extractDateCriteria('dateTo', $criteria)->format('Y-m-d'));
271
        }
272
273 3
        if (isset($criteria['onDate'])) {
274
            $queryBuilder
275 1
                ->andWhere('R.rate_date = :on_date')
276 1
                ->setParameter('on_date', self::extractDateCriteria('onDate', $criteria)->format('Y-m-d'));
277
        }
278
279 3
        if (isset($criteria['limit'])) {
280 1
            $queryBuilder->setMaxResults($criteria['limit']);
281
        }
282
283 3
        if (isset($criteria['offset'])) {
284 1
            $queryBuilder->setFirstResult($criteria['offset']);
285
        }
286
287
        /**
288
         * @var Statement $statement
289
         */
290 3
        $statement = $queryBuilder->execute();
291
292 3
        $result = [];
293
294 3
        while (($row = $statement->fetch()) !== false) {
295 3
            $rate = $this->buildRateFromTableRowData($row);
296 3
            $key = $this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName());
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...
297 3
            $this->identityMap[$key] = $rate;
298 3
            $result[] = $rate;
299
        }
300
301 3
        return $result;
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307 1
    public function count()
308
    {
309
        /**
310
         * @var Statement $statement
311
         */
312 1
        $statement = $this->connection->query(sprintf('SELECT count(*) as cnt FROM %s;', $this->tableName), \PDO::FETCH_ASSOC);
313 1
        return (int) $statement->fetchAll()[0]['cnt'];
314
    }
315
316
    /**
317
     * Builds rate key to speed up search.
318
     *
319
     * @param string $currencyCode
320
     * @param \DateTime $date
321
     * @param string $rateType
322
     * @param string $sourceName
323
     * @return string
324
     */
325 9 View Code Duplication
    protected function getRateKey($currencyCode, $date, $rateType, $sourceName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
326
    {
327 9
        return str_replace(
328 9
            array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
329 9
            array($currencyCode, $date->format('Y-m-d'), $rateType, $sourceName),
330 9
            '%currency_code%_%date%_%rate_type%_%source_name%'
331
        );
332
    }
333
334
    /**
335
     * Initialize table schema where rates would be stored.
336
     */
337 10
    protected function initialize()
338
    {
339 10
        if ($this->connection->getSchemaManager()->tablesExist([$this->tableName])) {
340
            return; // @codeCoverageIgnore
341
        }
342
343 8
        $schema = new Schema();
344
345 8
        $table = $schema->createTable($this->tableName);
346 8
        $table->addColumn('source_name', 'string', ['length' => 255]);
347 8
        $table->addColumn('rate_value', 'float', ['precision' => 10, 'scale' => 4]);
348 8
        $table->addColumn('currency_code', 'string', ['length' => 3]);
349 8
        $table->addColumn('rate_type', 'string', ['length' => 255]);
350 8
        $table->addColumn('rate_date', 'date', []);
351 8
        $table->addColumn('base_currency_code', 'string', ['length' => 3]);
352 8
        $table->addColumn('created_at', 'datetime', []);
353 8
        $table->addColumn('modified_at', 'datetime', []);
354
355 8
        $table->setPrimaryKey(['currency_code', 'rate_date', 'rate_type', 'source_name']);
356
357 8
        $this->connection->exec($schema->toSql($this->connection->getDatabasePlatform())[0]);
358 8
    }
359
360
    /**
361
     * Build rate from table row data.
362
     *
363
     * @param array $row Row data.
364
     * @return Rate
365
     */
366 6
    private function buildRateFromTableRowData(array $row)
367
    {
368 6
        return new Rate(
369 6
            $row['source_name'],
370 6
            (float) $row['rate_value'],
371 6
            $row['currency_code'],
372 6
            $row['rate_type'],
373 6
            \DateTime::createFromFormat('Y-m-d', $row['rate_date']),
0 ignored issues
show
Security Bug introduced by
It seems like \DateTime::createFromFor...-d', $row['rate_date']) targeting DateTime::createFromFormat() can also be of type false; however, RunOpenCode\ExchangeRate\Model\Rate::__construct() does only seem to accept object<DateTime>|integer, did you maybe forget to handle an error condition?
Loading history...
374 6
            $row['base_currency_code'],
375 6
            \DateTime::createFromFormat('Y-m-d H:i:s', $row['created_at']),
0 ignored issues
show
Security Bug introduced by
It seems like \DateTime::createFromFor...s', $row['created_at']) targeting DateTime::createFromFormat() can also be of type false; however, RunOpenCode\ExchangeRate\Model\Rate::__construct() does only seem to accept null|object<DateTime>|integer, did you maybe forget to handle an error condition?
Loading history...
376 6
            \DateTime::createFromFormat('Y-m-d H:i:s', $row['modified_at'])
0 ignored issues
show
Security Bug introduced by
It seems like \DateTime::createFromFor...', $row['modified_at']) targeting DateTime::createFromFormat() can also be of type false; however, RunOpenCode\ExchangeRate\Model\Rate::__construct() does only seem to accept null|object<DateTime>|integer, did you maybe forget to handle an error condition?
Loading history...
377
        );
378
    }
379
}
380