Completed
Push — develop ( 6d3477...b4cdc6 )
by Neomerx
05:19
created

Crud::createMessageFormatter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
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\Flute\Contracts\Adapters\PaginationStrategyInterface;
31
use Limoncello\Flute\Contracts\Adapters\RepositoryInterface;
32
use Limoncello\Flute\Contracts\Api\CrudInterface;
33
use Limoncello\Flute\Contracts\FactoryInterface;
34
use Limoncello\Flute\Contracts\Http\Query\IncludeParameterInterface;
35
use Limoncello\Flute\Contracts\Models\ModelStorageInterface;
36
use Limoncello\Flute\Contracts\Models\PaginatedDataInterface;
37
use Limoncello\Flute\Contracts\Models\TagStorageInterface;
38
use Limoncello\Flute\Exceptions\InvalidArgumentException;
39
use Limoncello\Flute\Http\Query\FilterParameterCollection;
40
use Limoncello\Flute\L10n\Messages;
41
use Neomerx\JsonApi\Contracts\Document\DocumentInterface;
42
use Neomerx\JsonApi\Exceptions\ErrorCollection;
43
use Neomerx\JsonApi\Exceptions\JsonApiException as E;
44
use Psr\Container\ContainerInterface;
45
46
/**
47
 * @package Limoncello\Flute
48
 *
49
 * @SuppressWarnings(PHPMD.TooManyMethods)
50
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
51
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
52
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
53
 */
