Completed
Push — master ( a7a479...f8e2d4 )
by Philip
02:33
created

MySQLData::create()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 43
rs 8.5806
cc 4
eloc 27
nc 5
nop 1
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
    protected function getManyFields() {
34
        $fields = $this->definition->getFieldNames(true);
35
        return array_filter($fields, function($field) {
36
            return $this->definition->getType($field) === 'many';
37
        });
38
    }
39
40
    protected function getFormFields() {
41
        $manyFields = $this->getManyFields();
42
        $formFields = [];
43
        foreach ($this->definition->getEditableFieldNames() as $field) {
44
            if (!in_array($field, $manyFields)) {
45
                $formFields[] = $field;
46
            }
47
        }
48
        return $formFields;
49
    }
50
51
    /**
52
     * Sets the values and parameters of the upcoming given query according
53
     * to the entity.
54
     *
55
     * @param Entity $entity
56
     * the entity with its fields and values
57
     * @param QueryBuilder $queryBuilder
58
     * the upcoming query
59
     * @param string $setMethod
60
     * what method to use on the QueryBuilder: 'setValue' or 'set'
61
     */
62
    protected function setValuesAndParameters(Entity $entity, QueryBuilder $queryBuilder, $setMethod) {
63
        $formFields = $this->getFormFields();
64
        $count      = count($formFields);
65
        for ($i = 0; $i < $count; ++$i) {
66
            $type  = $this->definition->getType($formFields[$i]);
67
            $value = $entity->get($formFields[$i]);
68
            if ($type == 'boolean') {
69
                $value = $value ? 1 : 0;
70
            }
71
            $queryBuilder->$setMethod('`'.$formFields[$i].'`', '?');
72
            $queryBuilder->setParameter($i, $value);
73
        }
74
    }
75
76
    /**
77
     * Performs the cascading children deletion.
78
     *
79
     * @param integer $id
80
     * the current entities id
81
     * @param boolean $deleteCascade
82
     * whether to delete children and sub children
83
     */
84
    protected function deleteChildren($id, $deleteCascade) {
85
        foreach ($this->definition->getChildren() as $childArray) {
86
            $childData = $this->definition->getServiceProvider()->getData($childArray[2]);
87
            $children  = $childData->listEntries([$childArray[1] => $id]);
88
            foreach ($children as $child) {
89
                $childData->doDelete($child, $deleteCascade);
90
            }
91
        }
92
    }
93
94
    /**
95
     * Checks whether the by id given entity still has children referencing it.
96
     *
97
     * @param integer $id
98
     * the current entities id
99
     *
100
     * @return boolean
101
     * true if the entity still has children
102
     */
