Completed
Push — master ( 29d040...599906 )
by Neomerx
04:36
created

Crud::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
ccs 6
cts 6
cp 1
cc 1
eloc 7
nc 1
nop 2
crap 1
1
<?php namespace Limoncello\Flute\Api;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use ArrayObject;
20
use Closure;
21
use Doctrine\DBAL\Driver\PDOConnection;
22
use Doctrine\DBAL\Platforms\AbstractPlatform;
23
use Doctrine\DBAL\Query\QueryBuilder;
24
use Doctrine\DBAL\Types\Type;
25
use Generator;
26
use Limoncello\Container\Traits\HasContainerTrait;
27
use Limoncello\Contracts\Data\ModelSchemeInfoInterface;
28
use Limoncello\Contracts\Data\RelationshipTypes;
29
use Limoncello\Contracts\L10n\FormatterFactoryInterface;
30
use Limoncello\Contracts\L10n\FormatterInterface;
31
use Limoncello\Flute\Contracts\Adapters\PaginationStrategyInterface;
32
use Limoncello\Flute\Contracts\Adapters\RepositoryInterface;
33
use Limoncello\Flute\Contracts\Api\CrudInterface;
34
use Limoncello\Flute\Contracts\Api\ModelsDataInterface;
35
use Limoncello\Flute\Contracts\FactoryInterface;
36
use Limoncello\Flute\Contracts\Http\Query\IncludeParameterInterface;
37
use Limoncello\Flute\Contracts\Models\ModelStorageInterface;
38
use Limoncello\Flute\Contracts\Models\PaginatedDataInterface;
39
use Limoncello\Flute\Contracts\Models\RelationshipStorageInterface;
40
use Limoncello\Flute\Contracts\Models\TagStorageInterface;
41
use Limoncello\Flute\Exceptions\InvalidArgumentException;
42
use Limoncello\Flute\Http\Query\FilterParameterCollection;
43
use Limoncello\Flute\L10n\Messages;
44
use Neomerx\JsonApi\Contracts\Document\DocumentInterface;
45
use Neomerx\JsonApi\Exceptions\ErrorCollection;
46
use Neomerx\JsonApi\Exceptions\JsonApiException as E;
47
use Psr\Container\ContainerInterface;
48
49
/**
50
 * @package Limoncello\Flute
51
 *
52
 * @SuppressWarnings(PHPMD.TooManyMethods)
53
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
54
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
55
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
56
 */
