Pdo::save()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 12
nc 2
nop 2
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 View Code Duplication
    public function beginTransaction()
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...
195
    {
196
        $this->transactional = true;
197
        if (0 === $this->transactionalLevel) {
198
            if ($this->conn->inTransaction()) {
199
                $this->conn->beginTransaction();
200
            }
201
        }
202
203
        $this->transactionalLevel += 1;
204
    }
205
206
    /**
207
     * Rollback transaction
208
     */
209 View Code Duplication
    public function rollback()
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...
210
    {
211
        if (true === $this->transactional) {
212
            $this->transactionalLevel = 0;
213
            $this->transactional = false;
214
            if ($this->conn->inTransaction()) {
215
                $this->conn->rollBack();
216
            }
217
        }
218
    }
219
220
    /**
221
     * Commit transaction.
222
     */
223 View Code Duplication
    public function commit()
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...
224
    {
225
        if (true === $this->transactional) {
226
            $this->transactionalLevel -= 1;
227
            if (0 === $this->transactionalLevel) {
228
                $this->transactional = false;
229
                if ($this->conn->inTransaction()) {
230
                    $this->conn->commit();
231
                }
232
            }
233
        }
234
    }
235
236
    /**
237
     * Compute fields to expressions.
238
     *
239
     * @param array     $fields
240
     * @param PdoFinder $finder
241
     *
242
     * @return PdoCompositeExpression
243
     */
244
    protected function computeFieldsExpression(array $fields, PdoFinder $finder)
245
    {
246
        $expressions = array();
247
        foreach ($fields as $name => $value) {
248
            $expressions[] = $finder->expr()->equal($name, $value);
249
        }
250
251
        return new PdoCompositeExpression(CompositeExpressionInterface::LOGICAL_AND, $expressions);
252
    }
253
254
    /**
255
     * @param array  $values
256
     * @param string $class
257
     *
258
     * @return array
259
     */
260
    protected function normalizeRelation(array $values, $class)
261
    {
262
        $relations = $class::{'relations'}();
263
        foreach ($relations as $property => $relation) {
264
            $field = $relation['reference'];
265
            if (isset($values[$field])) {
266
                foreach ($values as $index => $value) {
267
                    if (false !== strpos($index, $property) && $field !== $index) {
268
                        unset($values[$index]);
269
                    }
270
                }
271
            } elseif (isset($values[$property])) {
272
                $value = $values[$property];
273
                if (is_object($value)) {
274
                    if (method_exists($value, 'getId')) {
275
                        $value = $value->{'getId'}();
276
                    } elseif (property_exists($value, 'id')) {
277
                        $value = $value->{'id'};
278
                    } else {
279
                        $value = null;
280
                    }
281
                } elseif (is_array($value)) {
282
                    $value = $value['id'];
283
                }
284
285
                $values[$field] = $value ? (string) $value : null;
286
                unset($values[$property]);
287
            } else {
288
                $values[$field] = null;
289
            }
290
        }
291
292
        return $values;
293
    }
294
295
    /**
296
     * Normalize value.
297
     *
298
     * @param array $values
299
     *
300
     * @return array
301
     */
302
    protected function normalize(array $values)
303
    {
304
        foreach ($values as $name => $value) {
305
            if (is_array($value)) {
306
                throw new InvalidArgumentException(
307
                    sprintf(
308
                        'Cannot parse value with named "%s", data should be flat array.',
309
                        $name
310
                    )
311
                );
312
            }
313
314
            if (is_bool($value)) {
315
                $value = true === $value ? 'true' : 'false';
316
            }
317
318
            $values[$name] = self::castType($value);
319
        }
320
321
        return $values;
322
    }
323
324
    /**
325
     * Build query update.
326
     *
327
     * @param array  $values
328
     * @param array  $conditions
329
     * @param string $table
330
     *
331
     * @return string
332
     */
333
    protected function buildUpdateQuery(array $values, array $conditions, $table)
334
    {
335
        $sets = $this->buildDataSets($values);
336
        $wheres = $this->buildDataSets($conditions);
337
338
        return
339
            'UPDATE ' . $this->quote($table) .
340
            ' SET ' . implode(', ', $sets) .
341
            ' WHERE ' . implode(' AND ', $wheres);
342
    }
343
344
    /**
345
     * Build insert query.
346
     *
347
     * @param array  $values
348
     * @param string $table
349
     *
350
     * @return string
351
     */
352
    public function buildInsertQuery(array $values, $table)
353
    {
354
        $keys = array_map(array($this, 'quote'), array_keys($values));
355
        $values = array_keys($values);
356
357
        return
358
            'INSERT INTO ' . $this->quote($table) .
359
            ' (' . implode(', ', $keys) . ') VALUES (:' . implode(', :', $values) . ')';
360
    }
361
362
    /**
363
     * Build data sets.
364
     *
365
     * @param array $parts
366
     *
367
     * @return array
368
     */
369
    protected function buildDataSets(array $parts)
370
    {
371
        $sets = array();
372
        foreach ($parts as $name => $value) {
373
            $sets[] = $this->quote($name) . '=:' . $name;
374
        }
375
376
        return $sets;
377
    }
378
379
    /**
380
     * Quote field.
381
     *
382
     * @param string $field
383
     *
384
     * @return string
385
     */
386
    protected function quote($field)
387
    {
388
        return $this->quote . $field . $this->quote;
389
    }
390
391
    /**
392
     * @param mixed $value
393
     *
394
     * @return mixed|null|string
395
     */
396
    protected function resolveValue($value)
397
    {
398
        if ($value instanceof ValuableInterface) {
399
            return $value->getValue();
400
        }
401
402
        if ($value instanceof StringInterface) {
403
            return (string) $value;
404
        }
405
406
        return $value;
407
    }
408
}
409