Completed
Push — master ( 71a1b7...e8370f )
by Philip
05:25
created

MySQLData::getManyIds()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 23
rs 8.7972
cc 4
eloc 18
nc 4
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
    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
    protected function getManyIds($fields, $params) {
149
        $manyIds = [];
150
        foreach ($fields as $field) {
151
            $thisField = $this->definition->getManyThisField($field);
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...
152
            $thatField = $this->definition->getManyThatField($field);
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...
153
            $queryBuilder = $this->database->createQueryBuilder();
154
            $queryBuilder
155
                ->select('`'.$thisField.'`')
156
                ->from($field)
157
                ->where('`'.$thatField.'` IN (?)')
158
                ->setParameter(0, array_column($params[$field], 'id'), Connection::PARAM_INT_ARRAY)
159
                ->groupBy('`'.$thisField.'`')
160
            ;
161
            $queryResult = $queryBuilder->execute();
162
            $manyResults = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
163
            foreach ($manyResults as $manyResult) {
164
                if (!in_array($manyResult[$thisField], $manyIds)) {
165
                    $manyIds[] = $manyResult[$thisField];
166
                }
167
            }
168
        }
169
        return $manyIds;
170
    }
171
172
    /**
173
     * Adds sorting parameters to the query.
174
     *
175
     * @param QueryBuilder $queryBuilder
176
     * the query
177
     * @param $filter
178
     * the filter all resulting entities must fulfill, the keys as field names
179
     * @param $filterOperators
180
     * the operators of the filter like "=" defining the full condition of the field
181
     */
182
    protected function addFilter(QueryBuilder $queryBuilder, array $filter, array $filterOperators) {
183
        $i = 0;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 10 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...
184
        $manyFields = [];
185
        foreach ($filter as $field => $value) {
186
            if ($this->definition->getType($field) === 'many') {
187
                $manyFields[]= $field;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 0 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

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

will have no issues, while

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

will report issues in lines 1 and 2.

Loading history...
188
                continue;
189
            }
190
            if ($value === null) {
191
                $queryBuilder->andWhere('`'.$field.'` IS NULL');
192
            } else {
193
                $operator = array_key_exists($field, $filterOperators) ? $filterOperators[$field] : '=';
194
                $queryBuilder
195
                    ->andWhere('`'.$field.'` '.$operator.' ?')
196
                    ->setParameter($i, $value, \PDO::PARAM_STR);
197
            }
198
            $i++;
199
        }
200
        $idsToInclude = $this->getManyIds($manyFields, $filter);
201
        if (!empty($idsToInclude)) {
202
            $queryBuilder
203
                ->andWhere('id IN (?)')
204
                ->setParameter($i, $idsToInclude, Connection::PARAM_STR_ARRAY)
205
            ;
206
        }
207
    }
208
209
    /**
210
     * Adds pagination parameters to the query.
211
     *
212
     * @param QueryBuilder $queryBuilder
213
     * the query
214
     * @param integer|null $skip
215
     * the rows to skip
216
     * @param integer|null $amount
217
     * the maximum amount of rows
218
     */
219
    protected function addPagination(QueryBuilder $queryBuilder, $skip, $amount) {
220
        $queryBuilder->setMaxResults(9999999999);
221
        if ($amount !== null) {
222
            $queryBuilder->setMaxResults(abs(intval($amount)));
223
        }
224
        if ($skip !== null) {
225
            $queryBuilder->setFirstResult(abs(intval($skip)));
226
        }
227
    }
228
229
    /**
230
     * Adds sorting parameters to the query.
231
     *
232
     * @param QueryBuilder $queryBuilder
233
     * the query
234
     * @param string|null $sortField
235
     * the sort field
236
     * @param boolean|null $sortAscending
237
     * true if sort ascending, false if descending
238
     */
239
    protected function addSort(QueryBuilder $queryBuilder, $sortField, $sortAscending) {
240
        if ($sortField !== null) {
241
242
            $type = $this->definition->getType($sortField);
243
            if ($type === 'many') {
244
                $sortField = $this->definition->getInitialSortField();
245
            }
246
247
            $order = $sortAscending === true ? 'ASC' : 'DESC';
248
            $queryBuilder->orderBy('`'.$sortField.'`', $order);
249
        }
250
    }
