Completed
Push — master ( 317269...6113df )
by Philip
05:51
created

MySQLData::create()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 39
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 25
nc 5
nop 1

2 Methods

Rating   Name   Duplication   Size   Complexity  
A MySQLData::__construct() 0 6 1
A MySQLData::get() 0 7 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\Connection;
15
use Doctrine\DBAL\Query\QueryBuilder;
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->getFormFields();
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
            if ($type == 'reference' && is_array($value)) {
53
                $value = $value['id'];
54
            }
55
            $queryBuilder->$setMethod('`'.$formFields[$i].'`', '?');
56
            $queryBuilder->setParameter($i, $value);
57
        }
58
    }
59
60
    /**
61
     * Checks whether the by id given entity still has children referencing it.
62
     *
63
     * @param integer $id
64
     * the current entities id
65
     *
66
     * @return boolean
67
     * true if the entity still has children
68
     */
69
    protected function hasChildren($id) {
70
        foreach ($this->definition->getChildren() as $child) {
71
            $queryBuilder = $this->database->createQueryBuilder();
72
            $queryBuilder
73
                ->select('COUNT(id)')
74
                ->from('`'.$child[0].'`', '`'.$child[0].'`')
75
                ->where('`'.$child[1].'` = ?')
76
                ->andWhere('deleted_at IS NULL')
77
                ->setParameter(0, $id);
78
            $queryResult = $queryBuilder->execute();
79
            $result      = $queryResult->fetch(\PDO::FETCH_NUM);
80
            if ($result[0] > 0) {
81
                return true;
82
            }
83
        }
84
        return false;
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    protected function doDelete(Entity $entity, $deleteCascade) {
91
        $id = $entity->get('id');
92
        if ($deleteCascade) {
93
            $this->deleteChildren($id, $deleteCascade);
94
        } elseif ($this->hasChildren($id)) {
95
            return static::DELETION_FAILED_STILL_REFERENCED;
96
        }
97
98
        $query = $this->database->createQueryBuilder();
99
        $query
100
            ->update('`'.$this->definition->getTable().'`')
101
            ->set('deleted_at', 'UTC_TIMESTAMP()')
102
            ->where('id = ?')
103
            ->setParameter(0, $id);
104
105
        $query->execute();
106
        return static::DELETION_SUCCESS;
107
    }
108
109
    /**
110
     * Gets all possible many-to-many ids existing for this definition.
111
     *
112
     * @param array $fields
113
     * the many field names to fetch for
114
     * @param $params
115
     * the parameters the possible many field values to fetch for
116
     * @return array
117
     * an array of this many-to-many ids
118
     */
119
    protected function getManyIds(array $fields, array $params) {
120
        $manyIds = [];
121
        foreach ($fields as $field) {
122
            $thisField    = $this->definition->getSubTypeField($field, 'many', 'thisField');
123
            $thatField    = $this->definition->getSubTypeField($field, 'many', 'thatField');
124
            $queryBuilder = $this->database->createQueryBuilder();
125
            $queryBuilder
126
                ->select('`'.$thisField.'`')
127
                ->from($field)
128
                ->where('`'.$thatField.'` IN (?)')
129
                ->setParameter(0, array_column($params[$field], 'id'), Connection::PARAM_STR_ARRAY)
130
                ->groupBy('`'.$thisField.'`')
131
            ;
132
            $queryResult = $queryBuilder->execute();
133
            $manyResults = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
134
            $manyIds     = array_merge($manyIds, array_column($manyResults, $thisField));
135
136
        }
137
        return $manyIds;
138
    }
139
140
    /**
141
     * Adds sorting parameters to the query.
142
     *
143
     * @param QueryBuilder $queryBuilder
144
     * the query
145
     * @param $filter
146
     * the filter all resulting entities must fulfill, the keys as field names
147
     * @param $filterOperators
148
     * the operators of the filter like "=" defining the full condition of the field
149
     */
150
    protected function addFilter(QueryBuilder $queryBuilder, array $filter, array $filterOperators) {
151
        $i          = 0;
152
        $manyFields = [];
153
        foreach ($filter as $field => $value) {
154
            if ($this->definition->getType($field) === 'many') {
155
                $manyFields[] = $field;
156
                continue;
157
            }
158
            if ($value === null) {
159
                $queryBuilder->andWhere('`'.$field.'` IS NULL');
160
            } else {
161
                $operator = array_key_exists($field, $filterOperators) ? $filterOperators[$field] : '=';
162
                $queryBuilder
163
                    ->andWhere('`'.$field.'` '.$operator.' ?')
164
                    ->setParameter($i, $value, \PDO::PARAM_STR);
165
            }
166
            $i++;
167
        }
168
        $idsToInclude = $this->getManyIds($manyFields, $filter);
169
        if (!empty($idsToInclude)) {
170
            $queryBuilder
171
                ->andWhere('id IN (?)')
172
                ->setParameter($i, $idsToInclude, Connection::PARAM_STR_ARRAY)
173
            ;
174
        }
175
    }
176
177
    /**
178
     * Adds pagination parameters to the query.
179
     *
180
     * @param QueryBuilder $queryBuilder
181
     * the query
182
     * @param integer|null $skip
183
     * the rows to skip
184
     * @param integer|null $amount
185
     * the maximum amount of rows
186
     */
187
    protected function addPagination(QueryBuilder $queryBuilder, $skip, $amount) {
188
        $queryBuilder->setMaxResults(9999999999);
189
        if ($amount !== null) {
190
            $queryBuilder->setMaxResults(abs(intval($amount)));
191
        }
192
        if ($skip !== null) {
193
            $queryBuilder->setFirstResult(abs(intval($skip)));
194
        }
195
    }
196
197
    /**
198
     * Adds sorting parameters to the query.
199
     *
200
     * @param QueryBuilder $queryBuilder
201
     * the query
202
     * @param string|null $sortField
203
     * the sort field
204
     * @param boolean|null $sortAscending
205
     * true if sort ascending, false if descending
206
     */
207
    protected function addSort(QueryBuilder $queryBuilder, $sortField, $sortAscending) {
208
        if ($sortField !== null) {
209
210
            $type = $this->definition->getType($sortField);
211
            if ($type === 'many') {
212
                $sortField = $this->definition->getInitialSortField();
213
            }
214
215
            $order = $sortAscending === true ? 'ASC' : 'DESC';
216
            $queryBuilder->orderBy('`'.$sortField.'`', $order);
217
        }
218
    }
219
220
    /**
221
     * Adds the id and name of referenced entities to the given entities. The
222
     * reference field is before the raw id of the referenced entity and after
223
     * the fetch, it's an array with the keys id and name.
224
     *
225
     * @param Entity[] &$entities
226
     * the entities to fetch the references for
227
     * @param string $field
228
     * the reference field
229
     */
230
    protected function fetchReferencesForField(array &$entities, $field) {
231
        $nameField    = $this->definition->getSubTypeField($field, 'reference', 'nameField');
232
        $queryBuilder = $this->database->createQueryBuilder();
233
234
        $ids = $this->getReferenceIds($entities, $field);
235
236
        $referenceEntity = $this->definition->getSubTypeField($field, 'reference', 'entity');
237
        $table           = $this->definition->getServiceProvider()->getData($referenceEntity)->getDefinition()->getTable();
238
        $queryBuilder
239
            ->from('`'.$table.'`', '`'.$table.'`')
240
            ->where('id IN (?)')
241
            ->andWhere('deleted_at IS NULL');
242
        if ($nameField) {
243
            $queryBuilder->select('id', $nameField);
244
        } else {
245
            $queryBuilder->select('id');
246
        }
247
248
        $queryBuilder->setParameter(0, $ids, Connection::PARAM_STR_ARRAY);
249
250
        $queryResult = $queryBuilder->execute();
251
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
252
        $amount      = count($entities);
253
        foreach ($rows as $row) {
254
            for ($i = 0; $i < $amount; ++$i) {
255
                if ($entities[$i]->get($field) == $row['id']) {
256
                    $value = ['id' => $entities[$i]->get($field)];
257
                    if ($nameField) {
258
                        $value['name'] = $row[$nameField];
259
                    }
260
                    $entities[$i]->set($field, $value);
261
                }
262
            }
263
        }
264
    }
265
266
    /**
267
     * Generates a new UUID.
268
     *
269
     * @return string|null
270
     * the new UUID or null if this instance isn't configured to do so
271
     */
272
    protected function generateUUID() {
273
        $uuid = null;
274
        if ($this->useUUIDs) {
275
            $sql    = 'SELECT UUID() as id';
276
            $result = $this->database->fetchAssoc($sql);
277
            $uuid   = $result['id'];
278
        }
279
        return $uuid;
280
    }
281
282
    /**
283
     * Enriches the given mapping of entity id to raw entity data with some many-to-many data.
284
     *
285
     * @param array $idToData
286
     * a reference to the map entity id to raw entity data
287
     * @param $manyField
288
     * the many field to enrich data with
289
     */
290
    protected function enrichWithManyField(&$idToData, $manyField) {
291
        $queryBuilder = $this->database->createQueryBuilder();
292
        $nameField    = $this->definition->getSubTypeField($manyField, 'many', 'nameField');
293
        $thisField    = $this->definition->getSubTypeField($manyField, 'many', 'thisField');
294
        $thatField    = $this->definition->getSubTypeField($manyField, 'many', 'thatField');
295
        $entity       = $this->definition->getSubTypeField($manyField, 'many', 'entity');
296
        $entityTable  = $this->definition->getServiceProvider()->getData($entity)->getDefinition()->getTable();
297
        $nameSelect   = $nameField !== null ? ', t2.`'.$nameField.'` AS name' : '';
298
        $queryBuilder
299
            ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS id'.$nameSelect)
300
            ->from('`'.$manyField.'`', 't1')
301
            ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
302
            ->where('t1.`'.$thisField.'` IN (?)')
303
            ->andWhere('t2.deleted_at IS NULL');
304
        $queryBuilder->setParameter(0, array_keys($idToData), Connection::PARAM_STR_ARRAY);
305
        $queryResult    = $queryBuilder->execute();
306
        $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
307
        foreach ($manyReferences as $manyReference) {
308
            $entityId = $manyReference['this'];
309
            unset($manyReference['this']);
310
            $idToData[$entityId][$manyField][] = $manyReference;
311
        }
312
    }
313
314
    /**
315
     * Fetches to the rows belonging many-to-many entries and adds them to the rows.
316
     *
317
     * @param array $rows
318
     * the rows to enrich
319
     * @return array
320
     * the enriched rows
321
     */
322
    protected function enrichWithMany(array $rows) {
323
        $manyFields = $this->getManyFields();
324
        $idToData   = [];
325
        foreach ($rows as $row) {
326
            foreach ($manyFields as $manyField) {
327
                $row[$manyField] = [];
328
            }
329
            $idToData[$row['id']] = $row;
330
        }
331
        foreach ($manyFields as $manyField) {
332
            $this->enrichWithManyField($idToData, $manyField);
333
        }
334
        return array_values($idToData);
335
    }
336
337
    /**
338
     * First, deletes all to the given entity related many-to-many entries from the DB
339
     * and then writes them again.
340
     *
341
     * @param Entity $entity
342
     * the entity to save the many-to-many entries of
343
     */
344
    protected function saveMany(Entity $entity) {
345
        $manyFields = $this->getManyFields();
346
        $id         = $entity->get('id');
347
        foreach ($manyFields as $manyField) {
348
            $thisField = '`'.$this->definition->getSubTypeField($manyField, 'many', 'thisField').'`';
349
            $thatField = '`'.$this->definition->getSubTypeField($manyField, 'many', 'thatField').'`';
350
            $this->database->delete($manyField, [$thisField => $id]);
351
            $manyValues = $entity->get($manyField) ?: [];
352
            foreach ($manyValues as $thatId) {
353
                $this->database->insert($manyField, [
354
                    $thisField => $id,
355
                    $thatField => $thatId['id']
356
                ]);
357
            }
358
        }
359
    }
360
361
    /**
362
     * Adds the id and name of referenced entities to the given entities. Each
363
     * reference field is before the raw id of the referenced entity and after
364
     * the fetch, it's an array with the keys id and name.
365
     *
366
     * @param Entity[] &$entities
367
     * the entities to fetch the references for
368
     *
369
     * @return void
370
     */
371
    protected function enrichWithReference(array &$entities) {
372
        if (empty($entities)) {
373
            return;
374
        }
375
        foreach ($this->definition->getFieldNames() as $field) {
376
            if ($this->definition->getType($field) !== 'reference') {
377
                continue;
378
            }
379
            $this->fetchReferencesForField($entities, $field);
380
        }
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     */
386
    protected function doCreate(Entity $entity) {
387
388
        $queryBuilder = $this->database->createQueryBuilder();
389
        $queryBuilder
390
            ->insert('`'.$this->definition->getTable().'`')
391
            ->setValue('created_at', 'UTC_TIMESTAMP()')
392
            ->setValue('updated_at', 'UTC_TIMESTAMP()')
393
            ->setValue('version', 0);
394
395
396
        $this->setValuesAndParameters($entity, $queryBuilder, 'setValue');
397
398
        $id = $this->generateUUID();
399
        if ($this->useUUIDs) {
400
            $queryBuilder->setValue('`id`', '?');
401
            $uuidI = count($this->getFormFields());
402
            $queryBuilder->setParameter($uuidI, $id);
403
        }
404
405
        $queryBuilder->execute();
406
407
        if (!$this->useUUIDs) {
408
            $id = $this->database->lastInsertId();
409
        }
410
411
        $this->enrichEntityWithMetaData($id, $entity);
412
        $this->saveMany($entity);
413
        $entities = [$entity];
414
        $this->enrichWithReference($entities);
415
416
        return true;
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422
    protected function doUpdate(Entity $entity) {
423
        $queryBuilder = $this->database->createQueryBuilder();
424
        $queryBuilder->update('`'.$this->definition->getTable().'`')
425
            ->set('updated_at', 'UTC_TIMESTAMP()')
426
            ->set('version', 'version + 1')
427
            ->where('id = ?')
428
            ->setParameter(count($this->getFormFields()), $entity->get('id'));
429
430
        $this->setValuesAndParameters($entity, $queryBuilder, 'set');
431
        $affected = $queryBuilder->execute();
432
433
        $this->saveMany($entity);
434
        $entities = [$entity];
435
        $this->enrichWithReference($entities);
436
        return $affected > 0;
437
    }
438
439
    /**
440
     * Constructor.
441
     *
442
     * @param EntityDefinition $definition
443
     * the entity definition
444
     * @param FileProcessorInterface $fileProcessor
445
     * the file processor to use
446
     * @param $database
447
     * the Doctrine DBAL instance to use
448
     * @param boolean $useUUIDs
449
     * flag whether to use UUIDs as primary key
450
     */
451
    public function __construct(EntityDefinition $definition, FileProcessorInterface $fileProcessor, $database, $useUUIDs) {
452
        $this->definition    = $definition;
453
        $this->fileProcessor = $fileProcessor;
454
        $this->database      = $database;
455
        $this->useUUIDs      = $useUUIDs;
456
    }
457
458
    /**
459
     * {@inheritdoc}
460
     */
461
    public function get($id) {
462
        $entities = $this->listEntries(['id' => $id]);
463
        if (count($entities) == 0) {
464
            return null;
465
        }
466
        return $entities[0];
467
    }
468
469
    /**
470
     * {@inheritdoc}
471
     */
472
    public function listEntries(array $filter = [], array $filterOperators = [], $skip = null, $amount = null, $sortField = null, $sortAscending = null) {
473
        $fieldNames = $this->definition->getFieldNames();
474
475
        $queryBuilder = $this->database->createQueryBuilder();
476
        $table        = $this->definition->getTable();
477
        $queryBuilder
478
            ->select('`'.implode('`,`', $fieldNames).'`')
479
            ->from('`'.$table.'`', '`'.$table.'`')
480
            ->where('deleted_at IS NULL');
481
482
        $this->addFilter($queryBuilder, $filter, $filterOperators);
483
        $this->addPagination($queryBuilder, $skip, $amount);
484
        $this->addSort($queryBuilder, $sortField, $sortAscending);
485
486
        $queryResult = $queryBuilder->execute();
487
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
488
        $rows        = $this->enrichWithMany($rows);
489
        $entities    = [];
490
        foreach ($rows as $row) {
491
            $entities[] = $this->hydrate($row);
492
        }
493
        $this->enrichWithReference($entities);
494
        return $entities;
495
    }
496
497
    /**
498
     * {@inheritdoc}
499
     */
500
    public function getIdToNameMap($entity, $nameField) {
501
        $nameSelect   = $nameField !== null ? ',`'.$nameField.'`' : '';
502
        $drivingField = $nameField ?: 'id';
503
504
        $table        = $this->definition->getServiceProvider()->getData($entity)->getDefinition()->getTable();
505
        $queryBuilder = $this->database->createQueryBuilder();
506
        $queryBuilder
507
            ->select('id'.$nameSelect)
508
            ->from('`'.$table.'`', 't1')
509
            ->where('deleted_at IS NULL')
510
            ->orderBy($drivingField)
511
        ;
512
        $queryResult    = $queryBuilder->execute();
513
        $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
514
        $result         = array_reduce($manyReferences, function(&$carry, $manyReference) use ($drivingField) {
515
            $carry[$manyReference['id']] = $manyReference[$drivingField];
516
            return $carry;
517
        }, []);
518
        return $result;
519
    }
520
521
    /**
522
     * {@inheritdoc}
523
     */
524
    public function countBy($table, array $params, array $paramsOperators, $excludeDeleted) {
525
        $queryBuilder = $this->database->createQueryBuilder();
526
        $queryBuilder
527
            ->select('COUNT(id)')
528
            ->from('`'.$table.'`', '`'.$table.'`')
529
        ;
530
531
        $deletedExcluder = 'where';
532
        $i               = 0;
533
        $manyFields      = [];
534
        foreach ($params as $name => $value) {
535
            if ($this->definition->getType($name) === 'many') {
536
                $manyFields[] = $name;
537
                continue;
538
            }
539
            $queryBuilder
540
                ->andWhere('`'.$name.'` '.$paramsOperators[$name].' ?')
541
                ->setParameter($i, $value, \PDO::PARAM_STR)
542
            ;
543
            $i++;
544
            $deletedExcluder = 'andWhere';
545
        }
546
547
        $idsToInclude = $this->getManyIds($manyFields, $params);
548
        if (!empty($idsToInclude)) {
549
            $queryBuilder
550
                ->andWhere('id IN (?)')
551
                ->setParameter($i, $idsToInclude, Connection::PARAM_STR_ARRAY)
552
            ;
553
            $deletedExcluder = 'andWhere';
554
        }
555
556
        if ($excludeDeleted) {
557
            $queryBuilder->$deletedExcluder('deleted_at IS NULL');
558
        }
559
560
        $queryResult = $queryBuilder->execute();
561
        $result      = $queryResult->fetch(\PDO::FETCH_NUM);
562
        return intval($result[0]);
563
    }
564
565
    /**
566
     * {@inheritdoc}
567
     */
568
    public function hasManySet($field, array $thatIds, $excludeId = null) {
569
        $thisField    = $this->definition->getSubTypeField($field, 'many', 'thisField');
570
        $thatField    = $this->definition->getSubTypeField($field, 'many', 'thatField');
571
        $thatEntity   = $this->definition->getSubTypeField($field, 'many', 'entity');
572
        $entityTable  = $this->definition->getServiceProvider()->getData($thatEntity)->getDefinition()->getTable();
573
        $queryBuilder = $this->database->createQueryBuilder();
574
        $queryBuilder->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that')
575
            ->from('`'.$field.'`', 't1')
576
            ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
577
            ->where('t2.deleted_at IS NULL')
578
            ->orderBy('this, that');
579
        if ($excludeId !== null) {
580
            $queryBuilder->andWhere('t1.`'.$thisField.'` != ?')->setParameter(0, $excludeId);
581
        }
582
        $existingMany = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
583
        $existingMap  = array_reduce($existingMany, function(&$carry, $existing) {
584
            $carry[$existing['this']][] = $existing['that'];
585
            return $carry;
586
        }, []);
587
        sort($thatIds);
588
        return in_array($thatIds, array_values($existingMap));
589
    }
590
591
}
592