54
class Crud implements CrudInterface
55
{
56
    use HasContainerTrait;
57
58
    /** Internal constant. Query param name. */
59
    protected const INDEX_BIND = ':index';
60
61
    /** Internal constant. Query param name. */
62
    protected const CHILD_INDEX_BIND = ':childIndex';
63
64
    /** Internal constant. Path constant. */
65
    protected const ROOT_PATH = '';
66
67
    /** Internal constant. Path constant. */
68
    protected const PATH_SEPARATOR = DocumentInterface::PATH_SEPARATOR;
69
70
    /**
71
     * @var FactoryInterface
72
     */
73
    private $factory;
74
75
    /**
76
     * @var string
77
     */
78
    private $modelClass;
79
80
    /**
81
     * @var RepositoryInterface
82
     */
83
    private $repository;
84
85
    /**
86
     * @var ModelSchemeInfoInterface
87
     */
88
    private $modelSchemes;
89
90
    /**
91
     * @var PaginationStrategyInterface
92
     */
93
    private $paginationStrategy;
94
95
    /**
96
     * @param ContainerInterface $container
97
     * @param string             $modelClass
98
     */
99 46
    public function __construct(ContainerInterface $container, string $modelClass)
100
    {
101 46
        $this->setContainer($container);
102
103 46
        $this->factory            = $this->getContainer()->get(FactoryInterface::class);
104 46
        $this->modelClass         = $modelClass;
105 46
        $this->repository         = $this->getContainer()->get(RepositoryInterface::class);
106 46
        $this->modelSchemes       = $this->getContainer()->get(ModelSchemeInfoInterface::class);
107 46
        $this->paginationStrategy = $this->getContainer()->get(PaginationStrategyInterface::class);
108
    }
109
110
    /**
111
     * @inheritdoc
112
     */
113 16
    public function index(
114
        FilterParameterCollection $filterParams = null,
115
        array $sortParams = null,
116
        array $includePaths = null,
117
        array $pagingParams = null
118
    ): PaginatedDataInterface {
119 16
        $modelClass = $this->getModelClass();
120
121 16
        $builder = $this->getRepository()->index($modelClass);
122
123 16
        $errors = $this->getFactory()->createErrorCollection();
124 16
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
125 16
        $this->checkErrors($errors);
126 15
        $sortParams === null ?: $this->getRepository()->applySorting($builder, $modelClass, $sortParams);
127
128 15
        list($offset, $limit) = $this->getPaginationStrategy()->parseParameters($pagingParams);
129 15
        $builder->setFirstResult($offset)->setMaxResults($limit + 1);
130
131 15
        $data = $this->fetchCollectionData($this->builderOnIndex($builder), $modelClass);
132
133 15
        $this->loadRelationships($data, $includePaths);
0 ignored issues
show
Bug introduced by
It seems like $includePaths defined by parameter $includePaths on line 116 can also be of type array; however, Limoncello\Flute\Api\Crud::loadRelationships() does only seem to accept array<integer,object<Lim...rameterInterface>>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
134
135 15
        return $data;
136
    }
137
138
    /**
139
     * @inheritdoc
140
     */
141 1
    public function indexResources(FilterParameterCollection $filterParams = null, array $sortParams = null): array
142
    {
143 1
        $modelClass = $this->getModelClass();
144
145 1
        $builder = $this->getRepository()->index($modelClass);
146
147 1
        $errors = $this->getFactory()->createErrorCollection();
148 1
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
149 1
        $this->checkErrors($errors);
150 1
        $sortParams === null ?: $this->getRepository()->applySorting($builder, $modelClass, $sortParams);
151
152 1
        list($models) = $this->fetchCollection($this->builderOnIndex($builder), $modelClass);
153
154 1
        return $models;
155
    }
156
157
    /**
158
     * @inheritdoc
159
     */
160 1
    public function count(FilterParameterCollection $filterParams = null): ?int
161
    {
162 1
        $modelClass = $this->getModelClass();
163
164 1
        $builder = $this->getRepository()->count($modelClass);
165
166 1
        $errors = $this->getFactory()->createErrorCollection();
167 1
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
168 1
        $this->checkErrors($errors);
169
170 1
        $result = $this->builderOnCount($builder)->execute()->fetchColumn();
171
172 1
        return $result === false ? null : $result;
173
    }
174
175
    /**
176
     * @inheritdoc
177
     */
178 11
    public function read(
179
        $index,
180
        FilterParameterCollection $filterParams = null,
181
        array $includePaths = null
182
    ): PaginatedDataInterface {
183 11
        $model = $this->readResource($index, $filterParams);
184 10
        $data  = $this->getFactory()->createPaginatedData($model);
185
186 10
        $this->loadRelationships($data, $includePaths);
0 ignored issues
show
Bug introduced by
It seems like $includePaths defined by parameter $includePaths on line 181 can also be of type array; however, Limoncello\Flute\Api\Crud::loadRelationships() does only seem to accept array<integer,object<Lim...rameterInterface>>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
187
188 10
        return $data;
189
    }
190
191
    /**
192
     * @inheritdoc
193
     */
194 11
    public function readResource($index, FilterParameterCollection $filterParams = null)
195
    {
196 11
        if ($index !== null && is_scalar($index) === false) {
197 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
198
        }
199
200 10
        $modelClass = $this->getModelClass();
201
202 10
        $builder = $this->getRepository()
203 10
            ->read($modelClass, static::INDEX_BIND)
204 10
            ->setParameter(static::INDEX_BIND, $index);
205
206 10
        $errors = $this->getFactory()->createErrorCollection();
207 10
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $modelClass, $filterParams);
208 10
        $this->checkErrors($errors);
209
210 10
        $model = $this->fetchSingle($this->builderOnRead($builder), $modelClass);
211
212 10
        return $model;
213
    }
214
215
    /**
216
     * @inheritdoc
217
     *
218
     * @SuppressWarnings(PHPMD.ElseExpression)
219
     */
220 5
    public function readRelationship(
221
        $index,
222
        string $name,
223
        FilterParameterCollection $filterParams = null,
224
        array $sortParams = null,
225
        array $pagingParams = null
226
    ): PaginatedDataInterface {
227 5
        if ($index !== null && is_scalar($index) === false) {
228 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
229
        }
230
231 4
        $modelClass = $this->getModelClass();
232
233
        /** @var QueryBuilder $builder */
234
        list ($builder, $resultClass, $relationshipType) =
235 4
            $this->getRepository()->readRelationship($modelClass, static::INDEX_BIND, $name);
236
237 4
        $errors = $this->getFactory()->createErrorCollection();
238 4
        $filterParams === null ?: $this->getRepository()->applyFilters($errors, $builder, $resultClass, $filterParams);
239 4
        $this->checkErrors($errors);
240 4
        $sortParams === null ?: $this->getRepository()->applySorting($builder, $resultClass, $sortParams);
241
242 4
        $builder->setParameter(static::INDEX_BIND, $index);
243
244 4
        $isCollection = $relationshipType === RelationshipTypes::HAS_MANY ||
245 4
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
246
247 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...
248 3
            list($offset, $limit) = $this->getPaginationStrategy()->parseParameters($pagingParams);
249 3
            $builder->setFirstResult($offset)->setMaxResults($limit + 1);
250 3
            $data = $this->fetchCollectionData($this->builderOnReadRelationship($builder), $resultClass);
251
        } else {
252 1
            $data = $this->fetchSingleData($this->builderOnReadRelationship($builder), $resultClass);
253
        }
