Completed
Push — master ( 10bc89...0b2a25 )
by Iqbal
22:43
created

Pdo::rollback()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
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 $transactionLevel = 0;
58
59
    /**
60
     * Constructor.
61
     *
62
     * @param PdoConfig $config
63
     */
64
    public function __construct(PdoConfig $config)
65
    {
66
        try {
67
            $this->conn = new PhpPdo($config->getDsn(), $config->getUser(), $config->getPass());
68
            $this->conn->setAttribute(PhpPdo::ATTR_ERRMODE, PhpPdo::ERRMODE_EXCEPTION);
69
        } catch (\PDOException $e) {
70
            throw new PdoException($e);
71
        }
72
73
        $this->config = $config;
74
75
        if (isset($this->quoteMaps[$config->getEngine()])) {
76
            $this->quote = $this->quoteMaps[$config->getEngine()];
77
        }
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function save(ReadModelInterface $model, $table)
84
    {
85
        $class = get_class($model);
86
        $id = $this->resolveValue($model->getId());
87
        if (null !== $prev = $this->findById($id, $table, $class)) {
88
            $values = $this->normalize(array_merge($prev->serialize(), $model->serialize()));
89
            $conditions = $this->normalize(array('id' => $id));
90
            unset($values['id']);
91
            $this->exec($this->buildUpdateQuery($values, $conditions, $table), array_merge($values, $conditions));
92
93
            return;
94
        }
95
96
        $values = $this->normalize($model->serialize());
97
        $values['id'] = $id;
98
        $this->exec($this->buildInsertQuery($values, $table), $values);
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function remove($id, $table)
105
    {
106
        $values = array('id' => $this->resolveValue($id));
107
        $this->exec(sprintf('DELETE FROM %s WHERE id = :id', $this->quote($table)), $values);
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function findById($id, $table, $class)
114
    {
115
        $finder = $this->finder($table, $class);
116
117
        return $finder->where($finder->expr()->equal('id', $this->resolveValue($id)))->first();
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function finder($table, $class)
124
    {
125
        return new PdoFinder($this, $table, $class, $this->config->getParser(), $this->quote);
126
    }
127
128
    /**
129
     * Execute query without statement.
130
     *
131
     * @param string $sql
132
     * @param array  $values
133
     */
134
    public function exec($sql, array $values = array())
135
    {
136
        if (!empty($values)) {
137
            $normalized = array();
138
            foreach ($values as $index => $value) {
139
                $normalized[':' . $index] = $value;
140
            }
141
142
            $values = $normalized;
143
        }
144
145
        try {
146
            $conn = $this->conn->prepare($sql);
147
            $conn->execute($values);
148
        } catch (\PDOException $e) {
149
            throw new PdoException($e);
150
        }
151
    }
152
153
    /**
154
     * Execute query with statement.
155
     *
156
     * @param string $sql
157
     * @param int    $mode
158
     *
159
     * @return PDOStatement
160
     */
161
    public function query($sql, $mode = PhpPdo::FETCH_ASSOC)
162
    {
163
        try {
164
            return $this->conn->query($sql, $mode);
165
        } catch (\PDOException $e) {
166
            throw new PdoException($e);
167
        }
168
    }
169
170
    /**
171
     * Begin transaction
172
     */
173
    public function beginTransaction()
174
    {
175
        if (0 === $this->transactionLevel) {
176
            $this->conn->beginTransaction();
177
        }
178
179
        $this->transactionLevel += 1;
180
    }
181
182
    /**
183
     * Rollback transaction
184
     */
185
    public function rollback()
186
    {
187
        $this->transactionLevel = 0;
188
        $this->conn->rollBack();
189
    }
190
191
    /**
192
     * Commit transaction.
193
     */
194
    public function commit()
195
    {
196
        $this->transactionLevel -= 1;
197
        if (0 === $this->transactionLevel) {
198
            $this->conn->commit();
199
        }
200
    }
201
202
    /**
203
     * Compute fields to expressions.
204
     *
205
     * @param array     $fields
206
     * @param PdoFinder $finder
207
     *
208
     * @return PdoCompositeExpression
209
     */
210
    protected function computeFieldsExpression(array $fields, PdoFinder $finder)
211
    {
212
        $expressions = array();
213
        foreach ($fields as $name => $value) {
214
            $expressions[] = $finder->expr()->equal($name, $value);
215
        }
216
217
        return new PdoCompositeExpression(CompositeExpressionInterface::LOGICAL_AND, $expressions);
218
    }
219
220
    /**
221
     * Normalize value.
222
     *
223
     * @param array $values
224
     *
225
     * @return array
226
     */
227
    protected function normalize(array $values)
228
    {
229
        foreach ($values as $name => $value) {
230
            if (is_array($value)) {
231
                throw new InvalidArgumentException(
232
                    sprintf(
233
                        'Cannot parse value with named "%s", data should be flat array.',
234
                        $name
235
                    )
236
                );
237
            }
238
239
            if (is_bool($value)) {
240
                $value = true === $value ? 'true' : 'false';
241
            }
242
243
            $values[$name] = self::castType($value);
244
        }
245
246
        return $values;
247
    }
248
249
    /**
250
     * Build query update.
251
     *
252
     * @param array  $values
253
     * @param array  $conditions
254
     * @param string $table
255
     *
256
     * @return string
257
     */
258
    protected function buildUpdateQuery(array $values, array $conditions, $table)
259
    {
260
        $sets = $this->buildDataSets($values);
261
        $wheres = $this->buildDataSets($conditions);
262
263
        return
264
            'UPDATE ' . $this->quote($table) .
265
            ' SET ' . implode(', ', $sets) .
266
            ' WHERE ' . implode(' AND ', $wheres);
267
    }
268
269
    /**
270
     * Build insert query.
271
     *
272
     * @param array  $values
273
     * @param string $table
274
     *
275
     * @return string
276
     */
277
    public function buildInsertQuery(array $values, $table)
278
    {
279
        $keys = array_map(array($this, 'quote'), array_keys($values));
280
        $values = array_keys($values);
281
282
        return
283
            'INSERT INTO ' . $this->quote($table) .
284
            ' (' . implode(', ', $keys) . ') VALUES (:' . implode(', :', $values) . ')';
285
    }
286
287
    /**
288
     * Build data sets.
289
     *
290
     * @param array $parts
291
     *
292
     * @return array
293
     */
294
    protected function buildDataSets(array $parts)
295
    {
296
        $sets = array();
297
        foreach ($parts as $name => $value) {
298
            $sets[] = $this->quote($name) . '=:' . $name;
299
        }
300
301
        return $sets;
302
    }
303
304
    /**
305
     * Quote field.
306
     *
307
     * @param string $field
308
     *
309
     * @return string
310
     */
311
    protected function quote($field)
312
    {
313
        return $this->quote . $field . $this->quote;
314
    }
315
316
    /**
317
     * @param mixed $value
318
     *
319
     * @return mixed|null|string
320
     */
321
    protected function resolveValue($value)
322
    {
323
        if ($value instanceof ValuableInterface) {
324
            return $value->getValue();
325
        }
326
327
        if ($value instanceof StringInterface) {
328
            return (string) $value;
329
        }
330
331
        return $value;
332
    }
333
}
334