Completed
Push — master ( 599906...c694a4 )
by Neomerx
02:11
created

Crud::getModelSchemes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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
     * @param string             $modelClass
101
     */
102 46
    public function __construct(ContainerInterface $container, string $modelClass)
103
    {
104 46
        $this->setContainer($container);
105
106 46
        $this->factory            = $this->getContainer()->get(FactoryInterface::class);
107 46
        $this->modelClass         = $modelClass;
108 46
        $this->repository         = $this->getContainer()->get(RepositoryInterface::class);
109 46
        $this->modelSchemes       = $this->getContainer()->get(ModelSchemeInfoInterface::class);
110 46
        $this->paginationStrategy = $this->getContainer()->get(PaginationStrategyInterface::class);
111
    }
112
113
    /**
114
     * @inheritdoc
115
     */
116 16
    public function index(
117
        FilterParameterCollection $filterParams = null,
118
        array $sortParams = null,
119
        array $includePaths = null,
120
        array $pagingParams = null
121
    ): ModelsDataInterface {
122 16
        $modelClass = $this->getModelClass();
123
124 16
        $builder = $this->getRepository()->index($modelClass);
125
126 16
        $errors = $this->getFactory()->createErrorCollection();
127 16
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
128 16
        $this->checkErrors($errors);
129 15
        $sortParams === null ?: $this->getRepository()->applySorting($builder, $modelClass, $sortParams);
130
131 15
        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
136 15
        $relationships = null;
137 15
        if ($data->getData() !== null && $includePaths !== null) {
138 4
            $relationships = $this->readRelationships($data, $includePaths);
139
        }
140
141 15
        $result = $this->getFactory()->createModelsData($data, $relationships);
142
143 15
        return $result;
144
    }
145
146
    /**
147
     * @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
155 1
        $errors = $this->getFactory()->createErrorCollection();
156 1
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
157 1
        $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 1
        return $models;
163
    }
164
165
    /**
166
     * @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
174 1
        $errors = $this->getFactory()->createErrorCollection();
175 1
        $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 1
        return $result === false ? null : $result;
181
    }
182
183
    /**
184
     * @inheritdoc
185
     */
186 11
    public function read(
187
        $index,
188
        FilterParameterCollection $filterParams = null,
189
        array $includePaths = null
190
    ): ModelsDataInterface {
191 11
        $model = $this->readResource($index, $filterParams);
192 10
        $data  = $this->getFactory()->createPaginatedData($model);
193
194 10
        $relationships = null;
195 10
        if ($data->getData() !== null && $includePaths !== null) {
196 4
            $relationships = $this->readRelationships($data, $includePaths);
197
        }
198
199 10
        $result = $this->getFactory()->createModelsData($data, $relationships);
200
201 10
        return $result;
202
    }
203
204
    /**
205
     * @inheritdoc
206
     */
207 11
    public function readResource($index, FilterParameterCollection $filterParams = null)
208
    {
209 11
        if ($index !== null && is_scalar($index) === false) {
210 1
            throw new InvalidArgumentException(
211 1
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
212
            );
213
        }
214
215 10
        $modelClass = $this->getModelClass();
216
217 10
        $builder = $this->getRepository()
218 10
            ->read($modelClass, static::INDEX_BIND)
219 10
            ->setParameter(static::INDEX_BIND, $index);
220
221 10
        $errors = $this->getFactory()->createErrorCollection();
222 10
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
223 10
        $this->checkErrors($errors);
224
225 10
        $model = $this->fetchSingle($this->builderOnRead($builder), $modelClass);
226
227 10
        return $model;
228
    }
229
230
    /**
231
     * @inheritdoc
232
     *
233
     * @SuppressWarnings(PHPMD.ElseExpression)
234
     */