254
255 4
        return $data;
256
    }
257
258
    /**
259
     * @inheritdoc
260
     */
261 6
    public function hasInRelationship($parentId, string $name, $childId): bool
262
    {
263 6
        if ($parentId !== null && is_scalar($parentId) === false) {
264 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
265
        }
266 5
        if ($childId !== null && is_scalar($childId) === false) {
267 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
268
        }
269
270 4
        $modelClass = $this->getModelClass();
271
272
        /** @var QueryBuilder $builder */
273 4
        list ($builder) = $this->getRepository()
274 4
            ->hasInRelationship($modelClass, static::INDEX_BIND, $name, static::CHILD_INDEX_BIND);
275
276 4
        $builder->setParameter(static::INDEX_BIND, $parentId);
277 4
        $builder->setParameter(static::CHILD_INDEX_BIND, $childId);
278
279 4
        $result = $builder->execute()->fetch();
280
281 4
        return $result !== false;
282
    }
283
284
    /**
285
     * @inheritdoc
286
     */
287 2
    public function readRow($index): ?array
288
    {
289 2
        if ($index !== null && is_scalar($index) === false) {
290 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
291
        }
292
293 1
        $modelClass = $this->getModelClass();
294 1
        $builder    = $this->getRepository()
295 1
            ->read($modelClass, static::INDEX_BIND)
296 1
            ->setParameter(static::INDEX_BIND, $index);
297 1
        $typedRow   = $this->fetchRow($builder, $modelClass);
298
299 1
        return $typedRow;
300
    }
301
302
    /**
303
     * @inheritdoc
304
     */
305 6
    public function delete($index): int
306
    {
307 6
        if ($index !== null && is_scalar($index) === false) {
308 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
309
        }
310
311 5
        $modelClass = $this->getModelClass();
312
313 5
        $builder = $this->builderOnDelete(
314 5
            $this->getRepository()->delete($modelClass, static::INDEX_BIND)->setParameter(static::INDEX_BIND, $index)
315
        );
316
317 5
        $deleted = $builder->execute();
318
319 4
        return (int)$deleted;
320
    }
321
322
    /**
323
     * @inheritdoc
324
     */
325 5
    public function create($index, array $attributes, array $toMany = []): string
326
    {
327 5
        if ($index !== null && is_scalar($index) === false) {
328 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
329
        }
330
331 4
        $modelClass = $this->getModelClass();
332
333 4
        $allowedChanges = $this->filterAttributesOnCreate($modelClass, $attributes, $index);
334
335 4
        $saveMain = $this->getRepository()->create($modelClass, $allowedChanges);
336 4
        $saveMain = $this->builderSaveResourceOnCreate($saveMain);
337 4
        $saveMain->getSQL(); // prepare
338 4
        $this->inTransaction(function () use ($modelClass, $saveMain, $toMany, &$index) {
339 4
            $saveMain->execute();
340
            // if no index given will use last insert ID as index
341 4
            $index !== null ?: $index = $saveMain->getConnection()->lastInsertId();
342 4
            foreach ($toMany as $name => $values) {
343 2
                $indexBind      = ':index';
344 2
                $otherIndexBind = ':otherIndex';
345 2
                $saveToMany     = $this->getRepository()
346 2
                    ->createToManyRelationship($modelClass, $indexBind, $name, $otherIndexBind);
347 2
                $saveToMany     = $this->builderSaveRelationshipOnCreate($name, $saveToMany);
348 2
                $saveToMany->setParameter($indexBind, $index);
349 2
                foreach ($values as $value) {
350 2
                    $saveToMany->setParameter($otherIndexBind, $value)->execute();
351
                }
352
            }
353 4
        });
354
355 4
        return $index;
356
    }