103
    protected function hasChildren($id) {
104
        foreach ($this->definition->getChildren() as $child) {
105
            $queryBuilder = $this->database->createQueryBuilder();
106
            $queryBuilder
107
                ->select('COUNT(id)')
108
                ->from('`'.$child[0].'`', '`'.$child[0].'`')
109
                ->where('`'.$child[1].'` = ?')
110
                ->andWhere('deleted_at IS NULL')
111
                ->setParameter(0, $id);
112
            $queryResult = $queryBuilder->execute();
113
            $result      = $queryResult->fetch(\PDO::FETCH_NUM);
114
            if ($result[0] > 0) {
115
                return true;
116
            }
117
        }
118
        return false;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    protected function doDelete(Entity $entity, $deleteCascade) {
125
        $result = $this->shouldExecuteEvents($entity, 'before', 'delete');
126
        if (!$result) {
127
            return static::DELETION_FAILED_EVENT;
128
        }
129
        $id = $entity->get('id');
130
        if ($deleteCascade) {
131
            $this->deleteChildren($id, $deleteCascade);
132
        } elseif ($this->hasChildren($id)) {
133
            return static::DELETION_FAILED_STILL_REFERENCED;
134
        }
135
136
        $query = $this->database->createQueryBuilder();
137
        $query
138
            ->update('`'.$this->definition->getTable().'`')
139
            ->set('deleted_at', 'UTC_TIMESTAMP()')
140
            ->where('id = ?')
141
            ->setParameter(0, $id);
142
143
        $query->execute();
144
        $this->shouldExecuteEvents($entity, 'after', 'delete');
145
        return static::DELETION_SUCCESS;
146
    }
147
148
    /**
149
     * Adds sorting parameters to the query.
150
     *
151
     * @param QueryBuilder $queryBuilder
152
     * the query
153
     * @param $filter
154
     * the filter all resulting entities must fulfill, the keys as field names
155
     * @param $filterOperators
156
     * the operators of the filter like "=" defining the full condition of the field
157
     */
158
    protected function addFilter(QueryBuilder $queryBuilder, array $filter, array $filterOperators) {
159
        $i = 0;
160
        foreach ($filter as $field => $value) {
161
            if ($value === null) {
162
                $queryBuilder->andWhere('`'.$field.'` IS NULL');
163
            } else {
164
                $operator = array_key_exists($field, $filterOperators) ? $filterOperators[$field] : '=';
165
                $queryBuilder
166
                    ->andWhere('`'.$field.'` '.$operator.' ?')
167
                    ->setParameter($i, $value);
168
            }
169
            $i++;
170
        }
171
    }
172
173
    /**
174
     * Adds pagination parameters to the query.
175
     *
176
     * @param QueryBuilder $queryBuilder
177
     * the query
178
     * @param integer|null $skip
179
     * the rows to skip
180
     * @param integer|null $amount
181
     * the maximum amount of rows
182
     */
183
    protected function addPagination(QueryBuilder $queryBuilder, $skip, $amount) {
184
        $queryBuilder->setMaxResults(9999999999);
185
        if ($amount !== null) {
186
            $queryBuilder->setMaxResults(abs(intval($amount)));
187
        }
188
        if ($skip !== null) {
189
            $queryBuilder->setFirstResult(abs(intval($skip)));
190
        }
191
    }
192
193
    /**
194
     * Adds sorting parameters to the query.
195
     *
196
     * @param QueryBuilder $queryBuilder
197
     * the query
198
     * @param string|null $sortField
199
     * the sort field
200
     * @param boolean|null $sortAscending
201
     * true if sort ascending, false if descending
202
     */
203
    protected function addSort(QueryBuilder $queryBuilder, $sortField, $sortAscending) {
204
        if ($sortField !== null) {
205
206
            $type = $this->definition->getType($sortField);
207
            if ($type === 'many') {
208
                $sortField = $this->definition->getInitialSortField();
209
            }
210
211
            $order = $sortAscending === true ? 'ASC' : 'DESC';
212
            $queryBuilder->orderBy('`'.$sortField.'`', $order);
213
        }
214
    }
215
216
    /**
217
     * Adds the id and name of referenced entities to the given entities. The
218
     * reference field is before the raw id of the referenced entity and after
219
     * the fetch, it's an array with the keys id and name.
220
     *
221
     * @param Entity[] &$entities
222
     * the entities to fetch the references for
223
     * @param string $field
224
     * the reference field
225
     */
226
    protected function fetchReferencesForField(array &$entities, $field) {
227
        $nameField    = $this->definition->getReferenceNameField($field);
228
        $queryBuilder = $this->database->createQueryBuilder();
229
230
        $ids = array_map(function(Entity $entity) use ($field) {
231
            return $entity->get($field);
232
        }, $entities);
233
234
        $referenceEntity = $this->definition->getReferenceEntity($field);
235
        $table           = $this->definition->getServiceProvider()->getData($referenceEntity)->getDefinition()->getTable();
236
        $queryBuilder
237
            ->from('`'.$table.'`', '`'.$table.'`')
238
            ->where('id IN (?)')
239
            ->andWhere('deleted_at IS NULL');
240
        if ($nameField) {
241
            $queryBuilder->select('id', $nameField);
242
        } else {
243
            $queryBuilder->select('id');
244
        }
245
246
        $queryBuilder->setParameter(0, $ids, Connection::PARAM_INT_ARRAY);
247
248
        $queryResult = $queryBuilder->execute();
249
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
250
        $amount      = count($entities);
251
        foreach ($rows as $row) {
252
            for ($i = 0; $i < $amount; ++$i) {
253
                if ($entities[$i]->get($field) == $row['id']) {
254
                    $value = ['id' => $entities[$i]->get($field)];
255
                    if ($nameField) {
256
                        $value['name'] = $row[$nameField];
257
                    }
258
                    $entities[$i]->set($field, $value);
259
                }
260
            }
261
        }
262
    }
263
264
    /**
265
     * Generates a new UUID.
266
     *
267
     * @return string|null
268
     * the new UUID or null if this instance isn't configured to do so
269
     */
270
    protected function generateUUID() {
271
        $uuid = null;
272
        if ($this->useUUIDs) {
273
            $sql    = 'SELECT UUID() as id';
274
            $result = $this->database->fetchAssoc($sql);
275
            $uuid   = $result['id'];
276
        }
277
        return $uuid;
278
    }
279
280
    protected function enrichWithMany(array $rows) {
281
        $manyFields = $this->getManyFields();
282
        $mapping = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
283
        foreach ($rows as $row) {
284
            foreach ($manyFields as $manyField) {
285
                $row[$manyField] = [];
286
            }
287
            $mapping[$row['id']] = $row;
288
        }
289
        foreach ($manyFields as $manyField) {
290
            $queryBuilder = $this->database->createQueryBuilder();
291
            $nameField    = $this->definition->getManyNameField($manyField);
292
            $thisField    = $this->definition->getManyThisField($manyField);
293
            $thatField    = $this->definition->getManyThatField($manyField);
294
            $entity       = $this->definition->getManyEntity($manyField);
295
            $entityTable  = $this->definition->getServiceProvider()->getData($entity)->getDefinition()->getTable();
296
            $nameSelect   = $nameField !== null ? ', t2.`'.$nameField.'` AS name' : '';
297
            $queryBuilder
298
                ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that'.$nameSelect)
299
                ->from('`'.$manyField.'`', 't1')
300
                ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
301
                ->where('t1.`'.$thisField.'` IN (?)')
302
                ->andWhere('t2.deleted_at IS NULL');
303
            $queryBuilder->setParameter(0, array_keys($mapping), Connection::PARAM_INT_ARRAY);
304
            $queryResult    = $queryBuilder->execute();
305
            $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
306
            foreach ($manyReferences as $manyReference) {
307
                $many = ['id' => $manyReference['that']];
308
                if ($nameField !== null) {
309
                    $many['name'] = $manyReference['name'];
310
                }
311
                $mapping[$manyReference['this']][$manyField][] = $many;
312
            }
313
        }
314
        return array_values($mapping);
315
    }
316
317
    protected function populateMany(Entity $entity) {
318
        $manyFields = $this->getManyFields();
319
        $id = $entity->get('id');
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 9 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
320
        foreach ($manyFields as $manyField) {
321
            $thisField = $this->definition->getManyThisField($manyField);
322
            $thatField = $this->definition->getManyThatField($manyField);
323
            $this->database->delete($manyField, [$thisField => $id]);
324
            foreach ($entity->get($manyField) as $thatId) {
325
                $this->database->insert($manyField, [
326
                    $thisField => $id,
327
                    $thatField => $thatId['id']
328
                ]);
329
            }
330
        }
331
    }
332
333
    /**
334
     * Constructor.
335
     *
336
     * @param EntityDefinition $definition
337
     * the entity definition
338
     * @param FileProcessorInterface $fileProcessor
339
     * the file processor to use
340
     * @param $database
341
     * the Doctrine DBAL instance to use
342
     * @param boolean $useUUIDs
343
     * flag whether to use UUIDs as primary key
344
     */
345
    public function __construct(EntityDefinition $definition, FileProcessorInterface $fileProcessor, $database, $useUUIDs) {
346
        $this->definition    = $definition;
347
        $this->fileProcessor = $fileProcessor;
348
        $this->database      = $database;
349
        $this->useUUIDs      = $useUUIDs;
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     */
355
    public function get($id) {
356
        $entities = $this->listEntries(['id' => $id]);
357
        if (count($entities) == 0) {
358
            return null;
359
        }
360
        return $entities[0];
361
    }
362
363
    /**
364
     * {@inheritdoc}
365
     */
366
    public function listEntries(array $filter = [], array $filterOperators = [], $skip = null, $amount = null, $sortField = null, $sortAscending = null) {
367
        $fieldNames = $this->definition->getFieldNames();
368
369
        $queryBuilder = $this->database->createQueryBuilder();
370
        $table        = $this->definition->getTable();
371
        $queryBuilder
372
            ->select('`'.implode('`,`', $fieldNames).'`')
373
            ->from('`'.$table.'`', '`'.$table.'`')
374
            ->where('deleted_at IS NULL');
375
376
        $this->addFilter($queryBuilder, $filter, $filterOperators);
377
        $this->addPagination($queryBuilder, $skip, $amount);
378
        $this->addSort($queryBuilder, $sortField, $sortAscending);
379
380
        $queryResult = $queryBuilder->execute();
381
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
382
        $rows        = $this->enrichWithMany($rows);
383
        $entities    = [];
384
        foreach ($rows as $row) {
385
            $entities[] = $this->hydrate($row);
386
        }
387
        return $entities;
388
    }
389
390
    /**
391
     * {@inheritdoc}
392
     */
393
    public function create(Entity $entity) {
394
395
        $result = $this->shouldExecuteEvents($entity, 'before', 'create');
396
        if (!$result) {
397
            return false;
398
        }
399
400
        $queryBuilder = $this->database->createQueryBuilder();
401
        $queryBuilder
402
            ->insert('`'.$this->definition->getTable().'`')
403
            ->setValue('created_at', 'UTC_TIMESTAMP()')
404
            ->setValue('updated_at', 'UTC_TIMESTAMP()')
405
            ->setValue('version', 0);
406
407
408
        $this->setValuesAndParameters($entity, $queryBuilder, 'setValue');
409
410
        $id = $this->generateUUID();
411
        if ($this->useUUIDs) {
412
            $queryBuilder->setValue('`id`', '?');
413
            $uuidI = count($this->getFormFields());
414
            $queryBuilder->setParameter($uuidI, $id);
415
        }
416
417
        $queryBuilder->execute();
418
419
        if (!$this->useUUIDs) {
420
            $id = $this->database->lastInsertId();
421
        }
422
423
        $entity->set('id', $id);
424
425
        $createdEntity = $this->get($entity->get('id'));
426
        $entity->set('version', $createdEntity->get('version'));
427
        $entity->set('created_at', $createdEntity->get('created_at'));
428
        $entity->set('updated_at', $createdEntity->get('updated_at'));
429
430
        $this->populateMany($entity);
431
432
        $this->shouldExecuteEvents($entity, 'after', 'create');
433
434
        return true;
435
    }
436
437
    /**
438
     * {@inheritdoc}
439
     */
440
    public function update(Entity $entity) {
441
442
        $result = $this->shouldExecuteEvents($entity, 'before', 'update');
443
        if (!$result) {
444
            return false;
445
        }
446
447
        $formFields   = $this->getFormFields();
448
        $queryBuilder = $this->database->createQueryBuilder();
449
        $queryBuilder
450
            ->update('`'.$this->definition->getTable().'`')
451
            ->set('updated_at', 'UTC_TIMESTAMP()')
452
            ->set('version', 'version + 1')
453
            ->where('id = ?')
454
            ->setParameter(count($formFields), $entity->get('id'));
455
456
        $this->setValuesAndParameters($entity, $queryBuilder, 'set');
457
        $affected = $queryBuilder->execute();
458
459
        $this->populateMany($entity);
460
461
        $this->shouldExecuteEvents($entity, 'after', 'update');
462
463
        return $affected;
464
    }
465
466
    /**
467
     * {@inheritdoc}
468
     */
469
    public function getIdToNameMap($entity, $nameField) {
470
        $table = $this->definition->getServiceProvider()->getData($entity)->getDefinition()->getTable();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
471
        $queryBuilder = $this->database->createQueryBuilder();
472
        $nameSelect   = $nameField !== null ? ',`'.$nameField.'`' : '';
473
        $queryBuilder
474
            ->select('id'.$nameSelect)
475
            ->from('`'.$table.'`', 't1')
476
            ->where('deleted_at IS NULL');
477
        if ($nameField) {
478
            $queryBuilder->orderBy($nameField);
479
        } else {
480
            $queryBuilder->orderBy('id');
481
        }
482
        $queryResult    = $queryBuilder->execute();
483
        $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
484
        $result         = [];
485
        foreach ($manyReferences as $manyReference) {
486
            $result[$manyReference['id']] = $nameField ? $manyReference[$nameField] : $manyReference['id'];
487
        }
488
        return $result;
489
    }
490
491
    /**
492
     * {@inheritdoc}
493
     */
494
    public function countBy($table, array $params, array $paramsOperators, $excludeDeleted) {
495
        $queryBuilder = $this->database->createQueryBuilder();
496
        $queryBuilder
497
            ->select('COUNT(id)')
498
            ->from('`'.$table.'`', '`'.$table.'`');
499
500
        $deletedExcluder = 'where';
501
        $i               = 0;
502
        foreach ($params as $name => $value) {
503
            $queryBuilder
504
                ->andWhere('`'.$name.'`'.$paramsOperators[$name].'?')
505
                ->setParameter($i, $value);
506
            $i++;
507
            $deletedExcluder = 'andWhere';
508
        }
509
510
        if ($excludeDeleted) {
511
            $queryBuilder->$deletedExcluder('deleted_at IS NULL');
512
        }
513
514
        $queryResult = $queryBuilder->execute();
515
        $result      = $queryResult->fetch(\PDO::FETCH_NUM);
516
        return intval($result[0]);
517
    }
518
519
    /**
520
     * {@inheritdoc}
521
     */
522
    public function fetchReferences(array &$entities = null) {
523
        if (!$entities) {
524
            return;
525
        }
526
        foreach ($this->definition->getFieldNames() as $field) {
527
            if ($this->definition->getType($field) !== 'reference') {
528
                continue;
529
            }
530
            $this->fetchReferencesForField($entities, $field);
531
        }
532
    }
533
534
535
    /**
536
     * {@inheritdoc}
537
     */
538
    public function manySetExists($field, array $thatIds) {
539
        $thisField    = $this->definition->getManyThisField($field);
540
        $thatField    = $this->definition->getManyThatField($field);
541
        $thatEntity   = $this->definition->getManyEntity($field);
542
        $entityTable  = $this->definition->getServiceProvider()->getData($thatEntity)->getDefinition()->getTable();
543
        $queryBuilder = $this->database->createQueryBuilder();
544
        $queryBuilder
545
            ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that')
546
            ->from('`'.$field.'`', 't1')
547
            ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
548
            ->andWhere('t2.deleted_at IS NULL')
549
            ->orderBy('this, that');
550
        $queryResult  = $queryBuilder->execute();
551
        $existingMany = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
552
        $existingMap  = [];
553
        foreach ($existingMany as $existing) {
554
            $existingMap[$existing['this']][] = $existing['that'];
555
        }
556
        sort($thatIds);
557
        return in_array($thatIds, array_values($existingMap));
558
    }
559
560
}
561