235 5
    public function readRelationship(
236
        $index,
237
        string $name,
238
        FilterParameterCollection $filterParams = null,
239
        array $sortParams = null,
240
        array $pagingParams = null
241
    ): PaginatedDataInterface {
242 5
        if ($index !== null && is_scalar($index) === false) {
243 1
            throw new InvalidArgumentException(
244 1
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
245
            );
246
        }
247
248 4
        $modelClass = $this->getModelClass();
249
250
        /** @var QueryBuilder $builder */
251
        list ($builder, $resultClass, $relationshipType) =
252 4
            $this->getRepository()->readRelationship($modelClass, static::INDEX_BIND, $name);
253
254 4
        $errors = $this->getFactory()->createErrorCollection();
255 4
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $resultClass, $filterParams);
256 4
        $this->checkErrors($errors);
257 4
        $sortParams === null ?: $this->getRepository()->applySorting($builder, $resultClass, $sortParams);
258
259 4
        $builder->setParameter(static::INDEX_BIND, $index);
260
261 4
        $isCollection = $relationshipType === RelationshipTypes::HAS_MANY ||
262 4
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
263
264 4
        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 3
            list($offset, $limit) = $this->getPaginationStrategy()->parseParameters($pagingParams);
266 3
            $builder->setFirstResult($offset)->setMaxResults($limit);
267
            $data = $this
268 3
                ->fetchCollectionData($this->builderOnReadRelationship($builder), $resultClass, $limit, $offset);
269
        } else {
270 1
            $data = $this->fetchSingleData($this->builderOnReadRelationship($builder), $resultClass);
271
        }
272
273 4
        return $data;
274
    }
275
276
    /**
277
     * @inheritdoc
278
     */
279 6
    public function hasInRelationship($parentId, string $name, $childId): bool
280
    {
281 6
        if ($parentId !== null && is_scalar($parentId) === false) {
282 1
            throw new InvalidArgumentException(
283 1
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
284
            );
285
        }
286 5
        if ($childId !== null && is_scalar($childId) === false) {
287 1
            throw new InvalidArgumentException(
288 1
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
289
            );
290
        }
291
292 4
        $modelClass = $this->getModelClass();
293
294
        /** @var QueryBuilder $builder */
295 4
        list ($builder) = $this->getRepository()
296 4
            ->hasInRelationship($modelClass, static::INDEX_BIND, $name, static::CHILD_INDEX_BIND);
297
298 4
        $builder->setParameter(static::INDEX_BIND, $parentId);
299 4
        $builder->setParameter(static::CHILD_INDEX_BIND, $childId);
300
301 4
        $result = $builder->execute()->fetch();
302
303 4
        return $result !== false;
304
    }
305
306
    /**
307
     * @inheritdoc
308
     */
309 2
    public function readRow($index): ?array
310
    {
311 2
        if ($index !== null && is_scalar($index) === false) {
312 1
            throw new InvalidArgumentException(
313 1
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
314
            );
315
        }
316
317 1
        $modelClass = $this->getModelClass();
318 1
        $builder    = $this->getRepository()
319 1
            ->read($modelClass, static::INDEX_BIND)
320 1
            ->setParameter(static::INDEX_BIND, $index);
321 1
        $typedRow   = $this->fetchRow($builder, $modelClass);
322
323 1
        return $typedRow;
324
    }
325
326
    /**
327
     * @inheritdoc
328
     */
329 6
    public function delete($index): int
330
    {
331 6
        if ($index !== null && is_scalar($index) === false) {
332 1
            throw new InvalidArgumentException(
333 1
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
334
            );
335
        }
336
337 5
        $modelClass = $this->getModelClass();
338
339 5
        $builder = $this->builderOnDelete(
340 5
            $this->getRepository()->delete($modelClass, static::INDEX_BIND)->setParameter(static::INDEX_BIND, $index)
341
        );
342
343 5
        $deleted = $builder->execute();
344
345 4
        return (int)$deleted;
346
    }
347
348
    /**
349
     * @inheritdoc
350
     */
351 5
    public function create($index, array $attributes, array $toMany = []): string
