Completed
Push — master ( e1db9f...afd7f1 )
by Philip
02:57
created

MySQLData::addSort()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 3
eloc 4
nc 3
nop 3
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();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 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...
42
        $simpleFields = [];
43
        foreach ($this->definition->getEditableFieldNames() as $field) {
44
            if (!in_array($field, $manyFields)) {
45
                $manyFields[] = $field;
46
            }
47
        }
48
        return $simpleFields;
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
            $order = $sortAscending === true ? 'ASC' : 'DESC';
206
            $queryBuilder->orderBy('`'.$sortField.'`', $order);
207
        }
208
    }
209
210
    /**
211
     * Adds the id and name of referenced entities to the given entities. The
212
     * reference field is before the raw id of the referenced entity and after
213
     * the fetch, it's an array with the keys id and name.
214
     *
215
     * @param Entity[] &$entities
216
     * the entities to fetch the references for
217
     * @param string $field
218
     * the reference field
219
     */
220
    protected function fetchReferencesForField(array &$entities, $field) {
221
        $nameField    = $this->definition->getReferenceNameField($field);
222
        $queryBuilder = $this->database->createQueryBuilder();
223
224
        $ids = array_map(function(Entity $entity) use ($field) {
225
            return $entity->get($field);
226
        }, $entities);
227
228
        $referenceEntity = $this->definition->getReferenceEntity($field);
229
        $table           = $this->definition->getServiceProvider()->getData($referenceEntity)->getDefinition()->getTable();
230
        $queryBuilder
231
            ->from('`'.$table.'`', '`'.$table.'`')
232
            ->where('id IN (?)')
233
            ->andWhere('deleted_at IS NULL');
234
        if ($nameField) {
235
            $queryBuilder->select('id', $nameField);
236
        } else {
237
            $queryBuilder->select('id');
238
        }
239
240
        $queryBuilder->setParameter(0, $ids, Connection::PARAM_INT_ARRAY);
241
242
        $queryResult = $queryBuilder->execute();
243
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
244
        $amount      = count($entities);
245
        foreach ($rows as $row) {
246
            for ($i = 0; $i < $amount; ++$i) {
247
                if ($entities[$i]->get($field) == $row['id']) {
248
                    $value = ['id' => $entities[$i]->get($field)];
249
                    if ($nameField) {
250
                        $value['name'] = $row[$nameField];
251
                    }
252
                    $entities[$i]->set($field, $value);
253
                }
254
            }
255
        }
256
    }
257
258
    /**
259
     * Generates a new UUID.
260
     *
261
     * @return string|null
262
     * the new UUID or null if this instance isn't configured to do so
263
     */
264
    protected function generateUUID() {
265
        $uuid = null;
266
        if ($this->useUUIDs) {
267
            $sql    = 'SELECT UUID() as id';
268
            $result = $this->database->fetchAssoc($sql);
269
            $uuid   = $result['id'];
270
        }
271
        return $uuid;
272
    }
273
274
    protected function enrichWithMany(array $rows) {
275
        $manyFields = $this->getManyFields();
276
        $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...
277
        foreach ($rows as $row) {
278
            foreach ($manyFields as $manyField) {
279
                $row[$manyField] = [];
280
            }
281
            $mapping[$row['id']] = $row;
282
        }
283
        foreach ($manyFields as $manyField) {
284
            $queryBuilder = $this->database->createQueryBuilder();
285
            $nameField    = $this->definition->getManyNameField($manyField);
286
            $thisField    = $this->definition->getManyThisField($manyField);
287
            $thatField    = $this->definition->getManyThatField($manyField);
288
            $entity       = $this->definition->getManyEntity($manyField);
289
            $entityTable  = $this->definition->getServiceProvider()->getData($entity)->getDefinition()->getTable();
290
            $nameSelect   = $nameField !== null ? ', t2.`'.$nameField.'` AS name' : '';
291
            $queryBuilder
292
                ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that'.$nameSelect)
293
                ->from('`'.$manyField.'`', 't1')
294
                ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
295
                ->where('t1.`'.$thisField.'` IN (?)')
296
                ->where('t2.deleted_at IS NULL');
297
            $queryBuilder->setParameter(0, array_keys($mapping), Connection::PARAM_INT_ARRAY);
298
            $queryResult    = $queryBuilder->execute();
299
            $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
300
            foreach ($manyReferences as $manyReference) {
301
                $many = ['id' => $manyReference['that']];
302
                if ($nameField !== null) {
303
                    $many['name'] = $manyReference['name'];
304
                }
305
                $mapping[$manyReference['this']][$manyField][] = $many;
306
            }
307
        }
308
        return array_values($mapping);
309
    }
