Test Failed
Pull Request — feature/publishable (#31)
by Vincent
06:55
created

PublishableExtension::applyToItem()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 21
c 1
b 0
f 0
dl 0
loc 36
rs 9.2728
cc 5
nc 4
nop 6
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Component 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\ApiComponentBundle\Extension;
15
16
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryCollectionExtensionInterface;
17
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
18
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
19
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
20
use Doctrine\Common\Annotations\Reader;
21
use Doctrine\ORM\QueryBuilder;
22
use Doctrine\Persistence\ManagerRegistry;
23
use Silverback\ApiComponentBundle\Annotation\Publishable;
24
use Symfony\Component\ExpressionLanguage\Expression;
25
use Symfony\Component\HttpFoundation\RequestStack;
26
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
27
28
/**
29
 * @author Vincent Chalamon <[email protected]>
30
 */
31
final class PublishableExtension implements QueryItemExtensionInterface, ContextAwareQueryCollectionExtensionInterface
32
{
33
    private AuthorizationCheckerInterface $authorizationChecker;
34
    private Reader $reader;
35
    private ManagerRegistry $registry;
36
    private RequestStack $requestStack;
37
    private string $permission;
38
    private ?Publishable $configuration;
39
40
    public function __construct(AuthorizationCheckerInterface $authorizationChecker, Reader $reader, ManagerRegistry $registry, RequestStack $requestStack, string $permission)
41
    {
42
        $this->authorizationChecker = $authorizationChecker;
43
        $this->reader = $reader;
44
        $this->registry = $registry;
45
        $this->requestStack = $requestStack;
46
        $this->permission = $permission;
47
    }
48
49
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
50
    {
51
        $configuration = $this->getConfiguration($resourceClass);
52
        if (!$configuration || !($request = $this->requestStack->getCurrentRequest())) {
0 ignored issues
show
Unused Code introduced by
The assignment to $request is dead and can be removed.
Loading history...
introduced by
$configuration is of type Silverback\ApiComponentB...\Annotation\Publishable, thus it always evaluated to true.
Loading history...
53
            return;
54
        }
55
56
        if (!$this->isAllowed($context)) {
57
            // User has no access to draft object
58
            $this->updateQueryBuilderForUnauthorizedUsers($queryBuilder, $configuration);
59
60
            return;
61
        }
62
63
        // Reset queryBuilder to prevent an invalid DQL
64
        $queryBuilder->where('1 = 1');
65
66
        $alias = $queryBuilder->getRootAliases()[0];
67
        foreach ($identifiers as $identifier) {
68
            // (o.id = :id AND o.publishedAt IS NOT NULL AND o.publishedAt <= :currentTime)
69
            // OR ((o.publishedAt IS NULL OR o.publishedAt > :currentTime) AND o.publishedResource = :id)
70
            $queryBuilder->orWhere(
71
                $queryBuilder->expr()->andX(
72
                    $queryBuilder->expr()->eq("$alias.$identifier", ":id_$identifier"),
73
                    $queryBuilder->expr()->isNotNull("$alias.$configuration->fieldName"),
74
                    $queryBuilder->expr()->lte("$alias.$configuration->fieldName", ':currentTime'),
75
                ),
76
                $queryBuilder->expr()->andX(
77
                    $queryBuilder->expr()->orX(
78
                        $queryBuilder->expr()->isNull("$alias.$configuration->fieldName"),
79
                        $queryBuilder->expr()->gt("$alias.$configuration->fieldName", ':currentTime'),
80
                    ),
81
                    $queryBuilder->expr()->eq("$alias.$configuration->associationName", ":id_$identifier"),
82
                )
83
            )->setParameter('currentTime', new \DateTimeImmutable())
84
            ->setParameter("id_$identifier", $identifier);
85
        }
86
    }
87
88
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = []): void
89
    {
90
        if (!$configuration = $this->getConfiguration($resourceClass)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $configuration is dead and can be removed.
Loading history...
91
            return;
92
        }
93
94
        $configuration = $this->getConfiguration($resourceClass);
95
        if (!$this->isAllowed($context)) {
96
            // User has no access to draft object
97
            $this->updateQueryBuilderForUnauthorizedUsers($queryBuilder, $configuration);
98
99
            return;
100
        }
101
102
        $alias = $queryBuilder->getRootAliases()[0];
103
        $publishedResourceAlias = $queryNameGenerator->generateJoinAlias($configuration->associationName);
104
        $queryBuilder->leftJoin("$alias.$configuration->associationName", $publishedResourceAlias);
105
106
        // (o.publishedAt IS NOT NULL AND o.publishedAt <= :currentTime) OR (o.publishedAt IS NULL OR o.publishedAt > :currentTime)
107
        $queryBuilder->orWhere(
108
            $queryBuilder->expr()->andX(
109
                $queryBuilder->expr()->isNotNull("$alias.$configuration->fieldName"),
110
                $queryBuilder->expr()->lte("$alias.$configuration->fieldName", ':currentTime'),
111
            ),
112
            $queryBuilder->expr()->orX(
113
                $queryBuilder->expr()->isNull("$alias.$configuration->fieldName"),
114
                $queryBuilder->expr()->gt("$alias.$configuration->fieldName", ':currentTime'),
115
            ),
116
        )->setParameter('currentTime', new \DateTimeImmutable());
117
    }
118
119
    private function isAllowed(array $context): bool
120
    {
121
        return $this->authorizationChecker->isGranted(new Expression($this->permission)) && false === ($context['filters']['published'] ?? false);
122
    }
123
124
    private function updateQueryBuilderForUnauthorizedUsers(QueryBuilder $queryBuilder, Publishable $configuration): void
125
    {
126
        $alias = $queryBuilder->getRootAliases()[0];
127
        $queryBuilder
128
            ->andWhere("$alias.$configuration->fieldName IS NOT NULL")
129
            ->andWhere("$alias.$configuration->fieldName >= :currentTime")
130
            ->setParameter('currentTime', new \DateTimeImmutable());
131
    }
132
133
    private function getConfiguration(string $resourceClass): ?Publishable
134
    {
135
        if (!$this->configuration && ($em = $this->registry->getManagerForClass($resourceClass))) {
136
            $this->configuration = $this->reader->getClassAnnotation($em->getClassMetadata($resourceClass)->getReflectionClass(), Publishable::class);
137
        }
138
139
        return $this->configuration;
140
    }
141
}
142