352
    {
353 5
        if ($index !== null && is_scalar($index) === false) {
354 1
            throw new InvalidArgumentException(
355 1
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
356
            );
357
        }
358
359 4
        $modelClass = $this->getModelClass();
360
361 4
        $allowedChanges = $this->filterAttributesOnCreate($modelClass, $attributes, $index);
362
363 4
        $saveMain = $this->getRepository()->create($modelClass, $allowedChanges);
364 4
        $saveMain = $this->builderSaveResourceOnCreate($saveMain);
365 4
        $saveMain->getSQL(); // prepare
366 4
        $this->inTransaction(function () use ($modelClass, $saveMain, $toMany, &$index) {
367 4
            $saveMain->execute();
368
            // if no index given will use last insert ID as index
369 4
            $index !== null ?: $index = $saveMain->getConnection()->lastInsertId();
370 4
            foreach ($toMany as $name => $values) {
371 2
                $indexBind      = ':index';
372 2
                $otherIndexBind = ':otherIndex';
373 2
                $saveToMany     = $this->getRepository()
374 2
                    ->createToManyRelationship($modelClass, $indexBind, $name, $otherIndexBind);
375 2
                $saveToMany     = $this->builderSaveRelationshipOnCreate($name, $saveToMany);
376 2
                $saveToMany->setParameter($indexBind, $index);
377 2
                foreach ($values as $value) {
378 2
                    $saveToMany->setParameter($otherIndexBind, $value)->execute();
379
                }
380
            }
381 4
        });
382
383 4
        return $index;
384
    }
385
386
    /**
387
     * @inheritdoc
388
     */
389 4
    public function update($index, array $attributes, array $toMany = []): int
390
    {
391 4
        if ($index !== null && is_scalar($index) === false) {
392 1
            throw new InvalidArgumentException(
393 1
                $this->createMessageFormatter()->formatMessage(Messages::MSG_ERR_INVALID_ARGUMENT)
394
            );
395
        }
396
397 3
        $updated    = 0;
398 3
        $modelClass = $this->getModelClass();
399
400 3
        $allowedChanges = $this->filterAttributesOnUpdate($modelClass, $attributes);
401
402 3
        $saveMain = $this->getRepository()->update($modelClass, $index, $allowedChanges);
403 3
        $saveMain = $this->builderSaveResourceOnUpdate($saveMain);
404 3
        $saveMain->getSQL(); // prepare
405 3
        $this->inTransaction(function () use ($modelClass, $saveMain, $toMany, $index, &$updated) {
406 3
            $updated = $saveMain->execute();
407 3
            foreach ($toMany as $name => $values) {
408 2
                $indexBind      = ':index';
409 2
                $otherIndexBind = ':otherIndex';
410
411 2
                $cleanToMany = $this->getRepository()->cleanToManyRelationship($modelClass, $indexBind, $name);
412 2
                $cleanToMany = $this->builderCleanRelationshipOnUpdate($name, $cleanToMany);
413 2
                $cleanToMany->setParameter($indexBind, $index)->execute();
414
415 2
                $saveToMany = $this->getRepository()
416 2
                    ->createToManyRelationship($modelClass, $indexBind, $name, $otherIndexBind);
417 2
                $saveToMany = $this->builderSaveRelationshipOnUpdate($name, $saveToMany);
418 2
                $saveToMany->setParameter($indexBind, $index);
419 2
                foreach ($values as $value) {
420 2
                    $updated += (int)$saveToMany->setParameter($otherIndexBind, $value)->execute();
421
                }
422
            }
423 3
        });
424
425 3
        return (int)$updated;
426
    }
427
428
    /**
429
     * @return FactoryInterface
430
     */
431 32
    protected function getFactory(): FactoryInterface
432
    {
433 32
        return $this->factory;
434
    }
435
436
    /**
437
     * @return string
438
     */
439 38
    protected function getModelClass(): string
440
    {
441 38
        return $this->modelClass;
442
    }
443
444
    /**
445
     * @return RepositoryInterface
446
     */
447 38
    protected function getRepository(): RepositoryInterface
448
    {
449 38
        return $this->repository;
450
    }
451
452
    /**
453
     * @return ModelSchemeInfoInterface
454
     */
455 31
    protected function getModelSchemes(): ModelSchemeInfoInterface
456
    {
457 31
        return $this->modelSchemes;
458
    }