251
252
    /**
253
     * Adds the id and name of referenced entities to the given entities. The
254
     * reference field is before the raw id of the referenced entity and after
255
     * the fetch, it's an array with the keys id and name.
256
     *
257
     * @param Entity[] &$entities
258
     * the entities to fetch the references for
259
     * @param string $field
260
     * the reference field
261
     */
262
    protected function fetchReferencesForField(array &$entities, $field) {
263
        $nameField    = $this->definition->getReferenceNameField($field);
264
        $queryBuilder = $this->database->createQueryBuilder();
265
266
        $ids = array_map(function(Entity $entity) use ($field) {
267
            return $entity->get($field);
268
        }, $entities);
269
270
        $referenceEntity = $this->definition->getReferenceEntity($field);
271
        $table           = $this->definition->getServiceProvider()->getData($referenceEntity)->getDefinition()->getTable();
272
        $queryBuilder
273
            ->from('`'.$table.'`', '`'.$table.'`')
274
            ->where('id IN (?)')
275
            ->andWhere('deleted_at IS NULL');
276
        if ($nameField) {
277
            $queryBuilder->select('id', $nameField);
278
        } else {
279
            $queryBuilder->select('id');
280
        }
281
282
        $queryBuilder->setParameter(0, $ids, Connection::PARAM_INT_ARRAY);
283
284
        $queryResult = $queryBuilder->execute();
285
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
286
        $amount      = count($entities);
287
        foreach ($rows as $row) {
288
            for ($i = 0; $i < $amount; ++$i) {
289
                if ($entities[$i]->get($field) == $row['id']) {
290
                    $value = ['id' => $entities[$i]->get($field)];
291
                    if ($nameField) {
292
                        $value['name'] = $row[$nameField];
293
                    }
294
                    $entities[$i]->set($field, $value);
295
                }
296
            }
297
        }
298
    }
299
300
    /**
301
     * Generates a new UUID.
302
     *
303
     * @return string|null
304
     * the new UUID or null if this instance isn't configured to do so
305
     */
306
    protected function generateUUID() {
307
        $uuid = null;
308
        if ($this->useUUIDs) {
309
            $sql    = 'SELECT UUID() as id';
310
            $result = $this->database->fetchAssoc($sql);
311
            $uuid   = $result['id'];
312
        }
313
        return $uuid;
314
    }
315
316
    protected function enrichWithMany(array $rows) {
317
        $manyFields = $this->getManyFields();
318
        $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...
319
        foreach ($rows as $row) {
320
            foreach ($manyFields as $manyField) {
321
                $row[$manyField] = [];
322
            }
323
            $mapping[$row['id']] = $row;
324
        }
325
        foreach ($manyFields as $manyField) {
326
            $queryBuilder = $this->database->createQueryBuilder();
327
            $nameField    = $this->definition->getManyNameField($manyField);
328
            $thisField    = $this->definition->getManyThisField($manyField);
329
            $thatField    = $this->definition->getManyThatField($manyField);
330
            $entity       = $this->definition->getManyEntity($manyField);
331
            $entityTable  = $this->definition->getServiceProvider()->getData($entity)->getDefinition()->getTable();
332
            $nameSelect   = $nameField !== null ? ', t2.`'.$nameField.'` AS name' : '';
333
            $queryBuilder
334
                ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that'.$nameSelect)
335
                ->from('`'.$manyField.'`', 't1')
336
                ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
337
                ->where('t1.`'.$thisField.'` IN (?)')
338
                ->andWhere('t2.deleted_at IS NULL');
339
            $queryBuilder->setParameter(0, array_keys($mapping), Connection::PARAM_INT_ARRAY);
340
            $queryResult    = $queryBuilder->execute();
341
            $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
342
            foreach ($manyReferences as $manyReference) {
343
                $many = ['id' => $manyReference['that']];
344
                if ($nameField !== null) {
345
                    $many['name'] = $manyReference['name'];
346
                }
347
                $mapping[$manyReference['this']][$manyField][] = $many;
348
            }
349
        }
350
        return array_values($mapping);
351
    }
352
353
    protected function populateMany(Entity $entity) {
354
        $manyFields = $this->getManyFields();
355
        $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...
356
        foreach ($manyFields as $manyField) {
357
            $thisField = $this->definition->getManyThisField($manyField);
358
            $thatField = $this->definition->getManyThatField($manyField);
359
            $this->database->delete($manyField, [$thisField => $id]);
360
            foreach ($entity->get($manyField) as $thatId) {
361
                $this->database->insert($manyField, [
362
                    $thisField => $id,
363
                    $thatField => $thatId['id']
364
                ]);
365
            }
366
        }
367
    }
