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
|
|
|
|
223
|
|
|
if ($record = $stmt->fetch()) { |
224
|
|
|
return $this->deserialize($this->bindRelation($this->class, $record, $related), $this->class); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
return null; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Fetch sets of data. |
232
|
|
|
* |
233
|
|
|
* @return Collection |
234
|
|
|
*/ |
235
|
|
|
public function get() |
236
|
|
|
{ |
237
|
|
|
$stmt = $this->conn->query($this->getSQL(), PhpPdo::FETCH_ASSOC); |
238
|
|
|
$related = $this->buildRelations($this->table, $this->conditions, $this->class); |
239
|
|
|
|
240
|
|
|
return new Collection( |
241
|
|
|
array_map( |
242
|
|
|
function ($record) use ($related) { |
243
|
|
|
return $this->bindRelation($this->class, $record, $related); |
244
|
|
|
}, |
245
|
|
|
$stmt->fetchAll() |
246
|
|
|
), |
247
|
|
|
$this->class |
248
|
|
|
); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* @return string |
253
|
|
|
*/ |
254
|
|
|
public function getSQL() |
255
|
|
|
{ |
256
|
|
|
return $this->computeQuery( |
257
|
|
|
'*', |
258
|
|
|
$this->table, |
259
|
|
|
$this->alias, |
260
|
|
|
$this->conditions, |
261
|
|
|
$this->sorts, |
262
|
|
|
$this->joins, |
263
|
|
|
$this->limit, |
264
|
|
|
$this->offset |
265
|
|
|
); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Cast finder to string representation. |
270
|
|
|
* |
271
|
|
|
* @return string |
272
|
|
|
*/ |
273
|
|
|
public function __toString() |
274
|
|
|
{ |
275
|
|
|
return $this->getSQL(); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Deserialize record to read model. |
280
|
|
|
* |
281
|
|
|
* @param array $record |
282
|
|
|
* @param string $class |
283
|
|
|
* |
284
|
|
|
* @return ReadModelInterface |
285
|
|
|
*/ |
286
|
|
|
protected function deserialize(array $record, $class) |
287
|
|
|
{ |
288
|
|
|
return $class::{'deserialize'}($record); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* @param string $table |
293
|
|
|
* @param array $conditions |
294
|
|
|
* @param string $class |
295
|
|
|
* |
296
|
|
|
* @return array |
297
|
|
|
*/ |
298
|
|
|
protected function buildRelations($table, array $conditions, $class) |
299
|
|
|
{ |
300
|
|
|
$relations = $class::{'relations'}(); |
301
|
|
|
$related = array(); |
302
|
|
|
|
303
|
|
|
if (!empty($relations)) { |
304
|
|
|
foreach ($relations as $property => $relation) { |
305
|
|
|
$query = $this->computeQuery($relation['reference'], $table, null, $conditions); |
306
|
|
|
$relationQuery = $this->computeQuery( |
307
|
|
|
'*', |
308
|
|
|
$relation['table'], |
309
|
|
|
null, |
310
|
|
|
array(sprintf('%s in (%s)', $this->quote('id'), $query)) |
311
|
|
|
); |
312
|
|
|
|
313
|
|
|
$results = $this->conn->query($relationQuery, PhpPdo::FETCH_ASSOC)->fetchAll(); |
314
|
|
|
foreach ($results as $result) { |
315
|
|
|
if (!isset($related[$property])) { |
316
|
|
|
$related[$property] = array(); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
$related[$property][$result['id']] = $result; |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
return $related; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* @param string $class |
329
|
|
|
* @param array $record |
330
|
|
|
* @param array $related |
331
|
|
|
* |
332
|
|
|
* @return array |
333
|
|
|
*/ |
334
|
|
|
protected function bindRelation($class, array $record, array $related) |
335
|
|
|
{ |
336
|
|
|
$relations = $class::{'relations'}(); |
337
|
|
|
foreach ($relations as $property => $relation) { |
338
|
|
|
if (isset($related[$property]) && isset($related[$property][$record[$relation['reference']]])) { |
339
|
|
|
$record[$property] = $related[$property][$record[$relation['reference']]]; |
340
|
|
|
} else { |
341
|
|
|
$record[$property] = null; |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
return $record; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* @param string $property |
350
|
|
|
* @param string $alias |
351
|
|
|
* @param string $type |
352
|
|
|
*/ |
353
|
|
|
protected function join($property, $alias, $type) |
354
|
|
|
{ |
355
|
|
|
if (empty($alias)) { |
356
|
|
|
throw new InvalidArgumentException('Missing parameter: $alias'); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
if (!isset($this->relations[$property])) { |
360
|
|
|
throw new InvalidArgumentException( |
361
|
|
|
sprintf( |
362
|
|
|
'Read model "%s" does not have relation "%s"', |
363
|
|
|
$this->class, |
364
|
|
|
$property |
365
|
|
|
) |
366
|
|
|
); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
$relation = $this->relations[$property]; |
370
|
|
|
$this->joins[] = array( |
371
|
|
|
'table' => $relation['table'], |
372
|
|
|
'alias' => $alias, |
373
|
|
|
'reference' => $relation['reference'], |
374
|
|
|
'type' => $type, |
375
|
|
|
); |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* Compute query language. |
380
|
|
|
* |
381
|
|
|
* @param string $fields |
382
|
|
|
* @param string $table |
383
|
|
|
* @param string $alias |
384
|
|
|
* @param array $conditions |
385
|
|
|
* @param array $sorts |
386
|
|
|
* @param array $joins |
387
|
|
|
* @param int $limit |
388
|
|
|
* @param int $offset |
389
|
|
|
* |
390
|
|
|
* @return string |
391
|
|
|
*/ |
392
|
|
|
protected function computeQuery( |
393
|
|
|
$fields = '*', |
394
|
|
|
$table, |
395
|
|
|
$alias = null, |
396
|
|
|
array $conditions = null, |
397
|
|
|
array $sorts = null, |
398
|
|
|
array $joins = null, |
399
|
|
|
$limit = null, |
400
|
|
|
$offset = null |
401
|
|
|
) { |
402
|
|
|
if ($fields != '*') { |
403
|
|
|
$parts = array_map(function($field) { return $this->quote($field); }, explode(',', $fields)); |
404
|
|
|
$fields = implode(',', $parts); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
$parts = array( |
408
|
|
|
'SELECT ' . $fields, |
409
|
|
|
'FROM ' . $this->quote($table), |
410
|
|
|
); |
411
|
|
|
|
412
|
|
|
if (!empty($alias)) { |
413
|
|
|
$parts[] = 'AS ' . $this->quote($alias); |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
if (!empty($joins)) { |
417
|
|
|
foreach ($joins as $join) { |
418
|
|
|
$parts[] = sprintf( |
419
|
|
|
'%s %s AS %s ON %s.%s = %s.%s', |
420
|
|
|
$join['type'], |
421
|
|
|
$this->quote($join['table']), |
422
|
|
|
$this->quote($join['alias']), |
423
|
|
|
$this->quote($join['alias']), |
424
|
|
|
$this->quote('id'), |
425
|
|
|
$this->quote($alias), |
426
|
|
|
$this->quote($join['reference']) |
427
|
|
|
); |
428
|
|
|
} |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
if (!empty($conditions)) { |
432
|
|
|
$parts[] = 'WHERE ' . $this->normalizeConditions($conditions); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
if (!empty($this->sorts)) { |
436
|
|
|
$parts[] = 'ORDER BY ' . $this->normalizeSorts($sorts); |
|
|
|
|
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
if (null !== $limit) { |
440
|
|
|
$parts[] = sprintf('LIMIT %d OFFSET %d', $limit, $offset); |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
return implode(' ', $parts); |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
/** |
447
|
|
|
* Normalize conditions. |
448
|
|
|
* |
449
|
|
|
* @param array $conditions |
450
|
|
|
* |
451
|
|
|
* @return string |
452
|
|
|
*/ |
453
|
|
|
protected function normalizeConditions(array $conditions) |
454
|
|
|
{ |
455
|
|
|
$joiner = ' ' . CompositeExpressionInterface::LOGICAL_AND . ' '; |
456
|
|
|
|
457
|
|
|
return implode( |
458
|
|
|
$joiner, |
459
|
|
|
array_map( |
460
|
|
|
function ($item) { |
461
|
|
|
return (string) $item; |
462
|
|
|
}, |
463
|
|
|
$conditions |
464
|
|
|
) |
465
|
|
|
); |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
/** |
469
|
|
|
* Normalize sorts. |
470
|
|
|
* |
471
|
|
|
* @param array $sorts |
472
|
|
|
* |
473
|
|
|
* @return string |
474
|
|
|
*/ |
475
|
|
|
protected function normalizeSorts(array $sorts) |
476
|
|
|
{ |
477
|
|
|
$normalized = array(); |
478
|
|
|
foreach ($sorts as $field => $direction) { |
479
|
|
|
$normalized[] = $this->quote($field) . ' ' . $direction; |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
return implode(', ', $normalized); |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
/** |
486
|
|
|
* Quote field. |
487
|
|
|
* |
488
|
|
|
* @param string $field |
489
|
|
|
* |
490
|
|
|
* @return string |
491
|
|
|
*/ |
492
|
|
View Code Duplication |
protected function quote($field) |
|
|
|
|
493
|
|
|
{ |
494
|
|
|
$parts = explode('.', $field); |
495
|
|
|
$quotes = array(); |
496
|
|
|
|
497
|
|
|
foreach ($parts as $part) { |
498
|
|
|
$quotes[] = $this->quote . $part . $this->quote; |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
return implode('.', $quotes); |
502
|
|
|
} |
503
|
|
|
} |
504
|
|
|
|
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.