Completed
Push — master ( 39b308...0f3d85 )
by Philip
03:06
created

MySQLData::getMany()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 21
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 21
rs 8.7624
nc 12
cc 5
eloc 18
nop 2
1
<?php
2
3
/*
4
 * This file is part of the CRUDlex package.
5
 *
6
 * (c) Philip Lehmann-Böhm <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace CRUDlex;
13
14
use Doctrine\DBAL\Query\QueryBuilder;
15
use Doctrine\DBAL\Connection;
16
17
/**
18
 * MySQL Data implementation using a given Doctrine DBAL instance.
19
 */
20
class MySQLData extends AbstractData {
21
22
    /**
23
     * Holds the Doctrine DBAL instance.
24
     */
25
    protected $database;
26
27
    /**
28
     * Flag whether to use UUIDs as primary key.
29
     */
30
    protected $useUUIDs;
31
32
    /**
33
     * Sets the values and parameters of the upcoming given query according
34
     * to the entity.
35
     *
36
     * @param Entity $entity
37
     * the entity with its fields and values
38
     * @param QueryBuilder $queryBuilder
39
     * the upcoming query
40
     * @param string $setMethod
41
     * what method to use on the QueryBuilder: 'setValue' or 'set'
42
     */
43
    protected function setValuesAndParameters(Entity $entity, QueryBuilder $queryBuilder, $setMethod) {
44
        $formFields = $this->definition->getEditableFieldNames();
45
        $count      = count($formFields);
46
        for ($i = 0; $i < $count; ++$i) {
47
            $type  = $this->definition->getType($formFields[$i]);
48
            $value = $entity->get($formFields[$i]);
49
            if ($type == 'boolean') {
50
                $value = $value ? 1 : 0;
51
            }
52
            $queryBuilder->$setMethod('`'.$formFields[$i].'`', '?');
53
            $queryBuilder->setParameter($i, $value);
54
        }
55
    }
56
57
    /**
58
     * Performs the cascading children deletion.
59
     *
60
     * @param integer $id
61
     * the current entities id
62
     * @param boolean $deleteCascade
63
     * whether to delete children and sub children
64
     */
65
    protected function deleteChildren($id, $deleteCascade) {
66
        foreach ($this->definition->getChildren() as $childArray) {
67
            $childData = $this->definition->getServiceProvider()->getData($childArray[2]);
68
            $children  = $childData->listEntries([$childArray[1] => $id]);
69
            foreach ($children as $child) {
70
                $childData->doDelete($child, $deleteCascade);
71
            }
72
        }
73
    }
74
75
    /**
76
     * Checks whether the by id given entity still has children referencing it.
77
     *
78
     * @param integer $id
79
     * the current entities id
80
     *
81
     * @return boolean
82
     * true if the entity still has children
83
     */
84
    protected function hasChildren($id) {
85
        foreach ($this->definition->getChildren() as $child) {
86
            $queryBuilder = $this->database->createQueryBuilder();
87
            $queryBuilder
88
                ->select('COUNT(id)')
89
                ->from('`'.$child[0].'`', '`'.$child[0].'`')
90
                ->where('`'.$child[1].'` = ?')
91
                ->andWhere('deleted_at IS NULL')
92
                ->setParameter(0, $id);
93
            $queryResult = $queryBuilder->execute();
94
            $result      = $queryResult->fetch(\PDO::FETCH_NUM);
95
            if ($result[0] > 0) {
96
                return true;
97
            }
98
        }
99
        return false;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    protected function doDelete(Entity $entity, $deleteCascade) {
106
        $result = $this->shouldExecuteEvents($entity, 'before', 'delete');
107
        if (!$result) {
108
            return static::DELETION_FAILED_EVENT;
109
        }
110
        $id = $entity->get('id');
111
        if ($deleteCascade) {
112
            $this->deleteChildren($id, $deleteCascade);
113
        } elseif ($this->hasChildren($id)) {
114
            return static::DELETION_FAILED_STILL_REFERENCED;
115
        }
116
117
        $query = $this->database->createQueryBuilder();
118
        $query
119
            ->update('`'.$this->definition->getTable().'`')
120
            ->set('deleted_at', 'UTC_TIMESTAMP()')
121
            ->where('id = ?')
122
            ->setParameter(0, $id);
123
124
        $query->execute();
125
        $this->shouldExecuteEvents($entity, 'after', 'delete');
126
        return static::DELETION_SUCCESS;
127
    }
128
129
    /**
130
     * Adds sorting parameters to the query.
131
     *
132
     * @param QueryBuilder $queryBuilder
133
     * the query
134
     * @param $filter
135
     * the filter all resulting entities must fulfill, the keys as field names
136
     * @param $filterOperators
137
     * the operators of the filter like "=" defining the full condition of the field
138
     */
139
    protected function addFilter(QueryBuilder $queryBuilder, array $filter, array $filterOperators) {
140
        $i = 0;
141
        foreach ($filter as $field => $value) {
142
            if ($value === null) {
143
                $queryBuilder->andWhere('`'.$field.'` IS NULL');
144
            } else {
145
                $operator = array_key_exists($field, $filterOperators) ? $filterOperators[$field] : '=';
146
                $queryBuilder
147
                    ->andWhere('`'.$field.'` '.$operator.' ?')
148
                    ->setParameter($i, $value);
149
            }
150
            $i++;
151
        }
152
    }
153
154
    /**
155
     * Adds pagination parameters to the query.
156
     *
157
     * @param QueryBuilder $queryBuilder
158
     * the query
159
     * @param integer|null $skip
160
     * the rows to skip
161
     * @param integer|null $amount
162
     * the maximum amount of rows
163
     */
164
    protected function addPagination(QueryBuilder $queryBuilder, $skip, $amount) {
165
        $queryBuilder->setMaxResults(9999999999);
166
        if ($amount !== null) {
167
            $queryBuilder->setMaxResults(abs(intval($amount)));
168
        }
169
        if ($skip !== null) {
170
            $queryBuilder->setFirstResult(abs(intval($skip)));
171
        }
172
    }
173
174
    /**
175
     * Adds sorting parameters to the query.
176
     *
177
     * @param QueryBuilder $queryBuilder
178
     * the query
179
     * @param string|null $sortField
180
     * the sort field
181
     * @param boolean|null $sortAscending
182
     * true if sort ascending, false if descending
183
     */
184
    protected function addSort(QueryBuilder $queryBuilder, $sortField, $sortAscending) {
185
        if ($sortField !== null) {
186
            $order = $sortAscending === true ? 'ASC' : 'DESC';
187
            $queryBuilder->orderBy('`'.$sortField.'`', $order);
188
        }
189
    }
190
191
    /**
192
     * Adds the id and name of referenced entities to the given entities. The
193
     * reference field is before the raw id of the referenced entity and after
194
     * the fetch, it's an array with the keys id and name.
195
     *
196
     * @param Entity[] &$entities
197
     * the entities to fetch the references for
198
     * @param string $field
199
     * the reference field
200
     */
201
    protected function fetchReferencesForField(array &$entities, $field) {
202
        $nameField    = $this->definition->getReferenceNameField($field);
203
        $queryBuilder = $this->database->createQueryBuilder();
204
205
        $ids = array_map(function(Entity $entity) use ($field) {
206
            return $entity->get($field);
207
        }, $entities);
208
209
        $referenceEntity = $this->definition->getReferenceEntity($field);
210
        $table           = $this->definition->getServiceProvider()->getData($referenceEntity)->getDefinition()->getTable();
211
        $queryBuilder
212
            ->from('`'.$table.'`', '`'.$table.'`')
213
            ->where('id IN (?)')
214
            ->andWhere('deleted_at IS NULL');
215
        if ($nameField) {
216
            $queryBuilder->select('id', $nameField);
217
        } else {
218
            $queryBuilder->select('id');
219
        }
220
221
        $queryBuilder->setParameter(0, $ids, Connection::PARAM_INT_ARRAY);
222
223
        $queryResult = $queryBuilder->execute();
224
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
225
        $amount      = count($entities);
226
        foreach ($rows as $row) {
227
            for ($i = 0; $i < $amount; ++$i) {
228
                if ($entities[$i]->get($field) == $row['id']) {
229
                    $value = ['id' => $entities[$i]->get($field)];
230
                    if ($nameField) {
231
                        $value['name'] = $row[$nameField];
232
                    }
233
                    $entities[$i]->set($field, $value);
234
                }
235
            }
236
        }
237
    }
238
239
    /**
240
     * Generates a new UUID.
241
     *
242
     * @return string|null
243
     * the new UUID or null if this instance isn't configured to do so
244
     */
245
    protected function generateUUID() {
246
        $uuid = null;
247
        if ($this->useUUIDs) {
248
            $sql    = 'SELECT UUID() as id';
249
            $result = $this->database->fetchAssoc($sql);
250
            $uuid   = $result['id'];
251
        }
252
        return $uuid;
253
    }
254
255
    protected function enrichWithMany(array $rows) {
256
        $fields     = $this->definition->getFieldNames(true);
257
        $manyFields = array_filter($fields, function($field) {
258
            return $this->definition->getType($field) === 'many';
259
        });
260
        $mapping = [];
261
        foreach ($rows as $row) {
262
            foreach ($manyFields as $manyField) {
263
                $row[$manyField] = [];
264
            }
265
            $mapping[$row['id']] = $row;
266
        }
267
        foreach ($manyFields as $manyField) {
268
            $queryBuilder = $this->database->createQueryBuilder();
269
            $nameField    = $this->definition->getManyNameField($manyField);
270
            $thisField    = $this->definition->getManyThisField($manyField);
271
            $thatField    = $this->definition->getManyThatField($manyField);
272
            $entity       = $this->definition->getManyEntity($manyField);
273
            $entityTable  = $this->definition->getServiceProvider()->getData($entity)->getDefinition()->getTable();
274
            $nameSelect   = $nameField !== null ? ', t2.`'.$nameField.'` AS name' : '';
275
            $queryBuilder
276
                ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that'.$nameSelect)
277
                ->from('`'.$manyField.'`', 't1')
278
                ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
279
                ->where('t1.`'.$thisField.'` IN (?)')
280
                ->where('t2.deleted_at IS NULL');
281
            $queryBuilder->setParameter(0, array_keys($mapping), Connection::PARAM_INT_ARRAY);
282
            $queryResult    = $queryBuilder->execute();
283
            $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
284
            foreach ($manyReferences as $manyReference) {
285
                $many = ['id' => $manyReference['that']];
286
                if ($nameField !== null) {
287
                    $many['name'] = $manyReference['name'];
288
                }
289
                $mapping[$manyReference['this']][$manyField][] = $many;
290
            }
291
        }
292
        return array_values($mapping);
293
    }
294
295
    /**
296
     * Constructor.
297
     *
298
     * @param EntityDefinition $definition
299
     * the entity definition
300
     * @param FileProcessorInterface $fileProcessor
301
     * the file processor to use
302
     * @param $database
303
     * the Doctrine DBAL instance to use
304
     * @param boolean $useUUIDs
305
     * flag whether to use UUIDs as primary key
306
     */
307
    public function __construct(EntityDefinition $definition, FileProcessorInterface $fileProcessor, $database, $useUUIDs) {
308
        $this->definition    = $definition;
309
        $this->fileProcessor = $fileProcessor;
310
        $this->database      = $database;
311
        $this->useUUIDs      = $useUUIDs;
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317
    public function get($id) {
318
        $entities = $this->listEntries(['id' => $id]);
319
        if (count($entities) == 0) {
320
            return null;
321
        }
322
        return $entities[0];
323
    }
324
325
    /**
326
     * {@inheritdoc}
327
     */
328
    public function listEntries(array $filter = [], array $filterOperators = [], $skip = null, $amount = null, $sortField = null, $sortAscending = null) {
329
        $fieldNames = $this->definition->getFieldNames();
330
331
        $queryBuilder = $this->database->createQueryBuilder();
332
        $table        = $this->definition->getTable();
333
        $queryBuilder
334
            ->select('`'.implode('`,`', $fieldNames).'`')
335
            ->from('`'.$table.'`', '`'.$table.'`')
336
            ->where('deleted_at IS NULL');
337
338
        $this->addFilter($queryBuilder, $filter, $filterOperators);
339
        $this->addPagination($queryBuilder, $skip, $amount);
340
        $this->addSort($queryBuilder, $sortField, $sortAscending);
341
342
        $queryResult = $queryBuilder->execute();
343
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
344
        $rows        = $this->enrichWithMany($rows);
345
        $entities    = [];
346
        foreach ($rows as $row) {
347
            $entities[] = $this->hydrate($row);
348
        }
349
        return $entities;
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     */
355
    public function create(Entity $entity) {
356
357
        $result = $this->shouldExecuteEvents($entity, 'before', 'create');
358
        if (!$result) {
359
            return false;
360
        }
361
362
        $queryBuilder = $this->database->createQueryBuilder();
363
        $queryBuilder
364
            ->insert('`'.$this->definition->getTable().'`')
365
            ->setValue('created_at', 'UTC_TIMESTAMP()')
366
            ->setValue('updated_at', 'UTC_TIMESTAMP()')
367
            ->setValue('version', 0);
368
369
370
        $this->setValuesAndParameters($entity, $queryBuilder, 'setValue');
371
372
        $id = $this->generateUUID();
373
        if ($this->useUUIDs) {
374
            $queryBuilder->setValue('`id`', '?');
375
            $uuidI = count($this->definition->getEditableFieldNames());
376
            $queryBuilder->setParameter($uuidI, $id);
377
        }
378
379
        $queryBuilder->execute();
380
381
        if (!$this->useUUIDs) {
382
            $id = $this->database->lastInsertId();
383
        }
384
385
        $entity->set('id', $id);
386
387
        $createdEntity = $this->get($entity->get('id'));
388
        $entity->set('version', $createdEntity->get('version'));
389
        $entity->set('created_at', $createdEntity->get('created_at'));
390
        $entity->set('updated_at', $createdEntity->get('updated_at'));
391
392
        $this->shouldExecuteEvents($entity, 'after', 'create');
393
394
        return true;
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400
    public function update(Entity $entity) {
401
402
        $result = $this->shouldExecuteEvents($entity, 'before', 'update');
403
        if (!$result) {
404
            return false;
405
        }
406
407
        $formFields   = $this->definition->getEditableFieldNames();
408
        $queryBuilder = $this->database->createQueryBuilder();
409
        $queryBuilder
410
            ->update('`'.$this->definition->getTable().'`')
411
            ->set('updated_at', 'UTC_TIMESTAMP()')
412
            ->set('version', 'version + 1')
413
            ->where('id = ?')
414
            ->setParameter(count($formFields), $entity->get('id'));
415
416
        $this->setValuesAndParameters($entity, $queryBuilder, 'set');
417
        $affected = $queryBuilder->execute();
418
419
        $this->shouldExecuteEvents($entity, 'after', 'update');
420
421
        return $affected;
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427
    public function getReferences($referenceEntity, $nameField) {
428
429
        $table = $this->definition->getServiceProvider()->getData($referenceEntity)->getDefinition()->getTable();
430
431
        $queryBuilder = $this->database->createQueryBuilder();
432
        if ($nameField) {
433
            $queryBuilder->select('id', $nameField);
434
        } else {
435
            $queryBuilder->select('id');
436
        }
437
        $queryBuilder->from('`'.$table.'`', '`'.$table.'`')->where('deleted_at IS NULL');
438
        if ($nameField) {
439
            $queryBuilder->orderBy($nameField);
440
        } else {
441
            $queryBuilder->orderBy('id');
442
        }
443
        $queryResult = $queryBuilder->execute();
444
        $entries     = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
445
        $result      = [];
446
        foreach ($entries as $entry) {
447
            $result[$entry['id']] = $nameField ? $entry[$nameField] : $entry['id'];
448
        }
449
        return $result;
450
    }
451
452
    public function getMany($manyEntity, $nameField) {
453
        $queryBuilder = $this->database->createQueryBuilder();
454
        $entityTable  = $this->definition->getServiceProvider()->getData($manyEntity)->getDefinition()->getTable();
455
        $nameSelect   = $nameField !== null ? ',`'.$nameField.'`' : '';
456
        $queryBuilder
457
            ->select('id'.$nameSelect)
458
            ->from('`'.$entityTable.'`', 't1')
459
            ->where('deleted_at IS NULL');
460
        if ($nameField) {
461
            $queryBuilder->orderBy($nameField);
462
        } else {
463
            $queryBuilder->orderBy('id');
464
        }
465
        $queryResult    = $queryBuilder->execute();
466
        $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
467
        $result         = [];
468
        foreach ($manyReferences as $manyReference) {
469
            $result[$manyReference['id']] = $nameField ? $manyReference[$nameField] : $manyReference['id'];
470
        }
471
        return $result;
472
    }
473
474
    /**
475
     * {@inheritdoc}
476
     */
477
    public function countBy($table, array $params, array $paramsOperators, $excludeDeleted) {
478
        $queryBuilder = $this->database->createQueryBuilder();
479
        $queryBuilder
480
            ->select('COUNT(id)')
481
            ->from('`'.$table.'`', '`'.$table.'`');
482
483
        $deletedExcluder = 'where';
484
        $i               = 0;
485
        foreach ($params as $name => $value) {
486
            $queryBuilder
487
                ->andWhere('`'.$name.'`'.$paramsOperators[$name].'?')
488
                ->setParameter($i, $value);
489
            $i++;
490
            $deletedExcluder = 'andWhere';
491
        }
492
493
        if ($excludeDeleted) {
494
            $queryBuilder->$deletedExcluder('deleted_at IS NULL');
495
        }
496
497
        $queryResult = $queryBuilder->execute();
498
        $result      = $queryResult->fetch(\PDO::FETCH_NUM);
499
        return intval($result[0]);
500
    }
501
502
    /**
503
     * {@inheritdoc}
504
     */
505
    public function fetchReferences(array &$entities = null) {
506
        if (!$entities) {
507
            return;
508
        }
509
        foreach ($this->definition->getFieldNames() as $field) {
510
            if ($this->definition->getType($field) !== 'reference') {
511
                continue;
512
            }
513
            $this->fetchReferencesForField($entities, $field);
514
        }
515
    }
516
517
}
518