DoctrineDbalRepository::all()   F
last analyzed

Complexity

Conditions 10
Paths 512

Size

Total Lines 75

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 10

Importance

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