368
369
    /**
370
     * Constructor.
371
     *
372
     * @param EntityDefinition $definition
373
     * the entity definition
374
     * @param FileProcessorInterface $fileProcessor
375
     * the file processor to use
376
     * @param $database
377
     * the Doctrine DBAL instance to use
378
     * @param boolean $useUUIDs
379
     * flag whether to use UUIDs as primary key
380
     */
381
    public function __construct(EntityDefinition $definition, FileProcessorInterface $fileProcessor, $database, $useUUIDs) {
382
        $this->definition    = $definition;
383
        $this->fileProcessor = $fileProcessor;
384
        $this->database      = $database;
385
        $this->useUUIDs      = $useUUIDs;
386
    }
387
388
    /**
389
     * {@inheritdoc}
390
     */
391
    public function get($id) {
392
        $entities = $this->listEntries(['id' => $id]);
393
        if (count($entities) == 0) {
394
            return null;
395
        }
396
        return $entities[0];
397
    }
398
399
    /**
400
     * {@inheritdoc}
401
     */
402
    public function listEntries(array $filter = [], array $filterOperators = [], $skip = null, $amount = null, $sortField = null, $sortAscending = null) {
403
        $fieldNames = $this->definition->getFieldNames();
404
405
        $queryBuilder = $this->database->createQueryBuilder();
406
        $table        = $this->definition->getTable();
407
        $queryBuilder
408
            ->select('`'.implode('`,`', $fieldNames).'`')
409
            ->from('`'.$table.'`', '`'.$table.'`')
410
            ->where('deleted_at IS NULL');
411
412
        $this->addFilter($queryBuilder, $filter, $filterOperators);
413
        $this->addPagination($queryBuilder, $skip, $amount);
414
        $this->addSort($queryBuilder, $sortField, $sortAscending);
415
416
        $queryResult = $queryBuilder->execute();
417
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
418
        $rows        = $this->enrichWithMany($rows);
419
        $entities    = [];
420
        foreach ($rows as $row) {
421
            $entities[] = $this->hydrate($row);
422
        }
423
        return $entities;
424
    }
425
426
    /**
427
     * {@inheritdoc}
428
     */
429
    public function create(Entity $entity) {
430
431
        $result = $this->shouldExecuteEvents($entity, 'before', 'create');
432
        if (!$result) {
433
            return false;
434
        }
435
436
        $queryBuilder = $this->database->createQueryBuilder();
437
        $queryBuilder
438
            ->insert('`'.$this->definition->getTable().'`')
439
            ->setValue('created_at', 'UTC_TIMESTAMP()')
440
            ->setValue('updated_at', 'UTC_TIMESTAMP()')
441
            ->setValue('version', 0);
442
443
444
        $this->setValuesAndParameters($entity, $queryBuilder, 'setValue');
445
446
        $id = $this->generateUUID();
447
        if ($this->useUUIDs) {
448
            $queryBuilder->setValue('`id`', '?');
449
            $uuidI = count($this->getFormFields());
450
            $queryBuilder->setParameter($uuidI, $id);
451
        }
452
453
        $queryBuilder->execute();
454
455
        if (!$this->useUUIDs) {
456
            $id = $this->database->lastInsertId();
457
        }
458
459
        $entity->set('id', $id);
460
461
        $createdEntity = $this->get($entity->get('id'));
462
        $entity->set('version', $createdEntity->get('version'));
463
        $entity->set('created_at', $createdEntity->get('created_at'));
464
        $entity->set('updated_at', $createdEntity->get('updated_at'));
465
466
        $this->populateMany($entity);
467
468
        $this->shouldExecuteEvents($entity, 'after', 'create');
469
470
        return true;
471
    }
472
473
    /**
474
     * {@inheritdoc}
475
     */
476
    public function update(Entity $entity) {
477
478
        $result = $this->shouldExecuteEvents($entity, 'before', 'update');
479
        if (!$result) {
480
            return false;
481
        }
482
483
        $formFields   = $this->getFormFields();
484
        $queryBuilder = $this->database->createQueryBuilder();
485
        $queryBuilder
486
            ->update('`'.$this->definition->getTable().'`')
487
            ->set('updated_at', 'UTC_TIMESTAMP()')
488
            ->set('version', 'version + 1')
489
            ->where('id = ?')
490
            ->setParameter(count($formFields), $entity->get('id'));
491
492
        $this->setValuesAndParameters($entity, $queryBuilder, 'set');
493
        $affected = $queryBuilder->execute();
494
495
        $this->populateMany($entity);
496
497
        $this->shouldExecuteEvents($entity, 'after', 'update');
498
499
        return $affected;
500
    }