57
class Crud implements CrudInterface
58
{
59
    use HasContainerTrait;
60
61
    /** Internal constant. Query param name. */
62
    protected const INDEX_BIND = ':index';
63
64
    /** Internal constant. Query param name. */
65
    protected const CHILD_INDEX_BIND = ':childIndex';
66
67
    /** Internal constant. Path constant. */
68
    protected const ROOT_PATH = '';
69
70
    /** Internal constant. Path constant. */
71
    protected const PATH_SEPARATOR = DocumentInterface::PATH_SEPARATOR;
72
73
    /**
74
     * @var FactoryInterface
75
     */
76
    private $factory;
77
78
    /**
79
     * @var string
80
     */
81
    private $modelClass;
82
83
    /**
84
     * @var RepositoryInterface
85
     */
86
    private $repository;
87
88
    /**
89
     * @var ModelSchemeInfoInterface
90
     */
91
    private $modelSchemes;
92
93
    /**
94
     * @var PaginationStrategyInterface
95
     */
96
    private $paginationStrategy;
97
98
    /**
99
     * @param ContainerInterface $container
100 38
     * @param string             $modelClass
101
     */
102 38
    public function __construct(ContainerInterface $container, string $modelClass)
103
    {
104 38
        $this->setContainer($container);
105 38
106 38
        $this->factory            = $this->getContainer()->get(FactoryInterface::class);
107 38
        $this->modelClass         = $modelClass;
108 38
        $this->repository         = $this->getContainer()->get(RepositoryInterface::class);
109
        $this->modelSchemes       = $this->getContainer()->get(ModelSchemeInfoInterface::class);
110
        $this->paginationStrategy = $this->getContainer()->get(PaginationStrategyInterface::class);
111
    }
112
113
    /**
114 16
     * @inheritdoc
115
     */
116
    public function index(
117
        FilterParameterCollection $filterParams = null,
118
        array $sortParams = null,
119
        array $includePaths = null,
120 16
        array $pagingParams = null
121
    ): ModelsDataInterface {
122 16
        $modelClass = $this->getModelClass();
123
124 16
        $builder = $this->getRepository()->index($modelClass);
125 16
126 16
        $errors = $this->getFactory()->createErrorCollection();
127 15
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
128
        $this->checkErrors($errors);
129 15
        $sortParams === null ?: $this->getRepository()->applySorting($builder, $modelClass, $sortParams);
130 15
131
        list($offset, $limit) = $this->getPaginationStrategy()->parseParameters($pagingParams);
132 15
        $builder->setFirstResult($offset)->setMaxResults($limit);
133
134 15
        $data = $this->fetchCollectionData($this->builderOnIndex($builder), $modelClass, $limit, $offset);
135 15
136 4
        $relationships = null;
137
        if ($data->getData() !== null && $includePaths !== null) {
138
            $relationships = $this->readRelationships($data, $includePaths);
139 15
        }
140
141 15
        $result = $this->getFactory()->createModelsData($data, $relationships);
142
143
        return $result;
144
    }
145
146
    /**
147 1
     * @inheritdoc
148
     */
149 1
    public function indexResources(FilterParameterCollection $filterParams = null, array $sortParams = null): array
150
    {
151 1
        $modelClass = $this->getModelClass();
152
153 1
        $builder = $this->getRepository()->index($modelClass);
154 1
155 1
        $errors = $this->getFactory()->createErrorCollection();
156 1
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
157
        $this->checkErrors($errors);
158 1
        $sortParams === null ?: $this->getRepository()->applySorting($builder, $modelClass, $sortParams);
159
160 1
        list($models) = $this->fetchCollection($this->builderOnIndex($builder), $modelClass);
161
162
        return $models;
163
    }
164
165
    /**
166 1
     * @inheritdoc
167
     */
168 1
    public function count(FilterParameterCollection $filterParams = null): ?int
169
    {
170 1
        $modelClass = $this->getModelClass();
171
172 1
        $builder = $this->getRepository()->count($modelClass);
173 1
174 1
        $errors = $this->getFactory()->createErrorCollection();
175
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
176 1
        $this->checkErrors($errors);
177
178 1
        $result = $this->builderOnCount($builder)->execute()->fetchColumn();
179
180
        return $result === false ? null : $result;
181
    }
182
183
    /**
184 10
     * @inheritdoc
185
     */
186
    public function read(
187
        $index,
188
        FilterParameterCollection $filterParams = null,
189 10
        array $includePaths = null
190 10
    ): ModelsDataInterface {
191
        $model = $this->readResource($index, $filterParams);
192 10
        $data  = $this->getFactory()->createPaginatedData($model);
193 10
194 4
        $relationships = null;
195
        if ($data->getData() !== null && $includePaths !== null) {
196
            $relationships = $this->readRelationships($data, $includePaths);
197 10
        }
198
199 10
        $result = $this->getFactory()->createModelsData($data, $relationships);
200
201
        return $result;
202
    }
203
204
    /**
205 10
     * @inheritdoc
206
     */
207 10
    public function readResource($index, FilterParameterCollection $filterParams = null)
208
    {
209 10
        if ($index !== null && is_scalar($index) === false) {
210 10
            throw new InvalidArgumentException(
211 10
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
212
            );
213 10
        }
214 10
215 10
        $modelClass = $this->getModelClass();
216
217 10
        $builder = $this->getRepository()
218
            ->read($modelClass, static::INDEX_BIND)
219 10
            ->setParameter(static::INDEX_BIND, $index);
220
221
        $errors = $this->getFactory()->createErrorCollection();
222
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
223
        $this->checkErrors($errors);
224
225
        $model = $this->fetchSingle($this->builderOnRead($builder), $modelClass);
226
227 4
        return $model;
228
    }
229
230
    /**
231
     * @inheritdoc
232
     *
233
     * @SuppressWarnings(PHPMD.ElseExpression)
234 4
     */
235
    public function readRelationship(
236
        $index,
237
        string $name,
238 4
        FilterParameterCollection $filterParams = null,
239
        array $sortParams = null,
240 4
        array $pagingParams = null
241 4
    ): PaginatedDataInterface {
242 4
        if ($index !== null && is_scalar($index) === false) {
243 4
            throw new InvalidArgumentException(
244
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
245 4
            );
246
        }
247 4
248 4
        $modelClass = $this->getModelClass();
249
250 4
        /** @var QueryBuilder $builder */
251 3
        list ($builder, $resultClass, $relationshipType) =
252 3
            $this->getRepository()->readRelationship($modelClass, static::INDEX_BIND, $name);
253
254 3
        $errors = $this->getFactory()->createErrorCollection();
255
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $resultClass, $filterParams);
256 1
        $this->checkErrors($errors);
257
        $sortParams === null ?: $this->getRepository()->applySorting($builder, $resultClass, $sortParams);
258
259 4
        $builder->setParameter(static::INDEX_BIND, $index);
260
261
        $isCollection = $relationshipType === RelationshipTypes::HAS_MANY ||
262
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
263
264
        if ($isCollection == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
265 4
            list($offset, $limit) = $this->getPaginationStrategy()->parseParameters($pagingParams);
266
            $builder->setFirstResult($offset)->setMaxResults($limit);
267 4
            $data = $this
268
                ->fetchCollectionData($this->builderOnReadRelationship($builder), $resultClass, $limit, $offset);
269
        } else {
270
            $data = $this->fetchSingleData($this->builderOnReadRelationship($builder), $resultClass);
271 4
        }
272
273 4
        return $data;
274 4
    }