357
358
    /**
359
     * @inheritdoc
360
     */
361 4
    public function update($index, array $attributes, array $toMany = []): int
362
    {
363 4
        if ($index !== null && is_scalar($index) === false) {
364 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
365
        }
366
367 3
        $updated    = 0;
368 3
        $modelClass = $this->getModelClass();
369
370 3
        $allowedChanges = $this->filterAttributesOnUpdate($modelClass, $attributes);
371
372 3
        $saveMain = $this->getRepository()->update($modelClass, $index, $allowedChanges);
373 3
        $saveMain = $this->builderSaveResourceOnUpdate($saveMain);
374 3
        $saveMain->getSQL(); // prepare
375 3
        $this->inTransaction(function () use ($modelClass, $saveMain, $toMany, $index, &$updated) {
376 3
            $updated = $saveMain->execute();
377 3
            foreach ($toMany as $name => $values) {
378 2
                $indexBind      = ':index';
379 2
                $otherIndexBind = ':otherIndex';
380
381 2
                $cleanToMany = $this->getRepository()->cleanToManyRelationship($modelClass, $indexBind, $name);
382 2
                $cleanToMany = $this->builderCleanRelationshipOnUpdate($name, $cleanToMany);
383 2
                $cleanToMany->setParameter($indexBind, $index)->execute();
384
385 2
                $saveToMany = $this->getRepository()
386 2
                    ->createToManyRelationship($modelClass, $indexBind, $name, $otherIndexBind);
387 2
                $saveToMany = $this->builderSaveRelationshipOnUpdate($name, $saveToMany);
388 2
                $saveToMany->setParameter($indexBind, $index);
389 2
                foreach ($values as $value) {
390 2
                    $updated += (int)$saveToMany->setParameter($otherIndexBind, $value)->execute();
391
                }
392
            }
393 3
        });
394
395 3
        return (int)$updated;
396
    }
397
398
    /**
399
     * @return FactoryInterface
400
     */
401 32
    protected function getFactory(): FactoryInterface
402
    {
403 32
        return $this->factory;
404
    }
405
406
    /**
407
     * @return string
408
     */
409 38
    protected function getModelClass(): string
410
    {
411 38
        return $this->modelClass;
412
    }
413
414
    /**
415
     * @return RepositoryInterface
416
     */
417 38
    protected function getRepository(): RepositoryInterface
418
    {
419 38
        return $this->repository;
420
    }
421
422
    /**
423
     * @return ModelSchemeInfoInterface
424
     */
425 31
    protected function getModelSchemes(): ModelSchemeInfoInterface
426
    {
427 31
        return $this->modelSchemes;
428
    }
429
430
    /**
431
     * @return PaginationStrategyInterface
432
     */
433 21
    protected function getPaginationStrategy(): PaginationStrategyInterface
434
    {
435 21
        return $this->paginationStrategy;
436
    }
437
438
    /**
439
     * @param Closure $closure
440
     *
441
     * @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...
442
     */
443 7
    protected function inTransaction(Closure $closure): void
444
    {
445 7
        $connection = $this->getRepository()->getConnection();
446 7
        $connection->beginTransaction();
447
        try {
448 7
            $isOk = ($closure() === false ? null : true);
449 7
        } finally {
450 7
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
451
        }
452
    }
453
454
    /**
455
     * @param QueryBuilder $builder
456
     * @param string       $class
457
     *
458
     * @return PaginatedDataInterface
459
     */
460 1
    protected function fetchSingleData(QueryBuilder $builder, string $class): PaginatedDataInterface
461
    {
462 1
        $model = $this->fetchSingle($builder, $class);
463 1
        $data  = $this->getFactory()->createPaginatedData($model)->markAsSingleItem();
464
465 1
        return $data;
466
    }
467
468
    /**
469
     * @param QueryBuilder $builder
470
     * @param string       $class
471
     *
472
     * @return PaginatedDataInterface
473
     */