459
460
    /**
461
     * @return PaginationStrategyInterface
462
     */
463 21
    protected function getPaginationStrategy(): PaginationStrategyInterface
464
    {
465 21
        return $this->paginationStrategy;
466
    }
467
468
    /**
469
     * @param Closure $closure
470
     *
471
     * @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 7
    protected function inTransaction(Closure $closure): void
474
    {
475 7
        $connection = $this->getRepository()->getConnection();
476 7
        $connection->beginTransaction();
477
        try {
478 7
            $isOk = ($closure() === false ? null : true);
479 7
        } finally {
480 7
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
481
        }
482
    }
483
484
    /**
485
     * @param QueryBuilder $builder
486
     * @param string       $class
487
     *
488
     * @return PaginatedDataInterface
489
     */
490 1
    protected function fetchSingleData(QueryBuilder $builder, string $class): PaginatedDataInterface
491
    {
492 1
        $model = $this->fetchSingle($builder, $class);
493 1
        $data  = $this->getFactory()->createPaginatedData($model)->markAsSingleItem();
494
495 1
        return $data;
496
    }
497
498
    /**
499
     * @param QueryBuilder $builder
500
     * @param string       $class
501
     * @param int          $limit
502
     * @param int          $offset
503
     *
504
     * @return PaginatedDataInterface
505
     */
506 18
    protected function fetchCollectionData(
507
        QueryBuilder $builder,
508
        string $class,
509
        int $limit,
510
        int $offset
511
    ): PaginatedDataInterface {
512 18
        list($models, $hasMore, $limit, $offset) = $this->fetchCollection($builder, $class, $limit, $offset);
513
514 18
        $data = $this->getFactory()
515 18
            ->createPaginatedData($models)
516 18
            ->markAsCollection()
517 18
            ->setOffset($offset)
518 18
            ->setLimit($limit);
519 18
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
520
521 18
        return $data;
522
    }
523
524
    /**
525
     * @param QueryBuilder $builder
526
     * @param string       $class
527
     *
528
     * @return array|null
529
     */
530 1
    protected function fetchRow(QueryBuilder $builder, string $class): ?array
531
    {
532 1
        $statement = $builder->execute();
533 1
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
534 1
        $platform  = $builder->getConnection()->getDatabasePlatform();
535 1
        $typeNames = $this->getModelSchemes()->getAttributeTypes($class);
536
537 1
        $model = null;
538 1
        if (($attributes = $statement->fetch()) !== false) {
539 1
            $model = $this->readRowFromAssoc($attributes, $typeNames, $platform);
540
        }
541
542 1
        return $model;
543
    }
544
545
    /**
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 14
    protected function fetchSingle(QueryBuilder $builder, string $class)
552
    {
553 14
        $statement = $builder->execute();
554 14
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
555 14
        $platform = $builder->getConnection()->getDatabasePlatform();
556 14
        $typeNames = $this->getModelSchemes()->getAttributeTypes($class);
557
558 14
        $model = null;
559 14
        if (($attributes = $statement->fetch()) !== false) {
560 14
            $model = $this->readInstanceFromAssoc($class, $attributes, $typeNames, $platform);
561
        }
562
563 14
        return $model;
564
    }
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
     */
574 22
    protected function fetchCollection(QueryBuilder $builder, string $class, $limit = null, $offset = null): array
575
    {
576 22
        $statement = $builder->execute();
577 22
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
578 22
        $platform = $builder->getConnection()->getDatabasePlatform();
579 22
        $typeNames = $this->getModelSchemes()->getAttributeTypes($class);
580
581 22
        $models = [];
582 22
        while (($attributes = $statement->fetch()) !== false) {
583 22
            $models[] = $this->readInstanceFromAssoc($class, $attributes, $typeNames, $platform);
584
        }
585
586 22
        return $this->normalizePagingParams($models, $limit, $offset);
587
    }
588
589
    /**
590
     * @param string      $modelClass
591
     * @param array       $attributes
592
     * @param null|string $index
593
     *
594
     * @return array
595
     */