275
276 4
    /**
277
     * @inheritdoc
278 4
     */
279
    public function hasInRelationship($parentId, string $name, $childId): bool
280
    {
281
        if ($parentId !== null && is_scalar($parentId) === false) {
282
            throw new InvalidArgumentException(
283
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
284 1
            );
285
        }
286 1
        if ($childId !== null && is_scalar($childId) === false) {
287 1
            throw new InvalidArgumentException(
288 1
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
289 1
            );
290 1
        }
291
292 1
        $modelClass = $this->getModelClass();
293
294
        /** @var QueryBuilder $builder */
295
        list ($builder) = $this->getRepository()
296
            ->hasInRelationship($modelClass, static::INDEX_BIND, $name, static::CHILD_INDEX_BIND);
297
298 5
        $builder->setParameter(static::INDEX_BIND, $parentId);
299
        $builder->setParameter(static::CHILD_INDEX_BIND, $childId);
300 5
301
        $result = $builder->execute()->fetch();
302 5
303 5
        return $result !== false;
304
    }
305
306 5
    /**
307
     * @inheritdoc
308 4
     */
309
    public function readRow($index): ?array
310
    {
311
        if ($index !== null && is_scalar($index) === false) {
312
            throw new InvalidArgumentException(
313
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
314 4
            );
315
        }
316 4
317
        $modelClass = $this->getModelClass();
318 4
        $builder    = $this->getRepository()
319
            ->read($modelClass, static::INDEX_BIND)
320 4
            ->setParameter(static::INDEX_BIND, $index);
321 4
        $typedRow   = $this->fetchRow($builder, $modelClass);
322 4
323 4
        return $typedRow;
324 4
    }
325
326 4
    /**
327 4
     * @inheritdoc
328 2
     */
329 2
    public function delete($index): int
330 2
    {
331 2
        if ($index !== null && is_scalar($index) === false) {
332 2
            throw new InvalidArgumentException(
333 2
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
334 2
            );
335 2
        }
336
337
        $modelClass = $this->getModelClass();
338 4
339
        $builder = $this->builderOnDelete(
340 4
            $this->getRepository()->delete($modelClass, static::INDEX_BIND)->setParameter(static::INDEX_BIND, $index)
341
        );
342
343
        $deleted = $builder->execute();
344
345
        return (int)$deleted;
346 3
    }
347
348 3
    /**
349 3
     * @inheritdoc
350
     */
351 3
    public function create($index, array $attributes, array $toMany = []): string