474 18
    protected function fetchCollectionData(QueryBuilder $builder, string $class): PaginatedDataInterface
475
    {
476 18
        list($models, $hasMore, $limit, $offset) = $this->fetchCollection($builder, $class);
477
478 18
        $data = $this->getFactory()
479 18
            ->createPaginatedData($models)
480 18
            ->markAsCollection()
481 18
            ->setOffset($offset)
482 18
            ->setLimit($limit);
483 18
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
484
485 18
        return $data;
486
    }
487
488
    /**
489
     * @param QueryBuilder $builder
490
     * @param string       $class
491
     *
492
     * @return array|null
493
     */
494 1
    protected function fetchRow(QueryBuilder $builder, string $class): ?array
495
    {
496 1
        $statement = $builder->execute();
497 1
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
498 1
        $platform  = $builder->getConnection()->getDatabasePlatform();
499 1
        $typeNames = $this->getModelSchemes()->getAttributeTypes($class);
500
501 1
        $model = null;
502 1
        if (($attributes = $statement->fetch()) !== false) {
503 1
            $model = $this->readRowFromAssoc($attributes, $typeNames, $platform);
504
        }
505
506 1
        return $model;
507
    }
508
509
    /**
510
     * @param QueryBuilder $builder
511
     * @param string       $class
512
     *
513
     * @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...
514
     */
515 14
    protected function fetchSingle(QueryBuilder $builder, string $class)
516
    {
517 14
        $statement = $builder->execute();
518 14
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
519 14
        $platform  = $builder->getConnection()->getDatabasePlatform();
520 14
        $typeNames = $this->getModelSchemes()->getAttributeTypes($class);
521
522 14
        $model = null;
523 14
        if (($attributes = $statement->fetch()) !== false) {
524 14
            $model = $this->readInstanceFromAssoc($class, $attributes, $typeNames, $platform);
525
        }
526
527 14
        return $model;
528
    }
529
530
    /**
531
     * @param QueryBuilder $builder
532
     * @param string       $class
533
     *
534
     * @return array
535
     */
536 22
    protected function fetchCollection(QueryBuilder $builder, string $class): array
537
    {
538 22
        $statement = $builder->execute();
539 22
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
540 22
        $platform  = $builder->getConnection()->getDatabasePlatform();
541 22
        $typeNames = $this->getModelSchemes()->getAttributeTypes($class);
542
543 22
        $models = [];
544 22
        while (($attributes = $statement->fetch()) !== false) {
545 22
            $models[] = $this->readInstanceFromAssoc($class, $attributes, $typeNames, $platform);
546
        }
547
548 22
        return $this->normalizePagingParams($models, $builder->getMaxResults(), $builder->getFirstResult());
549
    }
550
551
    /**
552
     * @param string      $modelClass
553
     * @param array       $attributes
554
     * @param null|string $index
555
     *
556
     * @return array
557
     */
558 4
    protected function filterAttributesOnCreate(string $modelClass, array $attributes, string $index = null): array
559
    {
560 4
        $allowedAttributes = array_flip($this->getModelSchemes()->getAttributes($modelClass));
561 4
        $allowedChanges    = array_intersect_key($attributes, $allowedAttributes);
562 4
        if ($index !== null) {
563 1
            $pkName                  = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
564 1
            $allowedChanges[$pkName] = $index;
565
        }
566
567 4
        return $allowedChanges;
568
    }
569
570
    /**
571
     * @param string $modelClass
572
     * @param array  $attributes
573
     *
574
     * @return array
575
     */
576 3
    protected function filterAttributesOnUpdate(string $modelClass, array $attributes): array
577
    {
578 3
        $allowedAttributes = array_flip($this->getModelSchemes()->getAttributes($modelClass));
579 3
        $allowedChanges    = array_intersect_key($attributes, $allowedAttributes);
580
581 3
        return $allowedChanges;
582
    }
583
584
    /**
585
     * @param QueryBuilder $builder
586
     *
587
     * @return QueryBuilder
588
     */
589 1
    protected function builderOnCount(QueryBuilder $builder): QueryBuilder
590
    {
591 1
        return $builder;
592
    }
