Passed
Push — master ( 80e8d3...7b53d1 )
by Daniel
07:04
created

PublishableExtension::applyToItem()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 20
nc 5
nop 6
dl 0
loc 36
ccs 0
cts 20
cp 0
crap 42
rs 8.9777
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components Bundle Project
5
 *
6
 * (c) Daniel West <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Silverback\ApiComponentsBundle\Doctrine\Extension\ORM;
15
16
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryCollectionExtensionInterface;
17
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
18
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
19
use Doctrine\ORM\EntityRepository;
20
use Doctrine\ORM\QueryBuilder;
21
use Doctrine\Persistence\ManagerRegistry;
22
use Silverback\ApiComponentsBundle\Annotation\Publishable;
23
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableHelper;
24
use Symfony\Component\HttpFoundation\RequestStack;
25
26
/**
27
 * @author Vincent Chalamon <[email protected]>
28
 */
29
final class PublishableExtension implements QueryItemExtensionInterface, ContextAwareQueryCollectionExtensionInterface
30
{
31
    private PublishableHelper $publishableHelper;
32
    private RequestStack $requestStack;
33
    private ManagerRegistry $registry;
34
    private ?Publishable $configuration = null;
35
36
    public function __construct(PublishableHelper $publishableHelper, RequestStack $requestStack, ManagerRegistry $registry)
37
    {
38
        $this->publishableHelper = $publishableHelper;
39
        $this->requestStack = $requestStack;
40
        $this->registry = $registry;
41
    }
42
43
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
44
    {
45
        $configuration = $this->getConfiguration($resourceClass);
46
47
        if (!$configuration || !$this->requestStack->getCurrentRequest()) {
48
            return;
49
        }
50
51
        if (!$this->isDraftRequest($resourceClass, $context)) {
52
            // User has no access to draft object
53
            $this->updateQueryBuilderForUnauthorizedUsers($queryBuilder, $configuration);
54
55
            return;
56
        }
57
58
        $alias = $queryBuilder->getRootAliases()[0];
59
        $classMetadata = $this->registry->getManagerForClass($resourceClass)->getClassMetadata($resourceClass);
60
61
        // (o.publishedResource = :id OR o.id = :id) ORDER BY o.publishedResource IS NULL LIMIT 1
62
        $criteriaReset = false;
63
        foreach ($identifiers as $identifier => $value) {
64
            $predicates = $queryBuilder->expr()->orX($queryBuilder->expr()->eq("$alias.$configuration->associationName", ":id_$identifier"), $queryBuilder->expr()->eq("$alias.$identifier", ":id_$identifier"), );
65
66
            // Reset queryBuilder to prevent an invalid DQL
67
            if (!$criteriaReset) {
68
                $queryBuilder->where($predicates);
69
                $criteriaReset = true;
70
            } else {
71
                $queryBuilder->andWhere($predicates);
72
            }
73
            $queryBuilder->setParameter("id_$identifier", $value, $classMetadata->getTypeOfField($identifier));
74
        }
75
76
        $queryBuilder->addSelect("CASE WHEN $alias.$configuration->associationName IS NULL THEN 1 ELSE 0 END AS HIDDEN assocNameSort");
77
        $queryBuilder->orderBy('assocNameSort', 'ASC');
78
        $queryBuilder->setMaxResults(1);
79
    }
80
81
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = []): void
82
    {
83
        if (!$configuration = $this->getConfiguration($resourceClass)) {
84
            return;
85
        }
86
87
        if (!$this->isDraftRequest($resourceClass, $context)) {
88
            // User has no access to draft object
89
            $this->updateQueryBuilderForUnauthorizedUsers($queryBuilder, $configuration);
90
91
            return;
92
        }
93
94
        $alias = $queryBuilder->getRootAliases()[0];
95
        $identifiers = $this->registry->getManagerForClass($resourceClass)->getClassMetadata($resourceClass)->getIdentifier();
96
        $dql = $this->getDQL($configuration, $resourceClass);
97
98
        // o.id NOT IN (SELECT p.publishedResource FROM {table} t WHERE t.publishedResource IS NOT NULL)
99
        foreach ($identifiers as $identifier) {
100
            $queryBuilder->andWhere($queryBuilder->expr()->notIn("$alias.$identifier", $dql));
101
        }
102
    }
103
104
    private function getDQL(Publishable $configuration, string $resourceClass): string
105
    {
106
        /** @var EntityRepository $repository */
107
        $repository = $this->registry->getManagerForClass($resourceClass)->getRepository($resourceClass);
108
        $queryBuilder = $repository->createQueryBuilder('o2');
109
110
        return $queryBuilder
111
            ->select("IDENTITY(o2.$configuration->associationName)")
112
            ->where($queryBuilder->expr()->isNotNull("o2.$configuration->associationName"))
113
            ->getDQL();
114
    }
115
116
    private function isDraftRequest(string $resourceClass, array $context): bool
117
    {
118
        return $this->publishableHelper->isGranted($resourceClass) && false === ($context['filters']['published'] ?? false);
119
    }
120
121
    private function updateQueryBuilderForUnauthorizedUsers(QueryBuilder $queryBuilder, Publishable $configuration): void
122
    {
123
        $alias = $queryBuilder->getRootAliases()[0];
124
        $queryBuilder
125
            ->andWhere("$alias.$configuration->fieldName IS NOT NULL")
126
            ->andWhere("$alias.$configuration->fieldName <= :currentTime")
127
            ->setParameter('currentTime', new \DateTimeImmutable());
128
    }
129
130
    private function getConfiguration(string $resourceClass): ?Publishable
131
    {
132
        if (!$this->configuration && ($this->publishableHelper->getAnnotationReader()->isConfigured($resourceClass))) {
133
            $this->configuration = $this->publishableHelper->getAnnotationReader()->getConfiguration($resourceClass);
134
        }
135
136
        return $this->configuration;
137
    }
138
}
139