Completed
Push — master ( a10e39...8b1872 )
by Philip
07:15
created

MySQLData::addSort()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 8
cts 8
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 5
nop 3
crap 4
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
use League\Flysystem\FilesystemInterface;
17
18
/**
19
 * MySQL Data implementation using a given Doctrine DBAL instance.
20
 */
21
class MySQLData extends AbstractData
22
{
23
24
    /**
25
     * Holds the Doctrine DBAL instance.
26
     * @var Connection
27
     */
28
    protected $database;
29
30
    /**
31
     * Flag whether to use UUIDs as primary key.
32
     * @var bool
33
     */
34
    protected $useUUIDs;
35
36
    /**
37
     * Adds the soft deletion parameters if activated.
38
     *
39
     * @param QueryBuilder $queryBuilder
40
     * the query builder to add the deletion condition to
41
     * @param string $fieldPrefix
42
     * the prefix to add before the deleted_at field like an table alias
43
     * @param string $method
44
     * the method to use of the query builder, "where" or "andWhere"
45
     */
46 35
    protected function addSoftDeletionToQuery(QueryBuilder $queryBuilder, $fieldPrefix = '', $method = 'andWhere')
47
    {
48 35
        if (!$this->definition->isHardDeletion()) {
49 34
            $queryBuilder->$method($fieldPrefix.'deleted_at IS NULL');
50
        }
51 35
    }
52
53
    /**
54
     * Sets the values and parameters of the upcoming given query according
55
     * to the entity.
56
     *
57
     * @param Entity $entity
58
     * the entity with its fields and values
59
     * @param QueryBuilder $queryBuilder
60
     * the upcoming query
61
     * @param string $setMethod
62
     * what method to use on the QueryBuilder: 'setValue' or 'set'
63
     */
64 34
    protected function setValuesAndParameters(Entity $entity, QueryBuilder $queryBuilder, $setMethod)
65
    {
66 34
        $formFields = $this->getFormFields();
67 34
        $count      = count($formFields);
68 34
        for ($i = 0; $i < $count; ++$i) {
69 34
            $type  = $this->definition->getType($formFields[$i]);
70 34
            $value = $entity->get($formFields[$i]);
71 34
            if ($type == 'boolean') {
72 34
                $value = $value ? 1 : 0;
73
            }
74 34
            if ($type == 'reference' && is_array($value)) {
75 4
                $value = $value['id'];
76
            }
77 34
            $queryBuilder->$setMethod('`'.$formFields[$i].'`', '?');
78 34
            $queryBuilder->setParameter($i, $value);
79
        }
80 34
    }
81
82
    /**
83
     * Checks whether the by id given entity still has children referencing it.
84
     *
85
     * @param integer $id
86
     * the current entities id
87
     *
88
     * @return boolean
89
     * true if the entity still has children
90
     */
91 2
    protected function hasChildren($id)
92
    {
93 2
        foreach ($this->definition->getChildren() as $child) {
94 2
            $queryBuilder = $this->database->createQueryBuilder();
95
            $queryBuilder
96 2
                ->select('COUNT(id)')
97 2
                ->from('`'.$child[0].'`', '`'.$child[0].'`')
98 2
                ->where('`'.$child[1].'` = ?')
99 2
                ->setParameter(0, $id)
100
            ;
101 2
            if (!$this->getDefinition()->getServiceProvider()->getData($child[2])->getDefinition()->isHardDeletion()) {
102
                $queryBuilder->andWhere('deleted_at IS NULL');
103
            }
104 2
            $queryResult = $queryBuilder->execute();
105 2
            $result      = $queryResult->fetch(\PDO::FETCH_NUM);
106 2
            if ($result[0] > 0) {
107 2
                return true;
108
            }
109
        }
110 2
        return false;
111
    }
112
113
    /**
114
     * Deletes any many to many references pointing to the given entity.
115
     *
116
     * @param Entity $entity
117
     * the referenced entity
118
     */
119 4
    protected function deleteManyToManyReferences(Entity $entity)
120
    {
121 4
        foreach ($this->definition->getServiceProvider()->getEntities() as $entityName) {
122 4
            $data = $this->definition->getServiceProvider()->getData($entityName);
123 4
            foreach ($data->getDefinition()->getFieldNames(true) as $field) {
124 4
                if ($data->getDefinition()->getType($field) == 'many') {
125 4
                    $otherEntity = $data->getDefinition()->getSubTypeField($field, 'many', 'entity');
126 4
                    $otherData = $this->definition->getServiceProvider()->getData($otherEntity);
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...
127 4
                    if ($entity->getDefinition()->getTable() == $otherData->getDefinition()->getTable()) {
128 3
                        $thatField = $data->getDefinition()->getSubTypeField($field, 'many', 'thatField');
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...
129 3
                        $queryBuilder = $this->database->createQueryBuilder();
130
                        $queryBuilder
131 3
                            ->delete('`'.$field.'`')
132 3
                            ->where('`'.$thatField.'` = ?')
133 3
                            ->setParameter(0, $entity->get('id'))
134 4
                            ->execute()
135
                        ;
136
                    }
137
                }
138
            }
139
        }
140 4
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 4
    protected function doDelete(Entity $entity, $deleteCascade)
146
    {
147 4
        $id = $entity->get('id');
148 4
        if ($deleteCascade) {
149 3
            $result = $this->deleteChildren($id, $deleteCascade);
150 3
            if ($result !== static::DELETION_SUCCESS) {
151 3
                return $result;
152
            }
153 2
        } elseif ($this->hasChildren($id)) {
154 2
            return static::DELETION_FAILED_STILL_REFERENCED;
155
        }
156
157 4
        $this->deleteManyToManyReferences($entity);
158
159 4
        $query = $this->database->createQueryBuilder();
160 4
        if ($this->definition->isHardDeletion()) {
161 3
            $query->delete('`'.$this->definition->getTable().'`');
162
        } else {
163
            $query
164 3
                ->update('`'.$this->definition->getTable().'`')
165 3
                ->set('deleted_at', 'UTC_TIMESTAMP()')
166
            ;
167
        }
168
        $query
169 4
            ->where('id = ?')
170 4
            ->setParameter(0, $id)
171 4
            ->execute()
172
        ;
173 4
        return static::DELETION_SUCCESS;
174
    }
175
176
    /**
177
     * Gets all possible many-to-many ids existing for this definition.
178
     *
179
     * @param array $fields
180
     * the many field names to fetch for
181
     * @param $params
182
     * the parameters the possible many field values to fetch for
183
     * @return array
184
     * an array of this many-to-many ids
185
     */
186 35
    protected function getManyIds(array $fields, array $params)
187
    {
188 35
        $manyIds = [];
189 35
        foreach ($fields as $field) {
190 3
            $thisField    = $this->definition->getSubTypeField($field, 'many', 'thisField');
191 3
            $thatField    = $this->definition->getSubTypeField($field, 'many', 'thatField');
192 3
            $queryBuilder = $this->database->createQueryBuilder();
193
            $queryBuilder
194 3
                ->select('`'.$thisField.'`')
195 3
                ->from($field)
196 3
                ->where('`'.$thatField.'` IN (?)')
197 3
                ->setParameter(0, array_column($params[$field], 'id'), Connection::PARAM_STR_ARRAY)
198 3
                ->groupBy('`'.$thisField.'`')
199
            ;
200 3
            $queryResult = $queryBuilder->execute();
201 3
            $manyResults = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
202 3
            $manyIds     = array_merge($manyIds, array_column($manyResults, $thisField));
203
204
        }
205 35
        return $manyIds;
206
    }
207
208
    /**
209
     * Adds sorting parameters to the query.
210
     *
211
     * @param QueryBuilder $queryBuilder
212
     * the query
213
     * @param $filter
214
     * the filter all resulting entities must fulfill, the keys as field names
215
     * @param $filterOperators
216
     * the operators of the filter like "=" defining the full condition of the field
217
     */
218 35
    protected function addFilter(QueryBuilder $queryBuilder, array $filter, array $filterOperators)
219
    {
220 35
        $i          = 0;
221 35
        $manyFields = [];
222 35
        foreach ($filter as $field => $value) {
223 34
            if ($this->definition->getType($field) === 'many') {
224 2
                $manyFields[] = $field;
225 2
                continue;
226
            }
227 34
            if ($value === null) {
228 1
                $queryBuilder->andWhere('`'.$field.'` IS NULL');
229
            } else {
230 34
                $operator = array_key_exists($field, $filterOperators) ? $filterOperators[$field] : '=';
231
                $queryBuilder
232 34
                    ->andWhere('`'.$field.'` '.$operator.' ?')
233 34
                    ->setParameter($i, $value, \PDO::PARAM_STR);
234
            }
235 34
            $i++;
236
        }
237 35
        $idsToInclude = $this->getManyIds($manyFields, $filter);
238 35
        if (!empty($idsToInclude)) {
239
            $queryBuilder
240 2
                ->andWhere('id IN (?)')
241 2
                ->setParameter($i, $idsToInclude, Connection::PARAM_STR_ARRAY)
242
            ;
243
        }
244 35
    }
245
246
    /**
247
     * Adds pagination parameters to the query.
248
     *
249
     * @param QueryBuilder $queryBuilder
250
     * the query
251
     * @param integer|null $skip
252
     * the rows to skip
253
     * @param integer|null $amount
254
     * the maximum amount of rows
255
     */
256 35
    protected function addPagination(QueryBuilder $queryBuilder, $skip, $amount)
257
    {
258 35
        $queryBuilder->setMaxResults(9999999999);
259 35
        if ($amount !== null) {
260 5
            $queryBuilder->setMaxResults(abs(intval($amount)));
261
        }
262 35
        if ($skip !== null) {
263 5
            $queryBuilder->setFirstResult(abs(intval($skip)));
264
        }
265 35
    }
266
267
    /**
268
     * Adds sorting parameters to the query.
269
     *
270
     * @param QueryBuilder $queryBuilder
271
     * the query
272
     * @param string|null $sortField
273
     * the sort field
274
     * @param boolean|null $sortAscending
275
     * true if sort ascending, false if descending
276
     */
277 35
    protected function addSort(QueryBuilder $queryBuilder, $sortField, $sortAscending)
278
    {
279 35
        if ($sortField !== null) {
280
281 5
            $type = $this->definition->getType($sortField);
282 5
            if ($type === 'many') {
283 1
                $sortField = $this->definition->getInitialSortField();
284
            }
285
286 5
            $order = $sortAscending === true ? 'ASC' : 'DESC';
287 5
            $queryBuilder->orderBy('`'.$sortField.'`', $order);
288
        }
289 35
    }
290
291
    /**
292
     * Adds the id and name of referenced entities to the given entities. The
293
     * reference field is before the raw id of the referenced entity and after
294
     * the fetch, it's an array with the keys id and name.
295
     *
296
     * @param Entity[] &$entities
297
     * the entities to fetch the references for
298
     * @param string $field
299
     * the reference field
300
     */
301 21
    protected function fetchReferencesForField(array &$entities, $field)
302
    {
303 21
        $nameField    = $this->definition->getSubTypeField($field, 'reference', 'nameField');
304 21
        $queryBuilder = $this->database->createQueryBuilder();
305
306 21
        $ids = $this->getReferenceIds($entities, $field);
307
308 21
        $referenceEntity = $this->definition->getSubTypeField($field, 'reference', 'entity');
309 21
        $table           = $this->definition->getServiceProvider()->getData($referenceEntity)->getDefinition()->getTable();
310
        $queryBuilder
311 21
            ->from('`'.$table.'`', '`'.$table.'`')
312 21
            ->where('id IN (?)')
313
        ;
314 21
        $this->addSoftDeletionToQuery($queryBuilder);
315 21
        if ($nameField) {
316 21
            $queryBuilder->select('id', $nameField);
317
        } else {
318 21
            $queryBuilder->select('id');
319
        }
320
321 21
        $queryBuilder->setParameter(0, $ids, Connection::PARAM_STR_ARRAY);
322
323 21
        $queryResult = $queryBuilder->execute();
324 21
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
325 21
        $amount      = count($entities);
326 21
        foreach ($rows as $row) {
327 21
            for ($i = 0; $i < $amount; ++$i) {
328 21
                if ($entities[$i]->get($field) == $row['id']) {
329 21
                    $value = ['id' => $entities[$i]->get($field)];
330 21
                    if ($nameField) {
331 21
                        $value['name'] = $row[$nameField];
332
                    }
333 21
                    $entities[$i]->set($field, $value);
334
                }
335
            }
336
        }
337 21
    }
338
339
    /**
340
     * Generates a new UUID.
341
     *
342
     * @return string|null
343
     * the new UUID or null if this instance isn't configured to do so
344
     */
345 34
    protected function generateUUID()
346
        {
347 34
        $uuid = null;
348 34
        if ($this->useUUIDs) {
349 1
            $sql    = 'SELECT UUID() as id';
350 1
            $result = $this->database->fetchAssoc($sql);
351 1
            $uuid   = $result['id'];
352
        }
353 34
        return $uuid;
354
    }
355
356
    /**
357
     * Enriches the given mapping of entity id to raw entity data with some many-to-many data.
358
     *
359
     * @param array $idToData
360
     * a reference to the map entity id to raw entity data
361
     * @param $manyField
362
     * the many field to enrich data with
363
     */
364 34
    protected function enrichWithManyField(&$idToData, $manyField)
365
    {
366 34
        $queryBuilder     = $this->database->createQueryBuilder();
367 34
        $nameField        = $this->definition->getSubTypeField($manyField, 'many', 'nameField');
368 34
        $thisField        = $this->definition->getSubTypeField($manyField, 'many', 'thisField');
369 34
        $thatField        = $this->definition->getSubTypeField($manyField, 'many', 'thatField');
370 34
        $entity           = $this->definition->getSubTypeField($manyField, 'many', 'entity');
371 34
        $entityDefinition = $this->definition->getServiceProvider()->getData($entity)->getDefinition();
372 34
        $entityTable      = $entityDefinition->getTable();
373 34
        $nameSelect       = $nameField !== null ? ', t2.`'.$nameField.'` AS name' : '';
374
        $queryBuilder
375 34
            ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS id'.$nameSelect)
376 34
            ->from('`'.$manyField.'`', 't1')
377 34
            ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
378 34
            ->where('t1.`'.$thisField.'` IN (?)')
379
        ;
380 34
        if (!$entityDefinition->isHardDeletion()) {
381
            $queryBuilder->andWhere('t2.deleted_at IS NULL');
382
        }
383 34
        $queryBuilder->setParameter(0, array_keys($idToData), Connection::PARAM_STR_ARRAY);
384 34
        $queryResult    = $queryBuilder->execute();
385 34
        $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
386 34
        foreach ($manyReferences as $manyReference) {
387 2
            $entityId = $manyReference['this'];
388 2
            unset($manyReference['this']);
389 2
            $idToData[$entityId][$manyField][] = $manyReference;
390
        }
391 34
    }
392
393
    /**
394
     * Fetches to the rows belonging many-to-many entries and adds them to the rows.
395
     *
396
     * @param array $rows
397
     * the rows to enrich
398
     * @return array
399
     * the enriched rows
400
     */
401 35
    protected function enrichWithMany(array $rows)
402
    {
403 35
        $manyFields = $this->getManyFields();
404 35
        $idToData   = [];
405 35
        foreach ($rows as $row) {
406 34
            foreach ($manyFields as $manyField) {
407 34
                $row[$manyField] = [];
408
            }
409 34
            $idToData[$row['id']] = $row;
410
        }
411 35
        foreach ($manyFields as $manyField) {
412 34
            $this->enrichWithManyField($idToData, $manyField);
413
        }
414 35
        return array_values($idToData);
415
    }
416
417
    /**
418
     * First, deletes all to the given entity related many-to-many entries from the DB
419
     * and then writes them again.
420
     *
421
     * @param Entity $entity
422
     * the entity to save the many-to-many entries of
423
     */
424 34
    protected function saveMany(Entity $entity)
425
    {
426 34
        $manyFields = $this->getManyFields();
427 34
        $id         = $entity->get('id');
428 34
        foreach ($manyFields as $manyField) {
429 34
            $thisField = '`'.$this->definition->getSubTypeField($manyField, 'many', 'thisField').'`';
430 34
            $thatField = '`'.$this->definition->getSubTypeField($manyField, 'many', 'thatField').'`';
431 34
            $this->database->delete($manyField, [$thisField => $id]);
432 34
            $manyValues = $entity->get($manyField) ?: [];
433 34
            foreach ($manyValues as $thatId) {
434 8
                $this->database->insert($manyField, [
435 8
                    $thisField => $id,
436 34
                    $thatField => $thatId['id']
437
                ]);
438
            }
439
        }
440 34
    }
441
442
    /**
443
     * Adds the id and name of referenced entities to the given entities. Each
444
     * reference field is before the raw id of the referenced entity and after
445
     * the fetch, it's an array with the keys id and name.
446
     *
447
     * @param Entity[] &$entities
448
     * the entities to fetch the references for
449
     *
450
     * @return void
451
     */
452 35
    protected function enrichWithReference(array &$entities)
453
    {
454 35
        if (empty($entities)) {
455 11
            return;
456
        }
457 34
        foreach ($this->definition->getFieldNames() as $field) {
458 34
            if ($this->definition->getType($field) !== 'reference') {
459 34
                continue;
460
            }
461 21
            $this->fetchReferencesForField($entities, $field);
462
        }
463 34
    }
464
465
    /**
466
     * {@inheritdoc}
467
     */
468 34
    protected function doCreate(Entity $entity)
469
    {
470
471 34
        $queryBuilder = $this->database->createQueryBuilder();
472
        $queryBuilder
473 34
            ->insert('`'.$this->definition->getTable().'`')
474 34
            ->setValue('created_at', 'UTC_TIMESTAMP()')
475 34
            ->setValue('updated_at', 'UTC_TIMESTAMP()');
476 34
        if ($this->definition->hasOptimisticLocking()) {
477 21
            $queryBuilder->setValue('version', 0);
478
        }
479
480 34
        $this->setValuesAndParameters($entity, $queryBuilder, 'setValue');
481
482 34
        $id = $this->generateUUID();
483 34
        if ($this->useUUIDs) {
484 1
            $queryBuilder->setValue('`id`', '?');
485 1
            $uuidI = count($this->getFormFields());
486 1
            $queryBuilder->setParameter($uuidI, $id);
487
        }
488
489 34
        $queryBuilder->execute();
490
491 34
        if (!$this->useUUIDs) {
492 33
            $id = $this->database->lastInsertId();
493
        }
494
495 34
        $this->enrichEntityWithMetaData($id, $entity);
496 34
        $this->saveMany($entity);
497 34
        $entities = [$entity];
498 34
        $this->enrichWithReference($entities);
499
500 34
        return true;
501
    }
502
503
    /**
504
     * {@inheritdoc}
505
     */
506 13
    protected function doUpdate(Entity $entity)
507
    {
508 13
        $queryBuilder = $this->database->createQueryBuilder();
509 13
        $queryBuilder->update('`'.$this->definition->getTable().'`')
510 13
            ->set('updated_at', 'UTC_TIMESTAMP()')
511 13
            ->where('id = ?')
512 13
            ->setParameter(count($this->getFormFields()), $entity->get('id'));
513 13
        if ($this->definition->hasOptimisticLocking()) {
514 2
            $queryBuilder->set('version', 'version + 1');
515
        }
516
517 13
        $this->setValuesAndParameters($entity, $queryBuilder, 'set');
518 13
        $affected = $queryBuilder->execute();
519
520 13
        $this->saveMany($entity);
521 13
        $entities = [$entity];
522 13
        $this->enrichWithReference($entities);
523 13
        return $affected > 0;
524
    }
525
526
    /**
527
     * Constructor.
528
     *
529
     * @param EntityDefinition $definition
530
     * the entity definition
531
     * @param FilesystemInterface $filesystem
532
     * the filesystem to use
533
     * @param Connection $database
534
     * the Doctrine DBAL instance to use
535
     * @param boolean $useUUIDs
536
     * flag whether to use UUIDs as primary key
537
     */
538 76
    public function __construct(EntityDefinition $definition, FilesystemInterface $filesystem, Connection $database, $useUUIDs)
539
    {
540 76
        $this->definition = $definition;
541 76
        $this->filesystem = $filesystem;
542 76
        $this->database   = $database;
543 76
        $this->useUUIDs   = $useUUIDs;
544 76
        $this->events     = new EntityEvents();
545 76
    }
546
547
    /**
548
     * {@inheritdoc}
549
     */
550 34
    public function get($id)
551
    {
552 34
        $entities = $this->listEntries(['id' => $id]);
553 34
        if (count($entities) == 0) {
554 8
            return null;
555
        }
556 34
        return $entities[0];
557
    }
558
559
    /**
560
     * {@inheritdoc}
561
     */
562 35
    public function listEntries(array $filter = [], array $filterOperators = [], $skip = null, $amount = null, $sortField = null, $sortAscending = null)
563
    {
564 35
        $fieldNames = $this->definition->getFieldNames();
565
566 35
        $queryBuilder = $this->database->createQueryBuilder();
567 35
        $table        = $this->definition->getTable();
568
        $queryBuilder
569 35
            ->select('`'.implode('`,`', $fieldNames).'`')
570 35
            ->from('`'.$table.'`', '`'.$table.'`')
571
        ;
572
573 35
        $this->addFilter($queryBuilder, $filter, $filterOperators);
574 35
        $this->addSoftDeletionToQuery($queryBuilder);
575 35
        $this->addPagination($queryBuilder, $skip, $amount);
576 35
        $this->addSort($queryBuilder, $sortField, $sortAscending);
577
578 35
        $queryResult = $queryBuilder->execute();
579 35
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
580 35
        $rows        = $this->enrichWithMany($rows);
581 35
        $entities    = [];
582 35
        foreach ($rows as $row) {
583 34
            $entities[] = $this->hydrate($row);
584
        }
585 35
        $this->enrichWithReference($entities);
586 35
        return $entities;
587
    }
588
589
    /**
590
     * {@inheritdoc}
591
     */
592 8
    public function getIdToNameMap($entity, $nameField)
593
    {
594 8
        $nameSelect   = $nameField !== null ? ',`'.$nameField.'`' : '';
595 8
        $drivingField = $nameField ?: 'id';
596
597 8
        $entityDefinition = $this->definition->getServiceProvider()->getData($entity)->getDefinition();
598 8
        $table            = $entityDefinition->getTable();
599 8
        $queryBuilder     = $this->database->createQueryBuilder();
600
        $queryBuilder
601 8
            ->select('id'.$nameSelect)
602 8
            ->from('`'.$table.'`', 't1')
603 8
            ->orderBy($drivingField)
604
        ;
605 8
        if (!$entityDefinition->isHardDeletion()) {
606 7
            $queryBuilder->where('deleted_at IS NULL');
607
        }
608 8
        $queryResult    = $queryBuilder->execute();
609 8
        $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
610
        $result         = array_reduce($manyReferences, function(&$carry, $manyReference) use ($drivingField) {
611 7
            $carry[$manyReference['id']] = $manyReference[$drivingField];
612 7
            return $carry;
613 8
        }, []);
614 8
        return $result;
615
    }
616
617
    /**
618
     * {@inheritdoc}
619
     */
620 12
    public function countBy($table, array $params, array $paramsOperators, $excludeDeleted)
621
    {
622 12
        $queryBuilder = $this->database->createQueryBuilder();
623
        $queryBuilder
624 12
            ->select('COUNT(id)')
625 12
            ->from('`'.$table.'`', '`'.$table.'`')
626
        ;
627
628 12
        $deletedExcluder = 'where';
629 12
        $i               = 0;
630 12
        $manyFields      = [];
631 12
        foreach ($params as $name => $value) {
632 9
            if ($this->definition->getType($name) === 'many') {
633 2
                $manyFields[] = $name;
634 2
                continue;
635
            }
636
            $queryBuilder
637 9
                ->andWhere('`'.$name.'` '.$paramsOperators[$name].' ?')
638 9
                ->setParameter($i, $value, \PDO::PARAM_STR)
639
            ;
640 9
            $i++;
641 9
            $deletedExcluder = 'andWhere';
642
        }
643
644 12
        $idsToInclude = $this->getManyIds($manyFields, $params);
645 12
        if (!empty($idsToInclude)) {
646
            $queryBuilder
647 2
                ->andWhere('id IN (?)')
648 2
                ->setParameter($i, $idsToInclude, Connection::PARAM_STR_ARRAY)
649
            ;
650 2
            $deletedExcluder = 'andWhere';
651
        }
652
653 12
        if ($excludeDeleted) {
654 7
            $this->addSoftDeletionToQuery($queryBuilder, '', $deletedExcluder);
655
        }
656
657 12
        $queryResult = $queryBuilder->execute();
658 12
        $result      = $queryResult->fetch(\PDO::FETCH_NUM);
659 12
        return intval($result[0]);
660
    }
661
662
    /**
663
     * {@inheritdoc}
664
     */
665 2
    public function hasManySet($field, array $thatIds, $excludeId = null)
666
    {
667 2
        $thisField        = $this->definition->getSubTypeField($field, 'many', 'thisField');
668 2
        $thatField        = $this->definition->getSubTypeField($field, 'many', 'thatField');
669 2
        $thatEntity       = $this->definition->getSubTypeField($field, 'many', 'entity');
670 2
        $entityDefinition = $this->definition->getServiceProvider()->getData($thatEntity)->getDefinition();
671 2
        $entityTable      = $entityDefinition->getTable();
672 2
        $queryBuilder     = $this->database->createQueryBuilder();
673 2
        $queryBuilder->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that')
674 2
            ->from('`'.$field.'`', 't1')
675 2
            ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
676 2
            ->orderBy('this, that')
677
        ;
678 2
        $excludeMethod = 'where';
679 2
        if (!$entityDefinition->isHardDeletion()) {
680
            $queryBuilder->where('t2.deleted_at IS NULL');
681
            $excludeMethod = 'andWhere';
682
        }
683 2
        if ($excludeId !== null) {
684 2
            $queryBuilder->$excludeMethod('t1.`'.$thisField.'` != ?')->setParameter(0, $excludeId);
685
        }
686 2
        $existingMany = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
687 2
        $existingMap  = array_reduce($existingMany, function(&$carry, $existing) {
688 2
            $carry[$existing['this']][] = $existing['that'];
689 2
            return $carry;
690 2
        }, []);
691 2
        sort($thatIds);
692 2
        return in_array($thatIds, array_values($existingMap));
693
    }
694
695
}
696