352
    {
353 3
        if ($index !== null && is_scalar($index) === false) {
354 3
            throw new InvalidArgumentException(
355 3
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
356 3
            );
357 3
        }
358 3
359 2
        $modelClass = $this->getModelClass();
360 2
361
        $allowedChanges = $this->filterAttributesOnCreate($modelClass, $attributes, $index);
362 2
363 2
        $saveMain = $this->getRepository()->create($modelClass, $allowedChanges);
364 2
        $saveMain = $this->builderSaveResourceOnCreate($saveMain);
365
        $saveMain->getSQL(); // prepare
366 2
        $this->inTransaction(function () use ($modelClass, $saveMain, $toMany, &$index) {
367 2
            $saveMain->execute();
368 2
            // if no index given will use last insert ID as index
369 2
            $index !== null ?: $index = $saveMain->getConnection()->lastInsertId();
370 2
            foreach ($toMany as $name => $values) {
371 2
                $indexBind      = ':index';
372
                $otherIndexBind = ':otherIndex';
373
                $saveToMany     = $this->getRepository()
374 3
                    ->createToManyRelationship($modelClass, $indexBind, $name, $otherIndexBind);
375
                $saveToMany     = $this->builderSaveRelationshipOnCreate($name, $saveToMany);
376 3
                $saveToMany->setParameter($indexBind, $index);
377
                foreach ($values as $value) {
378
                    $saveToMany->setParameter($otherIndexBind, $value)->execute();
379
                }
380
            }
381
        });
382 38
383
        return $index;
384 38
    }
385
386
    /**
387
     * @inheritdoc
388
     */
389
    public function update($index, array $attributes, array $toMany = []): int
390 32
    {
391
        if ($index !== null && is_scalar($index) === false) {
392 32
            throw new InvalidArgumentException(
393
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
394
            );
395
        }
396
397
        $updated    = 0;
398 38
        $modelClass = $this->getModelClass();
399
400 38
        $allowedChanges = $this->filterAttributesOnUpdate($modelClass, $attributes);
401
402
        $saveMain = $this->getRepository()->update($modelClass, $index, $allowedChanges);
403
        $saveMain = $this->builderSaveResourceOnUpdate($saveMain);
404
        $saveMain->getSQL(); // prepare
405
        $this->inTransaction(function () use ($modelClass, $saveMain, $toMany, $index, &$updated) {
406 38
            $updated = $saveMain->execute();
407
            foreach ($toMany as $name => $values) {
408 38
                $indexBind      = ':index';
409
                $otherIndexBind = ':otherIndex';
410
411
                $cleanToMany = $this->getRepository()->cleanToManyRelationship($modelClass, $indexBind, $name);
412
                $cleanToMany = $this->builderCleanRelationshipOnUpdate($name, $cleanToMany);
413
                $cleanToMany->setParameter($indexBind, $index)->execute();
414 31
415
                $saveToMany = $this->getRepository()
416 31
                    ->createToManyRelationship($modelClass, $indexBind, $name, $otherIndexBind);
417
                $saveToMany = $this->builderSaveRelationshipOnUpdate($name, $saveToMany);
418
                $saveToMany->setParameter($indexBind, $index);
419
                foreach ($values as $value) {
420
                    $updated += (int)$saveToMany->setParameter($otherIndexBind, $value)->execute();
421
                }
422 21
            }
423
        });
424 21
425
        return (int)$updated;
426
    }
427
428
    /**
429
     * @return FactoryInterface
430
     */
431
    protected function getFactory(): FactoryInterface
432 7
    {
433
        return $this->factory;
434 7
    }
435 7
436
    /**
437 7
     * @return string
438 7
     */
439 7
    protected function getModelClass(): string
440
    {
441
        return $this->modelClass;
442
    }
443
444
    /**
445
     * @return RepositoryInterface
446
     */
447
    protected function getRepository(): RepositoryInterface
448
    {
449 1
        return $this->repository;
450
    }
451 1
452 1
    /**
453
     * @return ModelSchemeInfoInterface
454 1
     */
455
    protected function getModelSchemes(): ModelSchemeInfoInterface
456
    {
457
        return $this->modelSchemes;
458
    }
459
460
    /**
461
     * @return PaginationStrategyInterface
462
     */
463
    protected function getPaginationStrategy(): PaginationStrategyInterface
464
    {
465 18
        return $this->paginationStrategy;
466
    }
467
468
    /**
469
     * @param Closure $closure
470
     *
471 18
     * @return void
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use NoType.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
472
     */
473 18
    protected function inTransaction(Closure $closure): void
474 18
    {
475 18
        $connection = $this->getRepository()->getConnection();
476 18
        $connection->beginTransaction();
477 18
        try {
478 18
            $isOk = ($closure() === false ? null : true);
479
        } finally {
480 18
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
481
        }
482
    }
