Completed
Push — master ( c4d609...60216c )
by Iqbal
03:34
created

Pdo::normalizeRelation()   C

Complexity

Conditions 12
Paths 15

Size

Total Lines 34
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 34
rs 5.1612
cc 12
eloc 24
nc 15
nop 2

How to fix   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 Borobudur-Cqrs package.
4
 *
5
 * (c) Hexacodelabs <http://hexacodelabs.com>
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 Borobudur\Cqrs\ReadModel\Storage\Pdo;
12
13
use Borobudur\Collection\Collection;
14
use Borobudur\Cqrs\Exception\InvalidArgumentException;
15
use Borobudur\Cqrs\ReadModel\ReadModelInterface;
16
use Borobudur\Cqrs\ReadModel\Storage\Finder\Expression\CompositeExpressionInterface;
17
use Borobudur\Cqrs\ReadModel\Storage\Pdo\Exception\PdoException;
18
use Borobudur\Cqrs\ReadModel\Storage\Pdo\Expression\PdoCompositeExpression;
19
use Borobudur\Cqrs\ReadModel\Storage\StorageInterface;
20
use Borobudur\Cqrs\Serializer\DataTypeCasterTrait;
21
use Borobudur\Serialization\StringInterface;
22
use Borobudur\Serialization\ValuableInterface;
23
use PDO as PhpPdo;
24
use PDOStatement;
25
26
/**
27
 * @author      Iqbal Maulana <[email protected]>
28
 * @created     8/18/15
29
 */