593
594
    /**
595
     * @param QueryBuilder $builder
596
     *
597
     * @return QueryBuilder
598
     */
599 16
    protected function builderOnIndex(QueryBuilder $builder): QueryBuilder
600
    {
601 16
        return $builder;
602
    }
603
604
    /**
605
     * @param QueryBuilder $builder
606
     *
607
     * @return QueryBuilder
608
     */
609 10
    protected function builderOnRead(QueryBuilder $builder): QueryBuilder
610
    {
611 10
        return $builder;
612
    }
613
614
    /**
615
     * @param QueryBuilder $builder
616
     *
617
     * @return QueryBuilder
618
     */
619 4
    protected function builderOnReadRelationship(QueryBuilder $builder): QueryBuilder
620
    {
621 4
        return $builder;
622
    }
623
624
    /**
625
     * @param QueryBuilder $builder
626
     *
627
     * @return QueryBuilder
628
     */
629 4
    protected function builderSaveResourceOnCreate(QueryBuilder $builder): QueryBuilder
630
    {
631 4
        return $builder;
632
    }
633
634
    /**
635
     * @param QueryBuilder $builder
636
     *
637
     * @return QueryBuilder
638
     */
639 3
    protected function builderSaveResourceOnUpdate(QueryBuilder $builder): QueryBuilder
640
    {
641 3
        return $builder;
642
    }
643
644
    /**
645
     * @param string       $relationshipName
646
     * @param QueryBuilder $builder
647
     *
648
     * @return QueryBuilder
649
     *
650
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
651
     */
652 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
653
        $relationshipName,
0 ignored issues
show
Unused Code introduced by
The parameter $relationshipName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
654
        QueryBuilder $builder
655
    ): QueryBuilder {
656 2
        return $builder;
657
    }
658
659
    /**
660
     * @param string       $relationshipName
661
     * @param QueryBuilder $builder
662
     *
663
     * @return QueryBuilder
664
     *
665
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
666
     */
667 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
668
        $relationshipName,
0 ignored issues
show
Unused Code introduced by
The parameter $relationshipName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
669
        QueryBuilder $builder
670
    ): QueryBuilder {
671 2
        return $builder;
672
    }
673
674
    /**
675
     * @param string       $relationshipName
676
     * @param QueryBuilder $builder
677
     *
678
     * @return QueryBuilder
679
     *
680
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
681
     */
682 2
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
683
        $relationshipName,
0 ignored issues
show
Unused Code introduced by
The parameter $relationshipName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
684
        QueryBuilder $builder
685
    ): QueryBuilder {
686 2
        return $builder;
687
    }
688
689
    /**
690
     * @param QueryBuilder $builder
691
     *
692
     * @return QueryBuilder
693
     */
694 5
    protected function builderOnDelete(QueryBuilder $builder): QueryBuilder
695
    {
696 5
        return $builder;
697
    }
698
699
    /**
700
     * @param PaginatedDataInterface           $data
701
     * @param IncludeParameterInterface[]|null $paths
702
     *
703
     * @return void
704
     *
705
     * @SuppressWarnings(PHPMD.ElseExpression)
706
     */
707 25
    protected function loadRelationships(PaginatedDataInterface $data, ?array $paths): void
708
    {
709 25
        if (empty($data->getData()) === false && empty($paths) === false) {
710 8
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemes());
711 8
            $modelsAtPath = $this->getFactory()->createTagStorage();
712
713
            // we gonna send this storage via function params so it is an equivalent for &array
714 8
            $classAtPath = new ArrayObject();
715
716 8
            $model = null;
717 8
            if ($data->isCollection() === true) {
718 4
                foreach ($data->getData() as $model) {
719 4
                    $uniqueModel = $modelStorage->register($model);
720 4
                    if ($uniqueModel !== null) {
721 4
                        $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
722
                    }
723
                }
724
            } else {
725 4
                $model       = $data->getData();
726 4
                $uniqueModel = $modelStorage->register($model);
727 4
                if ($uniqueModel !== null) {
728 4
                    $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
729
                }
730
            }
731 8
            $classAtPath[static::ROOT_PATH] = get_class($model);
732
733 8
            foreach ($this->getPaths($paths) as list ($parentPath, $childPaths)) {
734 8
                $this->loadRelationshipsLayer(
735 8
                    $modelsAtPath,
736 8
                    $classAtPath,
737 8
                    $modelStorage,
738 8
                    $parentPath,
739 8
                    $childPaths
740
                );
741
            }
742
        }
