Passed
Push — master ( ff93da...605481 )
by Rafael
08:12
created

DoctrineOffsetCursorPaginator::paginate()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 37
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5.1158

Importance

Changes 0
Metric Value
eloc 25
c 0
b 0
f 0
dl 0
loc 37
ccs 15
cts 18
cp 0.8333
rs 9.2088
cc 5
nc 7
nop 3
crap 5.1158
1
<?php
2
/*******************************************************************************
3
 *  This file is part of the GraphQL Bundle package.
4
 *
5
 *  (c) YnloUltratech <[email protected]>
6
 *
7
 *  For the full copyright and license information, please view the LICENSE
8
 *  file that was distributed with this source code.
9
 ******************************************************************************/
10
11
namespace Ynlo\GraphQLBundle\Pagination;
12
13
use Doctrine\ORM\EntityManager;
14
use Doctrine\ORM\EntityManagerInterface;
15
use Doctrine\ORM\QueryBuilder;
16
use Ynlo\GraphQLBundle\Model\ConnectionInterface;
17
use Ynlo\GraphQLBundle\Model\NodeConnection;
18
19
/**
20
 * DoctrineOffsetCursorPaginator
21
 */
22
class DoctrineOffsetCursorPaginator implements DoctrineCursorPaginatorInterface
23
{
24
    /**
25
     * @var NodeConnection
26
     */
27
    protected $connection;
28
29
    /**
30 6
     * @var EntityManager|EntityManagerInterface
31
     */
32 6
    protected $entityManager;
33 6
34 6
    /**
35
     * DoctrineOffsetCursorPaginator constructor.
36 6
     *
37
     * @param EntityManagerInterface $entityManager
38 6
     */
39 6
    public function __construct(EntityManagerInterface $entityManager)
40
    {
41 6
        $this->entityManager = $entityManager;
42
    }
43 6
44
    /**
45 6
     * {@inheritdoc}
46 6
     */
47 6
    public function paginate(QueryBuilder $query, PaginationRequest $pagination, ConnectionInterface $connection)
48
    {
49 6
        $count = $this->getQueryTotal($query);
50 6
        $this->connection = $connection;
0 ignored issues
show
Documentation Bug introduced by
$connection is of type Ynlo\GraphQLBundle\Model\ConnectionInterface, but the property $connection was declared to be of type Ynlo\GraphQLBundle\Model\NodeConnection. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
51
        $this->connection->setTotalCount($count);
52
53 6
        $this->applyCursor($query, $count, $pagination);
54 6
55 6
        $limit = $pagination->getFirst() ?? $pagination->getLast();
56 6
        $query->setMaxResults($limit);
57 6
58 3
        $results = $query->getQuery()->execute();
59
60 6
        $offset = $query->getFirstResult();
61
62
        $cursorOffset = $offset - 1;
63
        foreach ($results as $result) {
64
            $cursorOffset ++;
65
66 6
            if (!$this->connection->getPageInfo()->getStartCursor()) {
67
                $this->connection->getPageInfo()->setStartCursor($this->encodeCursor($offset));
68 6
            }
69
70
            $cursor = $this->encodeCursor($cursorOffset);
71
            $this->connection->addEdge($this->connection->createEdge($result, $cursor));
72
            if ($limit > 0) {
73
                $this->connection->setPages((int) ceil($count / $limit));
74
                if ($offset > 0) {
75
                    $this->connection->getPageInfo()->setPage((int) ceil($offset / $limit) + 1);
76
                } else {
77 6
                    $this->connection->getPageInfo()->setPage(1);
78
                }
79 6
            } else {
80
                $this->connection->setPages(0);
81 6
                $this->connection->getPageInfo()->setPage(0);
82
            }
83
            $this->connection->getPageInfo()->setEndCursor($cursor);
84
        }
85 6
    }
86
87
    /**
88
     * @param QueryBuilder $qb
89 6
     *
90 6
     * @return int
91
     *
92 6
     * @throws \Doctrine\ORM\NonUniqueResultException
93 6
     */
94
    protected function getQueryTotal(QueryBuilder $qb): int
95 6
    {
96
        $countQuery = clone $qb;
97
98
        if (\count($qb->getParameters()) > 0) {
99
            $countQuery->setParameters($qb->getParameters());
100
        }
101
102
        if ($countQuery->getDQLPart('orderBy')) {
103 6
            $countQuery->resetDQLPart('orderBy');
104
        }
105 6
106 6
        $countQuery->setMaxResults(null);
107 6
        $countQuery->setFirstResult(0);
108 2
109
        $rootEntity = $qb->getRootEntities()[0];
110
        $metadata = $this->entityManager->getClassMetadata($rootEntity);
111
112 2
        $queryAlias = $qb->getAllAliases()[0];
113 2
        $countQuery->select(sprintf('count(DISTINCT %s.%s) as total', $queryAlias, $metadata->getIdentifier()[0]));
114 1
115
        return $countQuery->getQuery()->getSingleScalarResult();
116 1
    }
117
118 2
    /**
119
     * @param QueryBuilder      $qb
120
     * @param int               $count
121
     * @param PaginationRequest $pagination
122 2
     */
123 2
    protected function applyCursor(QueryBuilder $qb, $count, PaginationRequest $pagination): void
124
    {
125 4
        $limit = $pagination->getFirst() ?? $pagination->getLast();
126
        $offset = 0;
127 2
        if (null !== $pagination->getBefore()) {
0 ignored issues
show
introduced by
The condition null !== $pagination->getBefore() is always true.
Loading history...
128 1
            $offset = $this->decodeCursor($pagination->getBefore()) - $limit;
129 1
130 1
            //when the offset is less than 0,
131
            //the limit of records will be modified to start in 0
132
            if ($offset < 0) {
133 1
                if ($pagination->getFirst()) {
134
                    $pagination->setFirst($pagination->getFirst() - abs($offset));
0 ignored issues
show
Bug introduced by
$pagination->getFirst() - abs($offset) of type double is incompatible with the type integer expected by parameter $first of Ynlo\GraphQLBundle\Pagin...tionRequest::setFirst(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

134
                    $pagination->setFirst(/** @scrutinizer ignore-type */ $pagination->getFirst() - abs($offset));
Loading history...
135
                } else {
136
                    $pagination->setLast($pagination->getLast() - abs($offset));
0 ignored issues
show
Bug introduced by
$pagination->getLast() - abs($offset) of type double is incompatible with the type integer expected by parameter $last of Ynlo\GraphQLBundle\Pagin...ationRequest::setLast(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

136
                    $pagination->setLast(/** @scrutinizer ignore-type */ $pagination->getLast() - abs($offset));
Loading history...
137 6
                }
138 1
                $offset = 0;
139 1
            }
140
141
            //first records before any cursor always start in 0
142
            if ($pagination->getFirst()) {
143
                $offset = 0;
144 6
            }
145 6
        } elseif (null !== $pagination->getAfter()) {
146
            //last records after any cursor always start in ($count - $limit)
147
            if ($pagination->getLast()) {
148
                $offset = $count - $pagination->getLast();
149
                if ($offset < $this->decodeCursor($pagination->getAfter())) {
150
                    $offset =  $this->decodeCursor($pagination->getAfter()) + 1;
151
                }
152
            } else {
153
                $offset = $this->decodeCursor($pagination->getAfter()) + 1;
154
            }
155
        }
156
157 6
        if ($pagination->getLast() && !$pagination->getBefore() && !$pagination->getAfter()) {
158 3
            $offset = $count - $pagination->getLast();
159
            if ($offset < 0) {
160 3
                $offset = 0;
161
            }
162
        }
163 6
164 3
        $page = $pagination->getPage();
165
        if ($page > 0) {
166 3
            $pages = (int) ceil($count / $limit);
167
            if ($page > $pages) {
168
                $page = $pages;
169 6
            }
170 6
171
            $offset = ($page - 1) * $limit;
172
            if ($offset < 0) {
173
                $offset = 0;
174
            }
175
        }
176
177 4
        if (0 === $offset) {
178
            $this->connection->getPageInfo()->setHasPreviousPage(false);
179 4
        } else {
180
            $this->connection->getPageInfo()->setHasPreviousPage(true);
181 4
        }
182
183
        if ($offset + $limit >= $count) {
184
            $this->connection->getPageInfo()->setHasNextPage(false);
185
        } else {
186
            $this->connection->getPageInfo()->setHasNextPage(true);
187
        }
188
189 6
        $qb->setFirstResult($offset);
190
    }
191 6
192
    /**
193
     * @param string $cursor
194
     *
195
     * @return int
196
     */
197
    protected function decodeCursor($cursor): int
198
    {
199
        [, $offset] = explode(':', base64_decode($cursor));
200
201
        return $offset;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $offset returns the type string which is incompatible with the type-hinted return integer.
Loading history...
202
    }
203
204
    /**
205
     * @param string $offset
206
     *
207
     * @return string
208
     */
209
    protected function encodeCursor($offset): string
210
    {
211
        return base64_encode(sprintf('cursor:%s', $offset));
212
    }
213
}
214