Completed
Push — master ( 60216c...2a3eab )
by Iqbal
12:13
created

PdoFinder::reset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 7
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\Cqrs\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\Finder\FinderInterface;
18
use Borobudur\Cqrs\ReadModel\Storage\Pdo\Expression\PdoCompositeExpression;
19
use Borobudur\Cqrs\ReadModel\Storage\Pdo\Expression\PdoExpression;
20
use Borobudur\Cqrs\ReadModel\Storage\Pdo\Parser\ParserInterface;
21
use PDO as PhpPdo;
22
23
/**
24
 * @author      Iqbal Maulana <[email protected]>
25
 * @created     8/18/15
26
 */
27
class PdoFinder implements FinderInterface
28
{
29
    /**
30
     * @var Pdo
31
     */
32
    private $conn;
33
34
    /**
35
     * @var string
36
     */
37
    private $table;
38
39
    /**
40
     * @var string
41
     */
42
    private $class;
43
44
    /**
45
     * @var array
46
     */
47
    private $sorts = array();
48
49
    /**
50
     * @var int
51
     */
52
    private $limit;
53
54
    /**
55
     * @var int
56
     */
57
    private $offset = 0;
58
59
    /**
60
     * @var CompositeExpressionInterface[]
61
     */
62
    private $conditions = array();
63
64
    /**
65
     * @var array
66
     */
67
    private $joins = array();
68
69
    /**
70
     * @var ParserInterface
71
     */
72
    private $parser;
73
74
    /**
75
     * @var string
76
     */
77
    private $quote;
78
79
    /**
80
     * @var string
81
     */
82
    private $alias;
83
84
    /**
85
     * @var array
86
     */
87
    private $relations = array();
88
89
    /**
90
     * Constructor.
91
     *
92
     * @param Pdo             $conn
93
     * @param string          $table
94
     * @param string          $class
95
     * @param ParserInterface $parser
96
     * @param string          $quote
97
     * @param string|null     $alias
98
     */
99
    public function __construct(Pdo &$conn, $table, $class, $parser, $quote, $alias = null)
100
    {
101
        if (null === $alias) {
102
            $alias = lcfirst($table);
103
        }
104
105
        $this->conn = $conn;
106
        $this->table = $table;
107
        $this->class = $class;
108
        $this->parser = $parser;
109
        $this->parser->setQuote($quote);
110
        $this->quote = $quote;
111
        $this->alias = $alias;
112
        $this->relations = $class::{'relations'}();
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    public function sort(array $sorts)
119
    {
120
        $this->sorts = $sorts;
121
122
        return $this;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function limit($limit, $offset = 0)
129
    {
130
        $this->limit = $limit;
131
        $this->offset = $offset;
132
133
        return $this;
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139
    public function expr()
140
    {
141
        return new PdoExpression($this->parser);
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147
    public function where($expr)
148
    {
149
        if (!(1 === func_num_args() && $expr instanceof CompositeExpressionInterface)) {
150
            $expr = new PdoCompositeExpression(CompositeExpressionInterface::LOGICAL_AND, func_get_args());
151
        }
152
153
        $this->conditions[] = $expr;
154
155
        return $this;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function innerJoin($property, $alias)
162
    {
163
        $this->join($property, $alias, 'INNER JOIN');
164
165
        return $this;
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171
    public function leftJoin($property, $alias)
172
    {
173
        $this->join($property, $alias, 'LEFT JOIN');
174
175
        return $this;
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181
    public function rightJoin($property, $alias)
182
    {
183
        $this->join($property, $alias, 'RIGHT JOIN');
184
185
        return $this;
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191
    public function count()
192
    {
193
        $stmt = $this->conn->query(
194
            $this->computeQuery(
195
                'COUNT(*) AS num',
196
                $this->table,
197
                $this->alias,
198
                $this->conditions,
199
                $this->sorts,
200
                $this->joins,
201
                $this->limit,
202
                $this->offset
203
            ),
204
            PhpPdo::FETCH_ASSOC
205
        );
206
207
        if ($result = $stmt->fetch()) {
208
            return (int) $result['num'];
209
        }
210
211
        return 0;
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217
    public function first()
218
    {
219
        $this->limit(1);
220
        $stmt = $this->conn->query($this->getSQL(), PhpPdo::FETCH_ASSOC);
221
        $related = $this->buildRelations($this->table, $this->conditions, $this->class);
222
        $result = null;
223
    
224
        if ($record = $stmt->fetch()) {
225
            $result = $this->deserialize($this->bindRelation($this->class, $record, $related), $this->class);
226
        }
227
    
228
        $this->reset();
229
    
230
        return $result;
231
    }
232
233
    /**
234
     * Fetch sets of data.
235
     *
236
     * @return Collection
237
     */
238
    public function get()
239
    {
240
        $stmt = $this->conn->query($this->getSQL(), PhpPdo::FETCH_ASSOC);
241
        $related = $this->buildRelations($this->table, $this->conditions, $this->class);
242
    
243
        $results = new Collection(
244
            array_map(
245
                function ($record) use ($related) {
246
                    return $this->bindRelation($this->class, $record, $related);
247
                },
248
                $stmt->fetchAll()
249
            ),
250
            $this->class
251
        );
252
    
253
        $this->reset();
254
    
255
        return $results;
256
    }
257
258
    /**
259
     * @return string
260
     */
261
    public function getSQL()
262
    {
263
        return $this->computeQuery(
264
            '*',
265
            $this->table,
266
            $this->alias,
267
            $this->conditions,
268
            $this->sorts,
269
            $this->joins,
270
            $this->limit,
271
            $this->offset
272
        );
273
    }
274
275
    /**
276
     * Cast finder to string representation.
277
     *
278
     * @return string
279
     */
280
    public function __toString()
281
    {
282
        return $this->getSQL();
283
    }
284
285
    /**
286
     * Deserialize record to read model.
287
     *
288
     * @param array  $record
289
     * @param string $class
290
     *
291
     * @return ReadModelInterface
292
     */
293
    protected function deserialize(array $record, $class)
294
    {
295
        return $class::{'deserialize'}($record);
296
    }
297
298
    /**
299
     * @param string $table
300
     * @param array  $conditions
301
     * @param string $class
302
     *
303
     * @return array
304
     */
305
    protected function buildRelations($table, array $conditions, $class)
306
    {
307
        $relations = $class::{'relations'}();
308
        $related = array();
309
310
        if (!empty($relations)) {
311
            foreach ($relations as $property => $relation) {
312
                $query = $this->computeQuery($relation['reference'], $table, null, $conditions);
313
                $relationQuery = $this->computeQuery(
314
                    '*',
315
                    $relation['table'],
316
                    null,
317
                    array(sprintf('%s in (%s)', $this->quote('id'), $query))
318
                );
319
320
                $results = $this->conn->query($relationQuery, PhpPdo::FETCH_ASSOC)->fetchAll();
321
                foreach ($results as $result) {
322
                    if (!isset($related[$property])) {
323
                        $related[$property] = array();
324
                    }
325
326
                    $related[$property][$result['id']] = $result;
327
                }
328
            }
329
        }
330
331
        return $related;
332
    }
333
334
    /**
335
     * @param string $class
336
     * @param array  $record
337
     * @param array  $related
338
     *
339
     * @return array
340
     */
341
    protected function bindRelation($class, array $record, array $related)
342
    {
343
        $relations = $class::{'relations'}();
344
        foreach ($relations as $property => $relation) {
345
            if (isset($related[$property]) && isset($related[$property][$record[$relation['reference']]])) {
346
                $record[$property] = $related[$property][$record[$relation['reference']]];
347
            } else {
348
                $record[$property] = null;
349
            }
350
        }
351
352
        return $record;
353
    }
354
355
    /**
356
     * @param string $property
357
     * @param string $alias
358
     * @param string $type
359
     */
360
    protected function join($property, $alias, $type)
361
    {
362
        if (empty($alias)) {
363
            throw new InvalidArgumentException('Missing parameter: $alias');
364
        }
365
366
        if (!isset($this->relations[$property])) {
367
            throw new InvalidArgumentException(
368
                sprintf(
369
                    'Read model "%s" does not have relation "%s"',
370
                    $this->class,
371
                    $property
372
                )
373
            );
374
        }
375
376
        $relation = $this->relations[$property];
377
        $this->joins[] = array(
378
            'table'     => $relation['table'],
379
            'alias'     => $alias,
380
            'reference' => $relation['reference'],
381
            'type'      => $type,
382
        );
383
    }
384
385
    /**
386
     * Compute query language.
387
     *
388
     * @param string $fields
389
     * @param string $table
390
     * @param string $alias
391
     * @param array  $conditions
392
     * @param array  $sorts
393
     * @param array  $joins
394
     * @param int    $limit
395
     * @param int    $offset
396
     *
397
     * @return string
398
     */
399
    protected function computeQuery(
400
        $fields = '*',
401
        $table,
402
        $alias = null,
403
        array $conditions = null,
404
        array $sorts = null,
405
        array $joins = null,
406
        $limit = null,
407
        $offset = null
408
    ) {
409
        if ($fields != '*' && false === strpos($fields, '(')) {
410
            $parts = array_map(function($field) { return $this->quote($field); }, explode(',', $fields));
411
            $fields = implode(',', $parts);
412
        }
413
414
        $parts = array(
415
            'SELECT ' . $fields,
416
            'FROM ' . $this->quote($table),
417
        );
418
419
        if (!empty($alias)) {
420
            $parts[] = 'AS ' . $this->quote($alias);
421
        }
422
423
        if (!empty($joins)) {
424
            foreach ($joins as $join) {
425
                $parts[] = sprintf(
426
                    '%s %s AS %s ON %s.%s = %s.%s',
427
                    $join['type'],
428
                    $this->quote($join['table']),
429
                    $this->quote($join['alias']),
430
                    $this->quote($join['alias']),
431
                    $this->quote('id'),
432
                    $this->quote($alias),
433
                    $this->quote($join['reference'])
434
                );
435
            }
436
        }
437
438
        if (!empty($conditions)) {
439
            $parts[] = 'WHERE ' . $this->normalizeConditions($conditions);
440
        }
441
442
        if (!empty($this->sorts)) {
443
            $parts[] = 'ORDER BY ' . $this->normalizeSorts($sorts);
0 ignored issues
show
Bug introduced by
It seems like $sorts defined by parameter $sorts on line 404 can also be of type null; however, Borobudur\Cqrs\ReadModel...inder::normalizeSorts() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
444
        }
445
446
        if (null !== $limit) {
447
            $parts[] = sprintf('LIMIT %d OFFSET %d', $limit, $offset);
448
        }
449
450
        return implode(' ', $parts);
451
    }
452
453
    /**
454
     * Normalize conditions.
455
     *
456
     * @param array $conditions
457
     *
458
     * @return string
459
     */
460
    protected function normalizeConditions(array $conditions)
461
    {
462
        $joiner = ' ' . CompositeExpressionInterface::LOGICAL_AND . ' ';
463
464
        return implode(
465
            $joiner,
466
            array_map(
467
                function ($item) {
468
                    return (string) $item;
469
                },
470
                $conditions
471
            )
472
        );
473
    }
474
475
    /**
476
     * Normalize sorts.
477
     *
478
     * @param array $sorts
479
     *
480
     * @return string
481
     */
482
    protected function normalizeSorts(array $sorts)
483
    {
484
        $normalized = array();
485
        foreach ($sorts as $field => $direction) {
486
            $normalized[] = $this->quote($field) . ' ' . $direction;
487
        }
488
489
        return implode(', ', $normalized);
490
    }
491
492
    /**
493
     * Quote field.
494
     *
495
     * @param string $field
496
     *
497
     * @return string
498
     */
499 View Code Duplication
    protected function quote($field)
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...
500
    {
501
        $parts = explode('.', $field);
502
        $quotes = array();
503
504
        foreach ($parts as $part) {
505
            $quotes[] = $this->quote . $part . $this->quote;
506
        }
507
508
        return implode('.', $quotes);
509
    }
510
511
    /**
512
     * Reset statements.
513
     */
514
    protected function reset()
515
    {
516
        $this->conditions = array();
517
        $this->limit = null;
518
        $this->offset = 0;
519
        $this->sorts = array();
520
        $this->joins = array();
521
        $this->relations = array();
522
    }
523
}
524