743
    }
744
745
    /**
746
     * @param IncludeParameterInterface[] $paths
747
     *
748
     * @return Generator
749
     */
750 8
    private function getPaths(array $paths): Generator
751
    {
752
        // The idea is to normalize paths. It means build all intermediate paths.
753
        // e.g. if only `a.b.c` path it given it will be normalized to `a`, `a.b` and `a.b.c`.
754
        // Path depths store depth of each path (e.g. 0 for root, 1 for `a`, 2 for `a.b` and etc).
755
        // It is needed for yielding them in correct order (from top level to bottom).
756 8
        $normalizedPaths = [];
757 8
        $pathsDepths     = [];
758 8
        foreach ($paths as $path) {
759 8
            assert($path instanceof IncludeParameterInterface);
760 8
            $parentDepth = 0;
761 8
            $tmpPath     = static::ROOT_PATH;
762 8
            foreach ($path->getPath() as $pathPiece) {
763 8
                $parent                    = $tmpPath;
764 8
                $tmpPath                   = empty($tmpPath) === true ?
765 8
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
766 8
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
767 8
                $pathsDepths[$parent]      = $parentDepth++;
768
            }
769
        }
770
771
        // Here we collect paths in form of parent => [list of children]
772
        // 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...
773 8
        $parentWithChildren = [];
774 8
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
775 8
            $parentWithChildren[$parent][] = $childPath;
776
        }
777
778
        // And finally sort by path depth and yield parent with its children. Top level paths first then deeper ones.
779 8
        asort($pathsDepths, SORT_NUMERIC);
780 8
        foreach ($pathsDepths as $parent => $depth) {
781 8
            assert($depth !== null); // suppress unused
782 8
            $childPaths = $parentWithChildren[$parent];
783 8
            yield [$parent, $childPaths];
784
        }
785
    }
786
787
    /**
788
     * @param TagStorageInterface   $modelsAtPath
789
     * @param ArrayObject           $classAtPath
790
     * @param ModelStorageInterface $deDup
791
     * @param string                $parentsPath
792
     * @param array                 $childRelationships
793
     *
794
     * @return void
795
     *
796
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
797
     */
798 8
    private function loadRelationshipsLayer(
799
        TagStorageInterface $modelsAtPath,
800
        ArrayObject $classAtPath,
801
        ModelStorageInterface $deDup,
802
        string $parentsPath,
803
        array $childRelationships
804
    ): void {
805 8
        $rootClass   = $classAtPath[static::ROOT_PATH];
806 8
        $parentClass = $classAtPath[$parentsPath];
807 8
        $parents     = $modelsAtPath->get($parentsPath);
808
809
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
810
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
811
        // $models.
812
813 8
        foreach ($childRelationships as $name) {
814 8
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
815
816
            /** @var QueryBuilder $builder */
817
            list ($builder, $class, $relationshipType) =
818 8
                $this->getRepository()->readRelationship($parentClass, static::INDEX_BIND, $name);
819
820 8
            $classAtPath[$childrenPath] = $class;
821
822
            switch ($relationshipType) {
823 8
                case RelationshipTypes::BELONGS_TO:
824 7
                    $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
825 7
                    foreach ($parents as $parent) {
826 7
                        $builder->setParameter(static::INDEX_BIND, $parent->{$pkName});
827 7
                        $child = $deDup->register($this->fetchSingle($builder, $class));
828 7
                        if ($child !== null) {
829 6
                            $modelsAtPath->register($child, $childrenPath);
830
                        }
831 7
                        $parent->{$name} = $child;
832
                    }
833 7
                    break;
834 6
                case RelationshipTypes::HAS_MANY:
835 4
                case RelationshipTypes::BELONGS_TO_MANY:
836 6
                    list ($queryOffset, $queryLimit) = $this->getPaginationStrategy()
837 6
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
838 6
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
839 6
                    $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
840 6
                    foreach ($parents as $parent) {
841 6
                        $builder->setParameter(static::INDEX_BIND, $parent->{$pkName});
842
                        list($children, $hasMore, $limit, $offset) =
843 6
                            $this->fetchCollection($builder, $class);
844 6
                        $deDupedChildren = [];
845 6
                        foreach ($children as $child) {
846 6
                            $child = $deDup->register($child);
847 6
                            $modelsAtPath->register($child, $childrenPath);
848 6
                            if ($child !== null) {
849 6
                                $deDupedChildren[] = $child;
850
                            }
851
                        }
852
853 6
                        $paginated = $this->factory->createPaginatedData($deDupedChildren)
854 6
                            ->markAsCollection()
855 6
                            ->setOffset($offset)
856 6
                            ->setLimit($limit);
857 6
                        $hasMore === true ? $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
858
859 6
                        $parent->{$name} = $paginated;
860
                    }
861 8
                    break;
862
            }
863
        }
864
    }
