Completed
Push — master ( 7a3ddd...0e6359 )
by Nikola
03:22
created

DoctrineDbalRepository::all()   D

Complexity

Conditions 10
Paths 512

Size

Total Lines 72
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 12.1438

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 72
ccs 26
cts 36
cp 0.7221
rs 4.12
cc 10
eloc 43
nc 512
nop 1
crap 12.1438

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 $tableName Table name in which rates will be stored.
54
     * @param bool $initialize Should repository initialize table.
55
     */
56 5
    public function __construct(Connection $connection, $tableName = 'runopencode_exchange_rate', $initialize = false)
57
    {
58 5
        $this->connection = $connection;
59 5
        $this->tableName = $tableName;
60
61 5
        if ($initialize) {
62 5
            $this->initialize();
63
        }
64 5
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69 4
    public function save(array $rates)
70
    {
71 4
        $this->connection->beginTransaction();
72
73
        try {
74
75
            /**
76
             * @var RateInterface $rate
77
             */
78 4
            foreach ($rates as $rate) {
79
80 4
                if ($this->has($rate->getSourceName(), $rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType())) {
81
82
                    $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), [
83
                        'rate_value' => (float) $rate->getValue(),
84
                        'source_name' => $rate->getSourceName(),
85
                        'currency_code' => $rate->getCurrencyCode(),
86
                        'rate_date' => $rate->getDate()->format('Y-m-d'),
87
                        'rate_type' => $rate->getRateType(),
88
                        'modified_at' => date('Y-m-d H:i:s'),
89
                    ]);
90
91
                } else {
92
93 4
                    $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), [
94 4
                        'source_name' => $rate->getSourceName(),
95 4
                        'rate_value' => (float) $rate->getValue(),
96 4
                        'currency_code' => $rate->getCurrencyCode(),
97 4
                        'rate_type' => $rate->getRateType(),
98 4
                        'rate_date' => $rate->getDate()->format('Y-m-d'),
99 4
                        'base_currency_code' => $rate->getBaseCurrencyCode(),
100 4
                        'created_at' => date('Y-m-d H:i:s'),
101 4
                        'modified_at' => date('Y-m-d H:i:s'),
102
                    ]);
103
                }
104
            }
105
106 4
            $this->connection->commit();
107
108
        } catch (\Exception $e) {
109
            $this->connection->rollBack();
110
            throw $e;
111
        }
112 4
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117 1
    public function delete(array $rates)
118
    {
119 1
        $this->connection->beginTransaction();
120
121
        try {
122
123
            /**
124
             * @var RateInterface $rate
125
             */
126 1
            foreach ($rates as $rate) {
127 1
                $key = $this->getRateKey($rate->getCurrencyCode(), $rate->getDate(), $rate->getRateType(), $rate->getSourceName());
128
129 1
                if (isset($this->identityMap[$key])) {
130 1
                    unset($this->identityMap[$key]);
131
                }
132
133 1
                $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), [
134 1
                    'source_name' => $rate->getSourceName(),
135 1
                    'currency_code' => $rate->getCurrencyCode(),
136 1
                    'rate_date' => $rate->getDate()->format('Y-m-d'),
137 1
                    'rate_type' => $rate->getRateType(),
138
                ]);
139
            }
140
141 1
            $this->connection->commit();
142
        } catch (\Exception $e) {
143
            $this->connection->rollBack();
144
            throw $e;
145
        }
146 1
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 4
    public function has($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN)