596 4
    protected function filterAttributesOnCreate(string $modelClass, array $attributes, string $index = null): array
597
    {
598 4
        $allowedAttributes = array_flip($this->getModelSchemes()->getAttributes($modelClass));
599 4
        $allowedChanges    = array_intersect_key($attributes, $allowedAttributes);
600 4
        if ($index !== null) {
601 1
            $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
602 1
            $allowedChanges[$pkName] = $index;
603
        }
604
605 4
        return $allowedChanges;
606
    }
607
608
    /**
609
     * @param string $modelClass
610
     * @param array  $attributes
611
     *
612
     * @return array
613
     */
614 3
    protected function filterAttributesOnUpdate(string $modelClass, array $attributes): array
615
    {
616 3
        $allowedAttributes = array_flip($this->getModelSchemes()->getAttributes($modelClass));
617 3
        $allowedChanges    = array_intersect_key($attributes, $allowedAttributes);
618
619 3
        return $allowedChanges;
620
    }
621
622
    /**
623
     * @param QueryBuilder $builder
624
     *
625
     * @return QueryBuilder
626
     */
627 1
    protected function builderOnCount(QueryBuilder $builder): QueryBuilder
628
    {
629 1
        return $builder;
630
    }
631
632
    /**
633
     * @param QueryBuilder $builder
634
     *
635
     * @return QueryBuilder
636
     */
637 16
    protected function builderOnIndex(QueryBuilder $builder): QueryBuilder
638
    {
639 16
        return $builder;
640
    }
641
642
    /**
643
     * @param QueryBuilder $builder
644
     *
645
     * @return QueryBuilder
646
     */
647 10
    protected function builderOnRead(QueryBuilder $builder): QueryBuilder
648
    {
649 10
        return $builder;
650
    }
651
652
    /**
653
     * @param QueryBuilder $builder
654
     *
655
     * @return QueryBuilder
656
     */
657 4
    protected function builderOnReadRelationship(QueryBuilder $builder): QueryBuilder
658
    {
659 4
        return $builder;
660
    }
661
662
    /**
663
     * @param QueryBuilder $builder
664
     *
665
     * @return QueryBuilder
666
     */
667 4
    protected function builderSaveResourceOnCreate(QueryBuilder $builder): QueryBuilder
668
    {
669 4
        return $builder;
670
    }
671
672
    /**
673
     * @param QueryBuilder $builder
674
     *
675
     * @return QueryBuilder
676
     */
677 3
    protected function builderSaveResourceOnUpdate(QueryBuilder $builder): QueryBuilder
678
    {
679 3
        return $builder;
680
    }
681
682
    /**
683
     * @param string       $relationshipName
684
     * @param QueryBuilder $builder
685
     *
686
     * @return QueryBuilder
687
     *
688
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
689
     */
690 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
691
        $relationshipName,
692
        QueryBuilder $builder
693
    ): QueryBuilder {
694 2
        return $builder;
695
    }
696
697
    /**
698
     * @param string       $relationshipName
699
     * @param QueryBuilder $builder
700
     *
701
     * @return QueryBuilder
702
     *
703
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
704
     */
705 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
706
        $relationshipName,
707
        QueryBuilder $builder
708
    ): QueryBuilder {
709 2
        return $builder;
710
    }
711
712
    /**
713
     * @param string       $relationshipName
714
     * @param QueryBuilder $builder
715
     *
716
     * @return QueryBuilder
717
     *
718
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
719
     */
720 2
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
721
        $relationshipName,
722
        QueryBuilder $builder
723
    ): QueryBuilder {
724 2
        return $builder;
725
    }
726
727
    /**
728
     * @param QueryBuilder $builder
729
     *
730
     * @return QueryBuilder
731
     */
732 5
    protected function builderOnDelete(QueryBuilder $builder): QueryBuilder
733
    {
734 5
        return $builder;
735
    }
736
737
    /**
738
     * @param PaginatedDataInterface      $data
739
     * @param IncludeParameterInterface[] $paths
740
     *
741
     * @return RelationshipStorageInterface
742
     *
743
     * @SuppressWarnings(PHPMD.ElseExpression)
744
     */