865
866
    /**
867
     * @param array           $models
868
     * @param int|string|null $offset
869
     * @param int|string|null $limit
870
     *
871
     * @return array
872
     *
873
     * @SuppressWarnings(PHPMD.ElseExpression)
874
     */
875 22
    private function normalizePagingParams(array $models, $limit, $offset): array
876
    {
877 22
        if ($limit !== null) {
878 21
            $hasMore = count($models) >= $limit;
879 21
            if ($hasMore === true) {
880 7
                array_pop($models);
881
            }
882 21
            $limit = $limit - 1;
883
        } else {
884 1
            $hasMore = false;
885
        }
886
887 22
        return [$models, $hasMore, $limit, $offset];
888
    }
889
890
    /**
891
     * @param ErrorCollection $errors
892
     *
893
     * @return void
894
     */
895 32
    private function checkErrors(ErrorCollection $errors): void
896
    {
897 32
        if (empty($errors->getArrayCopy()) === false) {
898 1
            throw new E($errors);
899
        }
900
    }
901
902
    /**
903
     * @param string $message
904
     *
905
     * @return string
906
     */
907 8
    private function getMessage(string $message): string
908
    {
909
        /** @var FormatterFactoryInterface $factory */
910 8
        $factory   = $this->getContainer()->get(FormatterFactoryInterface::class);
911 8
        $formatter = $factory->createFormatter(Messages::RESOURCES_NAMESPACE);
912 8
        $result    = $formatter->formatMessage($message);
913
914 8
        return $result;
915
    }
916
917
    /**
918
     * @param string           $class
919
     * @param array            $attributes
920
     * @param Type[]           $typeNames
921
     * @param AbstractPlatform $platform
922
     *
923
     * @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...
924
     *
925
     * @SuppressWarnings(PHPMD.StaticAccess)
926
     */
927 30
    private function readInstanceFromAssoc(
928
        string $class,
929
        array $attributes,
930
        array $typeNames,
931
        AbstractPlatform $platform
932
    ) {
933 30
        $instance = new $class();
934 30
        foreach ($attributes as $name => $value) {
935 30
            if (array_key_exists($name, $typeNames) === true) {
936 30
                $type  = Type::getType($typeNames[$name]);
937 30
                $value = $type->convertToPHPValue($value, $platform);
938
            }
939 30
            $instance->{$name} = $value;
940
        }
941
942 30
        return $instance;
943
    }
944
945
    /**
946
     * @param array            $attributes
947
     * @param Type[]           $typeNames
948
     * @param AbstractPlatform $platform
949
     *
950
     * @return array
951
     *
952
     * @SuppressWarnings(PHPMD.StaticAccess)
953
     */
954 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
955
    {
956 1
        $row = [];
957 1
        foreach ($attributes as $name => $value) {
958 1
            if (array_key_exists($name, $typeNames) === true) {
959 1
                $type  = Type::getType($typeNames[$name]);
960 1
                $value = $type->convertToPHPValue($value, $platform);
961
            }
962 1
            $row[$name] = $value;
963
        }
964
965 1
        return $row;
966
    }
967
}
968