483
484
    /**
485
     * @param QueryBuilder $builder
486
     * @param string       $class
487
     *
488
     * @return PaginatedDataInterface
489 1
     */
490
    protected function fetchSingleData(QueryBuilder $builder, string $class): PaginatedDataInterface
491 1
    {
492 1
        $model = $this->fetchSingle($builder, $class);
493 1
        $data  = $this->getFactory()->createPaginatedData($model)->markAsSingleItem();
494 1
495
        return $data;
496 1
    }
497 1
498 1
    /**
499
     * @param QueryBuilder $builder
500
     * @param string       $class
501 1
     * @param int          $limit
502
     * @param int          $offset
503
     *
504
     * @return PaginatedDataInterface
505
     */
506
    protected function fetchCollectionData(
507
        QueryBuilder $builder,
508
        string $class,
509
        int $limit,
510 14
        int $offset
511
    ): PaginatedDataInterface {
512 14
        list($models, $hasMore, $limit, $offset) = $this->fetchCollection($builder, $class, $limit, $offset);
513 14
514 14
        $data = $this->getFactory()
515 14
            ->createPaginatedData($models)
516
            ->markAsCollection()
517 14
            ->setOffset($offset)
518 14
            ->setLimit($limit);
519 14
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
520
521
        return $data;
522 14
    }
523
524
    /**
525
     * @param QueryBuilder $builder
526
     * @param string       $class
527
     *
528
     * @return array|null
529
     */
530
    protected function fetchRow(QueryBuilder $builder, string $class): ?array
531
    {
532
        $statement = $builder->execute();
533 22
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
534
        $platform  = $builder->getConnection()->getDatabasePlatform();
535 22
        $typeNames = $this->getModelSchemes()->getAttributeTypes($class);
536 22
537 22
        $model = null;
538 22
        if (($attributes = $statement->fetch()) !== false) {
539
            $model = $this->readRowFromAssoc($attributes, $typeNames, $platform);
540 22
        }
541 22
542 22
        return $model;
543
    }
544
545 22
    /**
546
     * @param QueryBuilder $builder
547
     * @param string       $class
548
     *
549
     * @return mixed|null
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object|null.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
550
     */
551
    protected function fetchSingle(QueryBuilder $builder, string $class)
552
    {
553
        $statement = $builder->execute();
554
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
555 4
        $platform = $builder->getConnection()->getDatabasePlatform();
556
        $typeNames = $this->getModelSchemes()->getAttributeTypes($class);
557 4
558 4
        $model = null;
559 4
        if (($attributes = $statement->fetch()) !== false) {
560 1
            $model = $this->readInstanceFromAssoc($class, $attributes, $typeNames, $platform);
561 1
        }
562
563
        return $model;
564 4
    }
565
566
    /**
567
     * @param QueryBuilder    $builder
568
     * @param string          $class
569
     * @param int|string|null $offset
570
     * @param int|string|null $limit
571
     *
572
     * @return array
573 3
     */
574
    protected function fetchCollection(QueryBuilder $builder, string $class, $limit = null, $offset = null): array
575 3
    {
576 3
        $statement = $builder->execute();
577
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
578 3
        $platform = $builder->getConnection()->getDatabasePlatform();
579
        $typeNames = $this->getModelSchemes()->getAttributeTypes($class);
580
581
        $models = [];
582
        while (($attributes = $statement->fetch()) !== false) {
583
            $models[] = $this->readInstanceFromAssoc($class, $attributes, $typeNames, $platform);
584
        }
585
586 1
        return $this->normalizePagingParams($models, $limit, $offset);
587
    }
588 1
589
    /**
590
     * @param string      $modelClass
591
     * @param array       $attributes
592
     * @param null|string $index
593
     *
594
     * @return array
595
     */
596 16
    protected function filterAttributesOnCreate(string $modelClass, array $attributes, string $index = null): array
597
    {
598 16
        $allowedAttributes = array_flip($this->getModelSchemes()->getAttributes($modelClass));
599
        $allowedChanges    = array_intersect_key($attributes, $allowedAttributes);
600
        if ($index !== null) {
601
            $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
602
            $allowedChanges[$pkName] = $index;
603
        }
604
605
        return $allowedChanges;
606 10
    }
607
608 10
    /**
609
     * @param string $modelClass
610
     * @param array  $attributes
611
     *
612
     * @return array
613
     */