745 8
    protected function readRelationships(PaginatedDataInterface $data, array $paths): RelationshipStorageInterface
746
    {
747 8
        $result = $this->getFactory()->createRelationshipStorage();
748
749 8
        if (empty($data->getData()) === false && empty($paths) === false) {
750 8
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemes());
751 8
            $modelsAtPath = $this->getFactory()->createTagStorage();
752
753
            // we gonna send this storage via function params so it is an equivalent for &array
754 8
            $classAtPath = new ArrayObject();
755
756 8
            $model = null;
757 8
            if ($data->isCollection() === true) {
758 4
                foreach ($data->getData() as $model) {
759 4
                    $uniqueModel = $modelStorage->register($model);
760 4
                    if ($uniqueModel !== null) {
761 4
                        $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
762
                    }
763
                }
764
            } else {
765 4
                $model       = $data->getData();
766 4
                $uniqueModel = $modelStorage->register($model);
767 4
                if ($uniqueModel !== null) {
768 4
                    $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
769
                }
770
            }
771 8
            $classAtPath[static::ROOT_PATH] = get_class($model);
772
773 8
            foreach ($this->getPaths($paths) as list ($parentPath, $childPaths)) {
774 8
                $this->loadRelationshipsLayer(
775 8
                    $result,
776 8
                    $modelsAtPath,
777 8
                    $classAtPath,
778 8
                    $modelStorage,
779 8
                    $parentPath,
780 8
                    $childPaths
781
                );
782
            }
783
        }
784
785 8
        return $result;
786
    }
787
788
    /**
789
     * @param array           $models
790
     * @param int|string|null $offset
791
     * @param int|string|null $limit
792
     *
793
     * @return array
794
     *
795
     * @SuppressWarnings(PHPMD.ElseExpression)
796
     */
797 22
    private function normalizePagingParams(array $models, $limit, $offset): array
798
    {
799 22
        if ($limit !== null) {
800 21
            $hasMore = count($models) >= $limit;
801 21
            $limit   = $hasMore === true ? $limit - 1 : null;
802 21
            $offset  = $limit === null && $hasMore === false ? null : $offset;
803 21
            $hasMore === false ?: array_pop($models);
804
        } else {
805 1
            $hasMore = false;
806
        }
807
808 22
        return [$models, $hasMore, $limit, $offset];
809
    }
810
811
    /**
812
     * @param ErrorCollection $errors
813
     *
814
     * @return void
815
     */
816 32
    private function checkErrors(ErrorCollection $errors): void
817
    {
818 32
        if (empty($errors->getArrayCopy()) === false) {
819 1
            throw new E($errors);
820
        }
821
    }
822
823
    /**
824
     * @param IncludeParameterInterface[] $paths
825
     *
826
     * @return Generator
827
     */
828 8
    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 8
        $normalizedPaths = [];
835 8
        $pathsDepths     = [];
836 8
        foreach ($paths as $path) {
837 8
            $parentDepth = 0;
838 8
            $tmpPath     = static::ROOT_PATH;
839 8
            foreach ($path->getPath() as $pathPiece) {
840 8
                $parent                    = $tmpPath;
841 8
                $tmpPath                   = empty($tmpPath) === true ?
842 8
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
843 8
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
844 8
                $pathsDepths[$parent]      = $parentDepth++;
845
            }
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 8
        $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 8
        foreach ($pathsDepths as $parent => $depth) {
858 8
            $depth ?: null; // suppress unused
859 8
            $childPaths = $parentWithChildren[$parent];
860 8
            yield [$parent, $childPaths];
861
        }
862
    }
863
864
    /**
865
     * @param RelationshipStorageInterface $result
866
     * @param TagStorageInterface          $modelsAtPath
867
     * @param ArrayObject                  $classAtPath
868
     * @param ModelStorageInterface        $deDup
869
     * @param string                       $parentsPath
870
     * @param array                        $childRelationships
871
     *
872
     * @return void
873
     *
874
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
875
     */