501
502
    /**
503
     * {@inheritdoc}
504
     */
505
    public function getIdToNameMap($entity, $nameField) {
506
        $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...
507
        $queryBuilder = $this->database->createQueryBuilder();
508
        $nameSelect   = $nameField !== null ? ',`'.$nameField.'`' : '';
509
        $queryBuilder
510
            ->select('id'.$nameSelect)
511
            ->from('`'.$table.'`', 't1')
512
            ->where('deleted_at IS NULL');
513
        if ($nameField) {
514
            $queryBuilder->orderBy($nameField);
515
        } else {
516
            $queryBuilder->orderBy('id');
517
        }
518
        $queryResult    = $queryBuilder->execute();
519
        $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
520
        $result         = [];
521
        foreach ($manyReferences as $manyReference) {
522
            $result[$manyReference['id']] = $nameField ? $manyReference[$nameField] : $manyReference['id'];
523
        }
524
        return $result;
525
    }
526
527
    /**
528
     * {@inheritdoc}
529
     */
530
    public function countBy($table, array $params, array $paramsOperators, $excludeDeleted) {
531
        $queryBuilder = $this->database->createQueryBuilder();
532
        $queryBuilder
533
            ->select('COUNT(id)')
534
            ->from('`'.$table.'`', '`'.$table.'`')
535
        ;
536
537
        $deletedExcluder = 'where';
538
        $i               = 0;
539
        $manyFields      = [];
540
        foreach ($params as $name => $value) {
541
            if ($this->definition->getType($name) === 'many') {
542
                $manyFields[] = $name;
543
                continue;
544
            }
545
            $queryBuilder
546
                ->andWhere('`'.$name.'` '.$paramsOperators[$name].' ?')
547
                ->setParameter($i, $value, \PDO::PARAM_STR)
548
            ;
549
            $i++;
550
            $deletedExcluder = 'andWhere';
551
        }
552
553
        $idsToInclude = $this->getManyIds($manyFields, $params);
554
        if (!empty($idsToInclude)) {
555
            $queryBuilder
556
                ->andWhere('id IN (?)')
557
                ->setParameter($i, $idsToInclude, Connection::PARAM_STR_ARRAY)
558
            ;
559
            $deletedExcluder = 'andWhere';
560
        }
561
562
        if ($excludeDeleted) {
563
            $queryBuilder->$deletedExcluder('deleted_at IS NULL');
564
        }
565
566
        $queryResult = $queryBuilder->execute();
567
        $result      = $queryResult->fetch(\PDO::FETCH_NUM);
568
        return intval($result[0]);
569
    }
570
571
    /**
572
     * {@inheritdoc}
573
     */
574
    public function fetchReferences(array &$entities = null) {
575
        if (!$entities) {
576
            return;
577
        }
578
        foreach ($this->definition->getFieldNames() as $field) {
579
            if ($this->definition->getType($field) !== 'reference') {
580
                continue;
581
            }
582
            $this->fetchReferencesForField($entities, $field);
583
        }
584
    }
585
586
587
    /**
588
     * {@inheritdoc}
589
     */
590
    public function hasManySet($field, array $thatIds) {
591
        $thisField    = $this->definition->getManyThisField($field);
592
        $thatField    = $this->definition->getManyThatField($field);
593
        $thatEntity   = $this->definition->getManyEntity($field);
594
        $entityTable  = $this->definition->getServiceProvider()->getData($thatEntity)->getDefinition()->getTable();
595
        $queryBuilder = $this->database->createQueryBuilder();
596
        $queryBuilder
597
            ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that')
598
            ->from('`'.$field.'`', 't1')
599
            ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
600
            ->andWhere('t2.deleted_at IS NULL')
601
            ->orderBy('this, that');
602
        $queryResult  = $queryBuilder->execute();
603
        $existingMany = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
604
        $existingMap  = [];
605
        foreach ($existingMany as $existing) {
606
            $existingMap[$existing['this']][] = $existing['that'];
607
        }
608
        sort($thatIds);
609
        return in_array($thatIds, array_values($existingMap));
610
    }
611
612
}
613