152
    {
153 4
        if ($date === null) {
154 1
            $date = new \DateTime('now');
155
        }
156
157
        try {
158 4
            return (bool) $this->get($sourceName, $currencyCode, $date, $rateType);
159 4
        } catch (ExchangeRateException $e) {
160 4
            return false;
161
        }
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 4
    public function get($sourceName, $currencyCode, \DateTime $date = null, $rateType = RateType::MEDIAN)
168
    {
169 4
        if ($date === null) {
170 1
            $date = new \DateTime('now');
171
        }
172
173 4
        $key = $this->getRateKey($currencyCode, $date, $rateType, $sourceName);
174
175 4
        if (!isset($this->identityMap[$key])) {
176
177
            /**
178
             * @var array $result
179
             */
180 4
            $result = $this->connection->fetchAll(
181 4
                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),
182
                [
183 4
                    'source_name' => $sourceName,
184 4
                    'currency_code' => $currencyCode,
185 4
                    'rate_date' => $date->format('Y-m-d'),
186 4
                    'rate_type' => $rateType,
187
                ]
188
            );
189
190 4 View Code Duplication
            if (0 === count($result)) {
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...
191 4
                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')));
192
            }
193
194 1
            $this->identityMap[$key] = $this->buildRateFromTableRowData($result[0]);
195
        }
196
197 1
        return $this->identityMap[$key];
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203 1
    public function latest($sourceName, $currencyCode, $rateType = RateType::MEDIAN)
204
    {
205
        /**
206
         * @var array $result
207
         */
208 1
        $result = $this->connection->fetchAll(
209 1
            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),
210
            [
211 1
                'source_name' => $sourceName,
212 1
                'currency_code' => $currencyCode,
213 1
                'rate_type' => $rateType,
214
            ]
215
        );
216
217 1 View Code Duplication
        if (0 === count($result)) {
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...
218
            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')));
219
        }
220
221 1
        $rate = $this->buildRateFromTableRowData($result[0]);
222 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...
223
224 1
        return ($this->identityMap[$key] = $rate);
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 2
    public function all(array $criteria = array())
231
    {
232
        /**
233
         * @var QueryBuilder $queryBuilder
234
         */
235 2
        $queryBuilder = $this->connection->createQueryBuilder();
236
237
        $queryBuilder
238 2
            ->select('R.*')
239 2
            ->from($this->tableName, 'R')
240 2
            ->addOrderBy('R.rate_date', 'DESC')
241 2
            ->addOrderBy('R.source_name', 'ASC')
242 2
            ->addOrderBy('R.currency_code', 'ASC')
243 2
            ->addOrderBy('R.rate_type', 'ASC')
244
        ;
245
246 2
        if (0 !== count($currencyCodes = self::extractArrayCriteria('currencyCode', $criteria))) {
247
            $queryBuilder
248
                ->andWhere('R.currency_code IN (:currency_codes)')
249
                ->setParameter(':currency_codes', $currencyCodes, Connection::PARAM_STR_ARRAY);
250
        }
251
252 2
        if (0 !== count($rateTypes = self::extractArrayCriteria('rateType', $criteria))) {
253
            $queryBuilder
254
                ->andWhere('R.rate_type IN (:rate_types)')
255
                ->setParameter(':rate_types', $rateTypes, Connection::PARAM_STR_ARRAY);
256
        }
257
258 2
        if (0 !== count($sourceNames = self::extractArrayCriteria('sourceName', $criteria))) {
259
            $queryBuilder
260 1
                ->andWhere('R.source_name IN (:source_names)')
261 1
                ->setParameter(':source_names', $sourceNames, Connection::PARAM_STR_ARRAY);
262
        }
263
264 2
        if (isset($criteria['dateFrom'])) {
265
            $queryBuilder
266
                ->andWhere('R.rate_date >= :date_from')
267
                ->setParameter('date_from', $criteria['dateFrom']->format('Y-m-d'));
268
        }
269
270 2
        if (isset($criteria['dateTo'])) {
271
            $queryBuilder
272
                ->andWhere('R.rate_date <= :date_to')
273
                ->setParameter('date_to', $criteria['dateTo']->format('Y-m-d'));
274
        }
275
276 2
        if (isset($criteria['onDate'])) {
277
            $queryBuilder
278
                ->andWhere('R.rate_date = :on_date')
279
                ->setParameter('on_date', $criteria['onDate']->format('Y-m-d'));
280
        }
281
282 2
        if (isset($criteria['limit'])) {
283 1
            $queryBuilder->setMaxResults($criteria['limit']);
284
        }
285
286 2
        if (isset($criteria['offset'])) {
287 1
            $queryBuilder->setFirstResult($criteria['offset']);
288
        }
289
290
        /**
291
         * @var Statement $statement
292
         */
293 2
        $statement = $queryBuilder->execute();
294 2
        while (($row = $statement->fetch()) !== false) {
295 2
            $rate = $this->buildRateFromTableRowData($row);
296 2
            $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 2
            $this->identityMap[$key] = $rate;
298
        }
299
300 2
        return array_values($this->identityMap);
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306 1
    public function count()
307
    {
308
        /**
309
         * @var Statement $statement
310
         */
311 1
        $statement = $this->connection->query(sprintf('SELECT count(*) as cnt FROM %s;', $this->tableName), \PDO::FETCH_ASSOC);
312 1
        return (int) $statement->fetchAll()['cnt'];
313
    }
314
315
    /**
316
     * Builds rate key to speed up search.
317
     *
318
     * @param string $currencyCode
319
     * @param \DateTime $date
320
     * @param string $rateType
321
     * @param string $sourceName
322
     * @return string
323
     */
324 4 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...
325
    {
326 4
        return str_replace(
327 4
            array('%currency_code%', '%date%', '%rate_type%', '%source_name%'),
328 4
            array($currencyCode, $date->format('Y-m-d'), $rateType, $sourceName),
329 4
            '%currency_code%_%date%_%rate_type%_%source_name%'
330
        );
331
    }
332
333
    /**
334
     * Initialize table schema where rates would be stored.
335
     */
336 5
    protected function initialize()
337
    {
338 5
        $schema = new Schema();
339
340 5
        $table = $schema->createTable($this->tableName);
341 5
        $table->addColumn('source_name', 'string', ['length' => 255]);
342 5
        $table->addColumn('rate_value', 'float', ['precision' => 10, 'scale' => 4]);
343 5
        $table->addColumn('currency_code', 'string', ['length' => 3]);
344 5
        $table->addColumn('rate_type', 'string', ['length' => 255]);
345 5
        $table->addColumn('rate_date', 'date', []);
346 5
        $table->addColumn('base_currency_code', 'string', ['length' => 3]);
347 5
        $table->addColumn('created_at', 'datetime', []);
348 5
        $table->addColumn('modified_at', 'datetime', []);
349
350 5
        $table->setPrimaryKey(['currency_code', 'rate_date', 'rate_type', 'source_name']);
351
352 5
        $this->connection->exec($schema->toSql($this->connection->getDatabasePlatform())[0]);
353 5
    }
354
355
    /**
356
     * Build rate from table row data.
357
     *
358
     * @param array $row Row data.
359
     * @return Rate
360
     */
361 4
    private function buildRateFromTableRowData(array $row)
362
    {
363 4
        return new Rate(
364 4
            $row['source_name'],
365 4
            (float) $row['rate_value'],
366 4
            $row['currency_code'],
367 4
            $row['rate_type'],
368 4
            \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...
369 4
            $row['base_currency_code'],
370 4
            \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...
371 4
            \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...
372
        );
373
    }
374
}
375