310
311
    protected function populateMany(Entity $entity) {
312
        $manyFields = $this->getManyFields();
313
        $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...
314
        foreach ($manyFields as $manyField) {
315
            $thisField = $this->definition->getManyThisField($manyField);
316
            $thatField = $this->definition->getManyThatField($manyField);
317
            $this->database->delete($manyField, [$thisField => $id]);
318
            foreach ($entity->get($manyField) as $thatId) {
319
                $this->database->insert($manyField, [
320
                    $thisField => $id,
321
                    $thatField => $thatId['id']
322
                ]);
323
            }
324
        }
325
    }
326
327
    /**
328
     * Constructor.
329
     *
330
     * @param EntityDefinition $definition
331
     * the entity definition
332
     * @param FileProcessorInterface $fileProcessor
333
     * the file processor to use
334
     * @param $database
335
     * the Doctrine DBAL instance to use
336
     * @param boolean $useUUIDs
337
     * flag whether to use UUIDs as primary key
338
     */
339
    public function __construct(EntityDefinition $definition, FileProcessorInterface $fileProcessor, $database, $useUUIDs) {
340
        $this->definition    = $definition;
341
        $this->fileProcessor = $fileProcessor;
342
        $this->database      = $database;
343
        $this->useUUIDs      = $useUUIDs;
344
    }
345
346
    /**
347
     * {@inheritdoc}
348
     */
349
    public function get($id) {
350
        $entities = $this->listEntries(['id' => $id]);
351
        if (count($entities) == 0) {
352
            return null;
353
        }
354
        return $entities[0];
355
    }
356
357
    /**
358
     * {@inheritdoc}
359
     */
360
    public function listEntries(array $filter = [], array $filterOperators = [], $skip = null, $amount = null, $sortField = null, $sortAscending = null) {
361
        $fieldNames = $this->definition->getFieldNames();
362
363
        $queryBuilder = $this->database->createQueryBuilder();
364
        $table        = $this->definition->getTable();
365
        $queryBuilder
366
            ->select('`'.implode('`,`', $fieldNames).'`')
367
            ->from('`'.$table.'`', '`'.$table.'`')
368
            ->where('deleted_at IS NULL');
369
370
        $this->addFilter($queryBuilder, $filter, $filterOperators);
371
        $this->addPagination($queryBuilder, $skip, $amount);
372
        $this->addSort($queryBuilder, $sortField, $sortAscending);
373
374
        $queryResult = $queryBuilder->execute();
375
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
376
        $rows        = $this->enrichWithMany($rows);
377
        $entities    = [];
378
        foreach ($rows as $row) {
379
            $entities[] = $this->hydrate($row);
380
        }
381
        return $entities;
382
    }
383
384
    /**
385
     * {@inheritdoc}
386
     */
387
    public function create(Entity $entity) {
388
389
        $result = $this->shouldExecuteEvents($entity, 'before', 'create');
390
        if (!$result) {
391
            return false;
392
        }
393
394
        $queryBuilder = $this->database->createQueryBuilder();
395
        $queryBuilder
396
            ->insert('`'.$this->definition->getTable().'`')
397
            ->setValue('created_at', 'UTC_TIMESTAMP()')
398
            ->setValue('updated_at', 'UTC_TIMESTAMP()')
399
            ->setValue('version', 0);
400
401
402
        $this->setValuesAndParameters($entity, $queryBuilder, 'setValue');
403
404
        $id = $this->generateUUID();
405
        if ($this->useUUIDs) {
406
            $queryBuilder->setValue('`id`', '?');
407
            $uuidI = count($this->definition->getFormFields());
0 ignored issues
show
Bug introduced by
The method getFormFields() does not seem to exist on object<CRUDlex\EntityDefinition>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
408
            $queryBuilder->setParameter($uuidI, $id);
409
        }
410
411
        $queryBuilder->execute();
412
413
        if (!$this->useUUIDs) {
414
            $id = $this->database->lastInsertId();
415
        }
416
417
        $entity->set('id', $id);
418
419
        $createdEntity = $this->get($entity->get('id'));
420
        $entity->set('version', $createdEntity->get('version'));
421
        $entity->set('created_at', $createdEntity->get('created_at'));
422
        $entity->set('updated_at', $createdEntity->get('updated_at'));
423
424
        $this->populateMany($entity);
425
426
        $this->shouldExecuteEvents($entity, 'after', 'create');
427
428
        return true;
429
    }