614
    protected function filterAttributesOnUpdate(string $modelClass, array $attributes): array
615
    {
616 4
        $allowedAttributes = array_flip($this->getModelSchemes()->getAttributes($modelClass));
617
        $allowedChanges    = array_intersect_key($attributes, $allowedAttributes);
618 4
619
        return $allowedChanges;
620
    }
621
622
    /**
623
     * @param QueryBuilder $builder
624
     *
625
     * @return QueryBuilder
626 4
     */
627
    protected function builderOnCount(QueryBuilder $builder): QueryBuilder
628 4
    {
629
        return $builder;
630
    }
631
632
    /**
633
     * @param QueryBuilder $builder
634
     *
635
     * @return QueryBuilder
636 3
     */
637
    protected function builderOnIndex(QueryBuilder $builder): QueryBuilder
638 3
    {
639
        return $builder;
640
    }
641
642
    /**
643
     * @param QueryBuilder $builder
644
     *
645
     * @return QueryBuilder
646
     */
647
    protected function builderOnRead(QueryBuilder $builder): QueryBuilder
648
    {
649 2
        return $builder;
650
    }
651
652
    /**
653 2
     * @param QueryBuilder $builder
654
     *
655
     * @return QueryBuilder
656
     */
657
    protected function builderOnReadRelationship(QueryBuilder $builder): QueryBuilder
658
    {
659
        return $builder;
660
    }
661
662
    /**
663
     * @param QueryBuilder $builder
664 2
     *
665
     * @return QueryBuilder
666
     */
667
    protected function builderSaveResourceOnCreate(QueryBuilder $builder): QueryBuilder
668 2
    {
669
        return $builder;
670
    }
671
672
    /**
673
     * @param QueryBuilder $builder
674
     *
675
     * @return QueryBuilder
676
     */
677
    protected function builderSaveResourceOnUpdate(QueryBuilder $builder): QueryBuilder
678
    {
679 2
        return $builder;
680
    }
681
682
    /**
683 2
     * @param string       $relationshipName
684
     * @param QueryBuilder $builder
685
     *
686
     * @return QueryBuilder
687
     *
688
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
689
     */
690
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
691 5
        $relationshipName,
692
        QueryBuilder $builder
693 5
    ): QueryBuilder {
694
        return $builder;
695
    }
696
697
    /**
698
     * @param string       $relationshipName
699
     * @param QueryBuilder $builder
700
     *
701
     * @return QueryBuilder
702
     *
703
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
704 8
     */
705
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
706 8
        $relationshipName,
707
        QueryBuilder $builder
708 8
    ): QueryBuilder {
709 8
        return $builder;
710 8
    }
711
712
    /**
713 8
     * @param string       $relationshipName
714
     * @param QueryBuilder $builder
715 8
     *
716 8
     * @return QueryBuilder
717 4
     *
718 4
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
719 4
     */
720 4
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
721
        $relationshipName,
722
        QueryBuilder $builder
723
    ): QueryBuilder {
724 4
        return $builder;
725 4
    }
726 4
727 4
    /**
728
     * @param QueryBuilder $builder
729
     *
730 8
     * @return QueryBuilder
731
     */
732 8
    protected function builderOnDelete(QueryBuilder $builder): QueryBuilder
733 8
    {
734 8
        return $builder;
735 8
    }
736 8
737 8
    /**
738 8
     * @param PaginatedDataInterface      $data
739 8
     * @param IncludeParameterInterface[] $paths
740
     *
741
     * @return RelationshipStorageInterface
742
     *
743
     * @SuppressWarnings(PHPMD.ElseExpression)
744 8
     */
745
    protected function readRelationships(PaginatedDataInterface $data, array $paths): RelationshipStorageInterface
746
    {
747
        $result = $this->getFactory()->createRelationshipStorage();
748
749
        if (empty($data->getData()) === false && empty($paths) === false) {
750
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemes());
751
            $modelsAtPath = $this->getFactory()->createTagStorage();
752
753
            // we gonna send this storage via function params so it is an equivalent for &array
754
            $classAtPath = new ArrayObject();
755
756 22
            $model = null;
757
            if ($data->isCollection() === true) {
758 22
                foreach ($data->getData() as $model) {
759 21
                    $uniqueModel = $modelStorage->register($model);
760 21
                    if ($uniqueModel !== null) {
761 21
                        $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
762 21
                    }
763
                }
764 1
            } else {
765
                $model       = $data->getData();
766
                $uniqueModel = $modelStorage->register($model);
767 22
                if ($uniqueModel !== null) {
768
                    $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
769
                }
770
            }
771
            $classAtPath[static::ROOT_PATH] = get_class($model);
772
773
            foreach ($this->getPaths($paths) as list ($parentPath, $childPaths)) {
774
                $this->loadRelationshipsLayer(
775 32
                    $result,
776
                    $modelsAtPath,
777 32
                    $classAtPath,
778 1
                    $modelStorage,
779
                    $parentPath,
780
                    $childPaths
781
                );
782
            }
783
        }
