Passed
Pull Request — feature/publishable (#31)
by Vincent
07:19
created

PublishableExtension::applyToItem()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 37
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 37
ccs 0
cts 22
cp 0
rs 8.6346
cc 7
nc 4
nop 6
crap 56
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\QueryCollectionExtensionInterface;
17
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
18
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
19
use Doctrine\Common\Annotations\Reader;
20
use Doctrine\ORM\QueryBuilder;
21
use Doctrine\Persistence\ManagerRegistry;
22
use Silverback\ApiComponentBundle\Annotation\Publishable;
23
use Symfony\Component\ExpressionLanguage\Expression;
24
use Symfony\Component\HttpFoundation\Request;
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, QueryCollectionExtensionInterface
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()) || $request->isMethod(Request::METHOD_DELETE)) {
0 ignored issues
show
introduced by
$configuration is of type Silverback\ApiComponentB...\Annotation\Publishable, thus it always evaluated to true.
Loading history...
53
            return;
54
        }
55
56
        $alias = $queryBuilder->getRootAliases()[0];
57
        if (!$this->authorizationChecker->isGranted(new Expression($this->permission)) || true === ($context['filters']['published'] ?? false)) {
58
            // User has no access to draft object
59
            $queryBuilder
60
                ->andWhere("$alias.$configuration->fieldName IS NOT NULL")
61
                ->andWhere("$alias.$configuration->fieldName >= :currentTime");
62
63
            return;
64
        }
65
66
        // Reset queryBuilder to prevent an invalid DQL
67
        $queryBuilder->where('1 = 1');
68
69
        foreach ($identifiers as $identifier) {
70
            // (o.id = :id AND o.publishedAt IS NOT NULL AND o.publishedAt <= :currentTime)
71
            // OR ((o.publishedAt IS NULL OR o.publishedAt > :currentTime) AND o.publishedResource = :id)
72
            $queryBuilder->orWhere(
73
                $queryBuilder->expr()->andX(
74
                    $queryBuilder->expr()->eq("$alias.$identifier", ":id_$identifier"),
75
                    $queryBuilder->expr()->isNotNull("$alias.$configuration->fieldName"),
76
                    $queryBuilder->expr()->lte("$alias.$configuration->fieldName", ':currentTime'),
77
                ),
78
                $queryBuilder->expr()->andX(
79
                    $queryBuilder->expr()->orX(
80
                        $queryBuilder->expr()->isNull("$alias.$configuration->fieldName"),
81
                        $queryBuilder->expr()->gt("$alias.$configuration->fieldName", ':currentTime'),
82
                    ),
83
                    $queryBuilder->expr()->eq("$alias.$configuration->associationName", ":id_$identifier"),
84
                )
85
            )->setParameter('currentTime', new \DateTimeImmutable());
86
        }
87
    }
88
89
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
90
    {
91
        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...
92
            return;
93
        }
94
95
        $configuration = $this->getConfiguration($resourceClass);
96
        $alias = $queryBuilder->getRootAliases()[0];
97
        if (!$this->authorizationChecker->isGranted(new Expression($this->permission)) || true === ($context['filters']['published'] ?? false)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $context seems to never exist and therefore isset should always be false.
Loading history...
98
            // User has no access to draft object
99
            $queryBuilder
100
                ->andWhere("$alias.$configuration->fieldName IS NOT NULL")
101
                ->andWhere("$alias.$configuration->fieldName >= :currentTime");
102
103
            return;
104
        }
105
106
        $publishedResourceAlias = $queryNameGenerator->generateJoinAlias($configuration->associationName);
107
        $queryBuilder->leftJoin("$alias.$configuration->associationName", $publishedResourceAlias);
108
109
        // (o.publishedAt IS NOT NULL AND o.publishedAt <= :currentTime) OR (o.publishedAt IS NULL OR o.publishedAt > :currentTime)
110
        $queryBuilder->orWhere(
111
            $queryBuilder->expr()->andX(
112
                $queryBuilder->expr()->isNotNull("$alias.$configuration->fieldName"),
113
                $queryBuilder->expr()->lte("$alias.$configuration->fieldName", ':currentTime'),
114
            ),
115
            $queryBuilder->expr()->orX(
116
                $queryBuilder->expr()->isNull("$alias.$configuration->fieldName"),
117
                $queryBuilder->expr()->gt("$alias.$configuration->fieldName", ':currentTime'),
118
            ),
119
        )->setParameter('currentTime', new \DateTimeImmutable());
120
    }
121
122
    private function getConfiguration(string $resourceClass): ?Publishable
123
    {
124
        if (!$this->configuration && ($em = $this->registry->getManagerForClass($resourceClass))) {
125
            $this->configuration = $this->reader->getClassAnnotation($em->getClassMetadata($resourceClass)->getReflectionClass(), Publishable::class);
126
        }
127
128
        return $this->configuration;
129
    }
130
}
131