430
431
    /**
432
     * {@inheritdoc}
433
     */
434
    public function update(Entity $entity) {
435
436
        $result = $this->shouldExecuteEvents($entity, 'before', 'update');
437
        if (!$result) {
438
            return false;
439
        }
440
441
        $formFields   = $this->getFormFields();
442
        $queryBuilder = $this->database->createQueryBuilder();
443
        $queryBuilder
444
            ->update('`'.$this->definition->getTable().'`')
445
            ->set('updated_at', 'UTC_TIMESTAMP()')
446
            ->set('version', 'version + 1')
447
            ->where('id = ?')
448
            ->setParameter(count($formFields), $entity->get('id'));
449
450
        $this->setValuesAndParameters($entity, $queryBuilder, 'set');
451
        $affected = $queryBuilder->execute();
452
453
        $this->populateMany($entity);
454
455
        $this->shouldExecuteEvents($entity, 'after', 'update');
456
457
        return $affected;
458
    }
459
460
    /**
461
     * {@inheritdoc}
462
     */
463
    public function getIdToNameMap($entity, $nameField) {
464
        $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...
465
        $queryBuilder = $this->database->createQueryBuilder();
466
        $nameSelect   = $nameField !== null ? ',`'.$nameField.'`' : '';
467
        $queryBuilder
468
            ->select('id'.$nameSelect)
469
            ->from('`'.$table.'`', 't1')
470
            ->where('deleted_at IS NULL');
471
        if ($nameField) {
472
            $queryBuilder->orderBy($nameField);
473
        } else {
474
            $queryBuilder->orderBy('id');
475
        }
476
        $queryResult    = $queryBuilder->execute();
477
        $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
478
        $result         = [];
479
        foreach ($manyReferences as $manyReference) {
480
            $result[$manyReference['id']] = $nameField ? $manyReference[$nameField] : $manyReference['id'];
481
        }
482
        return $result;
483
    }
484
485
    /**
486
     * {@inheritdoc}
487
     */
488
    public function countBy($table, array $params, array $paramsOperators, $excludeDeleted) {
489
        $queryBuilder = $this->database->createQueryBuilder();
490
        $queryBuilder
491
            ->select('COUNT(id)')
492
            ->from('`'.$table.'`', '`'.$table.'`');
493
494
        $deletedExcluder = 'where';
495
        $i               = 0;
496
        foreach ($params as $name => $value) {
497
            $queryBuilder
498
                ->andWhere('`'.$name.'`'.$paramsOperators[$name].'?')
499
                ->setParameter($i, $value);
500
            $i++;
501
            $deletedExcluder = 'andWhere';
502
        }
503
504
        if ($excludeDeleted) {
505
            $queryBuilder->$deletedExcluder('deleted_at IS NULL');
506
        }
507
508
        $queryResult = $queryBuilder->execute();
509
        $result      = $queryResult->fetch(\PDO::FETCH_NUM);
510
        return intval($result[0]);
511
    }
512
513
    /**
514
     * {@inheritdoc}
515
     */
516
    public function fetchReferences(array &$entities = null) {
517
        if (!$entities) {
518
            return;
519
        }
520
        foreach ($this->definition->getFieldNames() as $field) {
521
            if ($this->definition->getType($field) !== 'reference') {
522
                continue;
523
            }
524
            $this->fetchReferencesForField($entities, $field);
525
        }
526
    }
527
528
}
529