784
785
        return $result;
786
    }
787 8
788
    /**
789
     * @param array           $models
790
     * @param int|string|null $offset
791
     * @param int|string|null $limit
792
     *
793 8
     * @return array
794 8
     *
795 8
     * @SuppressWarnings(PHPMD.ElseExpression)
796 8
     */
797 8
    private function normalizePagingParams(array $models, $limit, $offset): array
798 8
    {
799 8
        if ($limit !== null) {
800 8
            $hasMore = count($models) >= $limit;
801 8
            $limit   = $hasMore === true ? $limit - 1 : null;
802 8
            $offset  = $limit === null && $hasMore === false ? null : $offset;
803 8
            $hasMore === false ?: array_pop($models);
804
        } else {
805
            $hasMore = false;
806
        }
807
808
        return [$models, $hasMore, $limit, $offset];
809 8
    }
810 8
811 8
    /**
812
     * @param ErrorCollection $errors
813
     *
814
     * @return void
815 8
     */
816 8
    private function checkErrors(ErrorCollection $errors): void
817 8
    {
818 8
        if (empty($errors->getArrayCopy()) === false) {
819 8
            throw new E($errors);
820
        }
821
    }
822
823
    /**
824
     * @param IncludeParameterInterface[] $paths
825
     *
826
     * @return Generator
827
     */
828
    private function getPaths(array $paths): Generator
829
    {
830
        // The idea is to normalize paths. It means build all intermediate paths.
831
        // e.g. if only `a.b.c` path it given it will be normalized to `a`, `a.b` and `a.b.c`.
832
        // Path depths store depth of each path (e.g. 0 for root, 1 for `a`, 2 for `a.b` and etc).
833
        // It is needed for yielding them in correct order (from top level to bottom).
834
        $normalizedPaths = [];
835 8
        $pathsDepths     = [];
836
        foreach ($paths as $path) {
837
            $parentDepth = 0;
838
            $tmpPath     = static::ROOT_PATH;
839
            foreach ($path->getPath() as $pathPiece) {
840
                $parent                    = $tmpPath;
841
                $tmpPath                   = empty($tmpPath) === true ?
842
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
843 8
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
844 8
                $pathsDepths[$parent]      = $parentDepth++;
845 8
            }
846
        }
847
848
        // Here we collect paths in form of parent => [list of children]
849
        // e.g. '' => ['a', 'c', 'b'], 'b' => ['bb', 'aa'] and etc
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
850
        $parentWithChildren = [];
851 8
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
852 8
            $parentWithChildren[$parent][] = $childPath;
853
        }
854
855
        // And finally sort by path depth and yield parent with its children. Top level paths first then deeper ones.
856 8
        asort($pathsDepths, SORT_NUMERIC);
857
        foreach ($pathsDepths as $parent => $depth) {
858 8
            $depth ?: null; // suppress unused
859
            $childPaths = $parentWithChildren[$parent];
860
            yield [$parent, $childPaths];
861 8
        }
862 7
    }
863 7
864 7
    /**
865 7
     * @param RelationshipStorageInterface $result
866 7
     * @param TagStorageInterface          $modelsAtPath
867 6
     * @param ArrayObject                  $classAtPath
868
     * @param ModelStorageInterface        $deDup
869 7
     * @param string                       $parentsPath
870
     * @param array                        $childRelationships
871 7
     *
872 6
     * @return void
873 4
     *
874 6
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
875 6
     */