30
class Pdo implements StorageInterface
31
{
32
    use DataTypeCasterTrait;
33
34
    /**
35
     * @var PhpPdo
36
     */
37
    protected $conn;
38
39
    /**
40
     * @var PdoConfig
41
     */
42
    protected $config;
43
44
    /**
45
     * @var array
46
     */
47
    protected $quoteMaps = array('mysql' => '`', 'pgsql' => '"');
48
49
    /**
50
     * @var string
51
     */
52
    protected $quote = '`';
53
54
    /**
55
     * @var int
56
     */
57
    protected $transactionalLevel = 0;
58
59
    /**
60
     * @var bool
61
     */
62
    protected $transactional = false;
63
64
    /**
65
     * Constructor.
66
     *
67
     * @param PdoConfig $config
68
     */
69
    public function __construct(PdoConfig $config)
70
    {
71
        try {
72
            $this->conn = new PhpPdo($config->getDsn(), $config->getUser(), $config->getPass());
73
            $this->conn->setAttribute(PhpPdo::ATTR_ERRMODE, PhpPdo::ERRMODE_EXCEPTION);
74
        } catch (\PDOException $e) {
75
            throw new PdoException($e);
76
        }
77
78
        $this->config = $config;
79
80
        if (isset($this->quoteMaps[$config->getEngine()])) {
81
            $this->quote = $this->quoteMaps[$config->getEngine()];
82
        }
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function save(ReadModelInterface $model, $table)
89
    {
90
        $class = get_class($model);
91
        $id = $this->resolveValue($model->getId());
92
        if (null !== $prev = $this->findById($id, $table, $class)) {
93
            $values = $this->normalize($this->normalizeRelation(array_merge($prev->serialize(), $model->serialize()), $class));
94
            $conditions = $this->normalize(array('id' => $id));
95
            unset($values['id']);
96
            $this->exec($this->buildUpdateQuery($values, $conditions, $table), array_merge($values, $conditions));
97
98
            return;
99
        }
100
101
        $values = $this->normalize($this->normalizeRelation($model->serialize(), $class));
102
        $values['id'] = $id;
103
        $this->exec($this->buildInsertQuery($values, $table), $values);
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function remove($id, $table)
110
    {
111
        $values = array('id' => $this->resolveValue($id));
112
        $this->exec(sprintf('DELETE FROM %s WHERE id = :id', $this->quote($table)), $values);
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    public function findById($id, $table, $class)
119
    {
120
        $finder = $this->finder($table, $class);
121
122
        return $finder->where($finder->expr()->equal('id', $this->resolveValue($id)))->first();
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function finder($table, $class)
129
    {
130
        return new PdoFinder($this, $table, $class, $this->config->getParser(), $this->quote);
131
    }
132
133
    /**
134
     * Execute query without statement.
135
     *
136
     * @param string $sql
137
     * @param array  $values
138
     */
139
    public function exec($sql, array $values = array())
140
    {
141
        if (!empty($values)) {
142
            $normalized = array();
143
            foreach ($values as $index => $value) {
144
                $normalized[':' . $index] = $value;
145
            }
146
147
            $values = $normalized;
148
        }
149
150
        try {
151
            $conn = $this->conn->prepare($sql);
152
            $conn->execute($values);
153
        } catch (\PDOException $e) {
154
            throw new PdoException($e);
155
        }
156
    }
157
158
    /**
159
     * Execute query with statement.
160
     *
161
     * @param string $sql
162
     * @param int    $mode
163
     *
164
     * @return PDOStatement
165
     */
166
    public function query($sql, $mode = PhpPdo::FETCH_ASSOC)
167
    {
168
        try {
169
            return $this->conn->query($sql, $mode);
170
        } catch (\PDOException $e) {
171
            throw new PdoException($e);
172
        }
173
    }
174
175
    /**
176
     * @return int
177
     */
178
    public function getTransactionalLevel()
179
    {
180
        return $this->transactionalLevel;
181
    }
182
183
    /**
184
     * @return boolean
185
     */
186
    public function isTransactional()
187
    {
188
        return $this->transactional;
189
    }
190
191
    /**
192
     * Begin transaction
193
     */
194
    public function beginTransaction()
195
    {
196
        $this->transactional = true;
197
        if (0 === $this->transactionalLevel) {
198
            $this->conn->beginTransaction();
199
        }
200
201
        $this->transactionalLevel += 1;
202
    }
203
204
    /**
205
     * Rollback transaction
206
     */
207
    public function rollback()
208
    {
209
        if (true === $this->transactional) {
210
            $this->transactionalLevel = 0;
211
            $this->transactional = false;
212
            $this->conn->rollBack();
213
        }
214
    }
215
216
    /**
217
     * Commit transaction.
218
     */
219
    public function commit()
220
    {
221
        if (true === $this->transactional) {
222
            $this->transactionalLevel -= 1;
223
            if (0 === $this->transactionalLevel) {
224
                $this->transactional = false;
225
                $this->conn->commit();
226
            }
227
        }
228
    }
229
230
    /**
231
     * Compute fields to expressions.
232
     *
233
     * @param array     $fields
234
     * @param PdoFinder $finder
235
     *
236
     * @return PdoCompositeExpression
237
     */
238
    protected function computeFieldsExpression(array $fields, PdoFinder $finder)
239
    {
240
        $expressions = array();
241
        foreach ($fields as $name => $value) {
242
            $expressions[] = $finder->expr()->equal($name, $value);
243
        }
244
245
        return new PdoCompositeExpression(CompositeExpressionInterface::LOGICAL_AND, $expressions);
246
    }
247
248
    /**
249
     * @param array  $values
250
     * @param string $class
251
     *
252
     * @return array
253
     */
254
    protected function normalizeRelation(array $values, $class)
255
    {
256
        $relations = $class::{'relations'}();
257
        foreach ($relations as $property => $relation) {
258
            $field = $relation['reference'];
259
            if (isset($values[$field])) {
260
                foreach ($values as $index => $value) {
261
                    if (false !== strpos($index, $property) && $field !== $index) {
262
                        unset($values[$index]);
263
                    }
264
                }
265
            } elseif (isset($values[$property])) {
266
                $value = $values[$property];
267
                if (is_object($value)) {
268
                    if (method_exists($value, 'getId')) {
269
                        $value = $value->{'getId'}();
270
                    } elseif (property_exists($value, 'id')) {
271
                        $value = $value->{'id'};
272
                    } else {
273
                        $value = null;
274
                    }
275
                } elseif (is_array($value)) {
276
                    $value = $value['id'];
277
                }
278
279
                $values[$field] = $value ? (string) $value : null;
280
                unset($values[$property]);
281
            } else {
282
                $values[$field] = null;
283
            }
284
        }
285
286
        return $values;
287
    }
288
289
    /**
290
     * Normalize value.
291
     *
292
     * @param array $values
293
     *
294
     * @return array
295
     */
296
    protected function normalize(array $values)
297
    {
298
        foreach ($values as $name => $value) {
299
            if (is_array($value)) {
300
                throw new InvalidArgumentException(
301
                    sprintf(
302
                        'Cannot parse value with named "%s", data should be flat array.',
303
                        $name
304
                    )
305
                );
306
            }
307
308
            if (is_bool($value)) {
309
                $value = true === $value ? 'true' : 'false';
310
            }
311
312
            $values[$name] = self::castType($value);
313
        }
314
315
        return $values;
316
    }
317
318
    /**
319
     * Build query update.
320
     *
321
     * @param array  $values
322
     * @param array  $conditions
323
     * @param string $table
324
     *
325
     * @return string
326
     */
327
    protected function buildUpdateQuery(array $values, array $conditions, $table)
328
    {
329
        $sets = $this->buildDataSets($values);
330
        $wheres = $this->buildDataSets($conditions);
331
332
        return
333
            'UPDATE ' . $this->quote($table) .
334
            ' SET ' . implode(', ', $sets) .
335
            ' WHERE ' . implode(' AND ', $wheres);
336
    }
337
338
    /**
339
     * Build insert query.
340
     *
341
     * @param array  $values
342
     * @param string $table
343
     *
344
     * @return string
345
     */
346
    public function buildInsertQuery(array $values, $table)
347
    {
348
        $keys = array_map(array($this, 'quote'), array_keys($values));
349
        $values = array_keys($values);
350
351
        return
352
            'INSERT INTO ' . $this->quote($table) .
353
            ' (' . implode(', ', $keys) . ') VALUES (:' . implode(', :', $values) . ')';
354
    }
355
356
    /**
357
     * Build data sets.
358
     *
359
     * @param array $parts
360
     *
361
     * @return array
362
     */
363
    protected function buildDataSets(array $parts)
364
    {
365
        $sets = array();
366
        foreach ($parts as $name => $value) {
367
            $sets[] = $this->quote($name) . '=:' . $name;
368
        }
369
370
        return $sets;
371
    }
372
373
    /**
374
     * Quote field.
375
     *
376
     * @param string $field
377
     *
378
     * @return string
379
     */
380
    protected function quote($field)
381
    {
382
        return $this->quote . $field . $this->quote;
383
    }
384
385
    /**
386
     * @param mixed $value
387
     *
388
     * @return mixed|null|string
389
     */
390
    protected function resolveValue($value)
391
    {
392
        if ($value instanceof ValuableInterface) {
393
            return $value->getValue();
394
        }
395
396
        if ($value instanceof StringInterface) {
397
            return (string) $value;
398
        }
399
400
        return $value;
401
    }
402
}
403