876 8
    protected function loadRelationshipsLayer(
877
        RelationshipStorageInterface $result,
878
        TagStorageInterface $modelsAtPath,
879
        ArrayObject $classAtPath,
880
        ModelStorageInterface $deDup,
881
        string $parentsPath,
882
        array $childRelationships
883
    ): void {
884 8
        $rootClass   = $classAtPath[static::ROOT_PATH];
885 8
        $parentClass = $classAtPath[$parentsPath];
886 8
        $parents     = $modelsAtPath->get($parentsPath);
887
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
        // $models.
891
892 8
        foreach ($childRelationships as $name) {
893 8
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
894
895
            /** @var QueryBuilder $builder */
896
            list ($builder, $class, $relationshipType) =
897 8
                $this->getRepository()->readRelationship($parentClass, static::INDEX_BIND, $name);
898
899 8
            $classAtPath[$childrenPath] = $class;
900
901
            switch ($relationshipType) {
902 8
                case RelationshipTypes::BELONGS_TO:
903 7
                    $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
904 7
                    foreach ($parents as $parent) {
905 7
                        $builder->setParameter(static::INDEX_BIND, $parent->{$pkName});
906 7
                        $child = $deDup->register($this->fetchSingle($builder, $class));
907 7
                        if ($child !== null) {
908 6
                            $modelsAtPath->register($child, $childrenPath);
909
                        }
910 7
                        $result->addToOneRelationship($parent, $name, $child);
911
                    }
912 7
                    break;
913 6
                case RelationshipTypes::HAS_MANY:
914 4
                case RelationshipTypes::BELONGS_TO_MANY:
915 6
                    list ($queryOffset, $queryLimit) = $this->getPaginationStrategy()
916 6
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
917 6
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit);
918 6
                    $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
919 6
                    foreach ($parents as $parent) {
920 6
                        $builder->setParameter(static::INDEX_BIND, $parent->{$pkName});
921
                        list($children, $hasMore, $limit, $offset) =
922 6
                            $this->fetchCollection($builder, $class, $queryLimit, $queryOffset);
923 6
                        $deDupedChildren = [];
924 6
                        foreach ($children as $child) {
925 6
                            $child = $deDup->register($child);
926 6
                            $modelsAtPath->register($child, $childrenPath);
927 6
                            if ($child !== null) {
928 6
                                $deDupedChildren[] = $child;
929
                            }
930
                        }
931 6
                        $result->addToManyRelationship($parent, $name, $deDupedChildren, $hasMore, $offset, $limit);
932
                    }
933 8
                    break;
934
            }
935
        }
936
    }
937
938
    /**
939
     * @param string             $namespace
940
     *
941
     * @return FormatterInterface
942
     */
943 8
    protected function createMessageFormatter(string $namespace = Messages::RESOURCES_NAMESPACE): FormatterInterface
944
    {
945
        /** @var FormatterFactoryInterface $factory */
946 8
        $factory          = $this->getContainer()->get(FormatterFactoryInterface::class);
947 8
        $messageFormatter = $factory->createFormatter($namespace);
948
949 8
        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 30
    private function readInstanceFromAssoc(
963
        string $class,
964
        array $attributes,
965
        array $typeNames,
966
        AbstractPlatform $platform
967
    ) {
968 30
        $instance = new $class();
969 30
        foreach ($attributes as $name => $value) {
970 30
            if (array_key_exists($name, $typeNames) === true) {
971 30
                $type  = Type::getType($typeNames[$name]);
972 30
                $value = $type->convertToPHPValue($value, $platform);
973
            }
974 30
            $instance->{$name} = $value;
975
        }
976
977 30
        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 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
990
    {
991 1
        $row = [];
992 1
        foreach ($attributes as $name => $value) {
993 1
            if (array_key_exists($name, $typeNames) === true) {
994 1
                $type  = Type::getType($typeNames[$name]);
995 1
                $value = $type->convertToPHPValue($value, $platform);
996
            }
997 1
            $row[$name] = $value;
998
        }
999
1000 1
        return $row;
1001
    }
1002
}
1003