876 6
    protected function loadRelationshipsLayer(
877 6
        RelationshipStorageInterface $result,
878 6
        TagStorageInterface $modelsAtPath,
879 6
        ArrayObject $classAtPath,
880
        ModelStorageInterface $deDup,
881 6
        string $parentsPath,
882 6
        array $childRelationships
883 6
    ): void {
884 6
        $rootClass   = $classAtPath[static::ROOT_PATH];
885 6
        $parentClass = $classAtPath[$parentsPath];
886 6
        $parents     = $modelsAtPath->get($parentsPath);
887 6
888
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
889
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
890 6
        // $models.
891
892 8
        foreach ($childRelationships as $name) {
893
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
894
895
            /** @var QueryBuilder $builder */
896
            list ($builder, $class, $relationshipType) =
897
                $this->getRepository()->readRelationship($parentClass, static::INDEX_BIND, $name);
898
899
            $classAtPath[$childrenPath] = $class;
900
901
            switch ($relationshipType) {
902
                case RelationshipTypes::BELONGS_TO:
903
                    $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
904
                    foreach ($parents as $parent) {
905
                        $builder->setParameter(static::INDEX_BIND, $parent->{$pkName});
906
                        $child = $deDup->register($this->fetchSingle($builder, $class));
907 30
                        if ($child !== null) {
908
                            $modelsAtPath->register($child, $childrenPath);
909
                        }
910
                        $result->addToOneRelationship($parent, $name, $child);
911
                    }
912
                    break;
913 30
                case RelationshipTypes::HAS_MANY:
914 30
                case RelationshipTypes::BELONGS_TO_MANY:
915 30
                    list ($queryOffset, $queryLimit) = $this->getPaginationStrategy()
916 30
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
917 30
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit);
918
                    $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
919 30
                    foreach ($parents as $parent) {
920
                        $builder->setParameter(static::INDEX_BIND, $parent->{$pkName});
921
                        list($children, $hasMore, $limit, $offset) =
922 30
                            $this->fetchCollection($builder, $class, $queryLimit, $queryOffset);
923
                        $deDupedChildren = [];
924
                        foreach ($children as $child) {
925
                            $child = $deDup->register($child);
926
                            $modelsAtPath->register($child, $childrenPath);
927
                            if ($child !== null) {
928
                                $deDupedChildren[] = $child;
929
                            }
930
                        }
931
                        $result->addToManyRelationship($parent, $name, $deDupedChildren, $hasMore, $offset, $limit);
932
                    }
933
                    break;
934 1
            }
935
        }
936 1
    }
937 1
938 1
    /**
939 1
     * @param string             $namespace
940 1
     *
941
     * @return FormatterInterface
942 1
     */
943
    protected function createMessageFormatter(string $namespace = Messages::RESOURCES_NAMESPACE): FormatterInterface
944
    {
945 1
        /** @var FormatterFactoryInterface $factory */
946
        $factory          = $this->getContainer()->get(FormatterFactoryInterface::class);
947
        $messageFormatter = $factory->createFormatter($namespace);
948
949
        return $messageFormatter;
950
    }
951
952
    /**
953
     * @param string           $class
954
     * @param array            $attributes
955
     * @param Type[]           $typeNames
956
     * @param AbstractPlatform $platform
957
     *
958
     * @return mixed|null
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
959
     *
960
     * @SuppressWarnings(PHPMD.StaticAccess)
961
     */
962
    private function readInstanceFromAssoc(
963
        string $class,
964
        array $attributes,
965
        array $typeNames,
966
        AbstractPlatform $platform
967
    ) {
968
        $instance = new $class();
969
        foreach ($attributes as $name => $value) {
970
            if (array_key_exists($name, $typeNames) === true) {
971
                $type  = Type::getType($typeNames[$name]);
972
                $value = $type->convertToPHPValue($value, $platform);
973
            }
974
            $instance->{$name} = $value;
975
        }
976
977
        return $instance;
978
    }
979
980
    /**
981
     * @param array            $attributes
982
     * @param Type[]           $typeNames
983
     * @param AbstractPlatform $platform
984
     *
985
     * @return array
986
     *
987
     * @SuppressWarnings(PHPMD.StaticAccess)
988
     */
989
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
990
    {
991
        $row = [];
992
        foreach ($attributes as $name => $value) {
993
            if (array_key_exists($name, $typeNames) === true) {
994
                $type  = Type::getType($typeNames[$name]);
995
                $value = $type->convertToPHPValue($value, $platform);
996
            }
997
            $row[$name] = $value;
998
        }
999
1000
        return $row;
1001
    }
1002
}
1003