DoctrineOffsetCursorPaginator::applyCursor()   F
last analyzed

Complexity

Conditions 17
Paths 600

Size

Total Lines 67
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 20.3271

Importance

Changes 0
Metric Value
eloc 40
c 0
b 0
f 0
dl 0
loc 67
ccs 24
cts 31
cp 0.7742
rs 1.6055
cc 17
nc 600
nop 3
crap 20.3271

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use Ynlo\GraphQLBundle\Model\NodeInterface;
19
20
/**
21
 * DoctrineOffsetCursorPaginator
22
 */
23
class DoctrineOffsetCursorPaginator implements DoctrineCursorPaginatorInterface
24
{
25
    /**
26
     * @var NodeConnection
27
     */
28
    protected $connection;
29
30
    /**
31
     * @var EntityManager|EntityManagerInterface
32
     */
33
    protected $entityManager;
34
35
    /**
36
     * DoctrineOffsetCursorPaginator constructor.
37
     *
38
     * @param EntityManagerInterface $entityManager
39 6
     */
40
    public function __construct(EntityManagerInterface $entityManager)
41 6
    {
42 6
        $this->entityManager = $entityManager;
43
    }
44
45
    /**
46
     * {@inheritdoc}
47 6
     */
48
    public function paginate(QueryBuilder $query, PaginationRequest $pagination, ConnectionInterface $connection)
49 6
    {
50 6
        $count = $this->getQueryTotal($query);
51 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...
52
        $this->connection->setTotalCount($count);
53 6
54
        $this->applyCursor($query, $count, $pagination);
55 6
56 6
        $limit = $pagination->getFirst() ?? $pagination->getLast();
57
        $query->setMaxResults($limit);
58 6
        $query->distinct(true);
59
60 6
        $results = $query->getQuery()->execute();
61
62 6
        $offset = $query->getFirstResult();
63 6
64 6
        $cursorOffset = $offset - 1;
65
        foreach ($results as $result) {
66 6
            $cursorOffset++;
67 6
68
            if (!$this->connection->getPageInfo()->getStartCursor()) {
69
                $this->connection->getPageInfo()->setStartCursor($this->encodeCursor($offset));
70 6
            }
71 6
72 6
            $cursor = $this->encodeCursor($cursorOffset);
73 6
            $this->connection->addEdge($this->connection->createEdge($this->parseNode($result), $cursor));
74 6
            if ($limit > 0) {
75 3
                $this->connection->setPages((int) ceil($count / $limit));
76
                if ($offset > 0) {
77 6
                    $this->connection->getPageInfo()->setPage((int) ceil($offset / $limit) + 1);
78
                } else {
79
                    $this->connection->getPageInfo()->setPage(1);
80
                }
81
            } else {
82
                $this->connection->setPages(0);
83 6
                $this->connection->getPageInfo()->setPage(0);
84
            }
85 6
            $this->connection->getPageInfo()->setEndCursor($cursor);
86
        }
87
    }
88
89
    /**
90
     * Override this method to convert any arbitrary result into a node compatible result
91
     *
92
     * @param mixed $result
93
     *
94 6
     * @return NodeInterface
95
     */
96 6
    protected function parseNode($result): NodeInterface
97
    {
98 6
        if ($result instanceof NodeInterface){
99
            return $result;
100
        }
101
102 6
        throw new \LogicException('Incompatible result exception, expected object of type Node');
103
    }
104
105
    /**
106 6
     * @param QueryBuilder $qb
107 6
     *
108
     * @return int
109 6
     *
110 6
     * @throws \Doctrine\ORM\NonUniqueResultException
111
     */
112 6
    protected function getQueryTotal(QueryBuilder $qb): int
113 6
    {
114
        $countQuery = clone $qb;
115 6
116
        if (\count($qb->getParameters()) > 0) {
117
            $countQuery->setParameters($qb->getParameters());
118
        }
119
120
        if ($countQuery->getDQLPart('orderBy')) {
121
            $countQuery->resetDQLPart('orderBy');
122
        }
123 6
124
        if ($countQuery->getDQLPart('groupBy')) {
125 6
            $countQuery->resetDQLPart('groupBy');
126 6
        }
127 6
128 2
        $countQuery->setMaxResults(null);
129
        $countQuery->setFirstResult(0);
130
131
        $rootEntity = $qb->getRootEntities()[0];
132 2
        $metadata = $this->entityManager->getClassMetadata($rootEntity);
133 2
134 1
        $queryAlias = $qb->getAllAliases()[0];
135
        $countQuery->select(sprintf('count(DISTINCT %s.%s) as total', $queryAlias, $metadata->getIdentifier()[0]));
136 1
137
        return $countQuery->getQuery()->getSingleScalarResult();
138 2
    }
139
140
    /**
141
     * @param QueryBuilder      $qb
142 2
     * @param int               $count
143 2
     * @param PaginationRequest $pagination
144
     */
145 4
    protected function applyCursor(QueryBuilder $qb, $count, PaginationRequest $pagination): void
146
    {
147 2
        $limit = $pagination->getFirst() ?? $pagination->getLast();
148 1
        $offset = 0;
149 1
        if (null !== $pagination->getBefore()) {
0 ignored issues
show
introduced by
The condition null !== $pagination->getBefore() is always true.
Loading history...
150 1
            $offset = $this->decodeCursor($pagination->getBefore()) - $limit;
151
152
            //when the offset is less than 0,
153 1
            //the limit of records will be modified to start in 0
154
            if ($offset < 0) {
155
                if ($pagination->getFirst()) {
156
                    $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

156
                    $pagination->setFirst(/** @scrutinizer ignore-type */ $pagination->getFirst() - abs($offset));
Loading history...
157 6
                } else {
158 1
                    $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

158
                    $pagination->setLast(/** @scrutinizer ignore-type */ $pagination->getLast() - abs($offset));
Loading history...
159 1
                }
160
                $offset = 0;
161
            }
162
163
            //first records before any cursor always start in 0
164 6
            if ($pagination->getFirst()) {
165 6
                $offset = 0;
166
            }
167
        } elseif (null !== $pagination->getAfter()) {
168
            //last records after any cursor always start in ($count - $limit)
169
            if ($pagination->getLast()) {
170
                $offset = $count - $pagination->getLast();
171
                if ($offset < $this->decodeCursor($pagination->getAfter())) {
172
                    $offset = $this->decodeCursor($pagination->getAfter()) + 1;
173
                }
174
            } else {
175
                $offset = $this->decodeCursor($pagination->getAfter()) + 1;
176
            }
177 6
        }
178 3
179
        if ($pagination->getLast() && !$pagination->getBefore() && !$pagination->getAfter()) {
180 3
            $offset = $count - $pagination->getLast();
181
            if ($offset < 0) {
182
                $offset = 0;
183 6
            }
184 3
        }
185
186 3
        $page = $pagination->getPage();
187
        if ($page > 0) {
188
            $pages = (int) ceil($count / $limit);
189 6
            if ($page > $pages) {
190 6
                $page = $pages;
191
            }
192
193
            $offset = ($page - 1) * $limit;
194
            if ($offset < 0) {
195
                $offset = 0;
196
            }
197 4
        }
198
199 4
        if (0 === $offset) {
200
            $this->connection->getPageInfo()->setHasPreviousPage(false);
201 4
        } else {
202
            $this->connection->getPageInfo()->setHasPreviousPage(true);
203
        }
204
205
        if ($offset + $limit >= $count) {
206
            $this->connection->getPageInfo()->setHasNextPage(false);
207
        } else {
208
            $this->connection->getPageInfo()->setHasNextPage(true);
209 6
        }
210
211 6
        $qb->setFirstResult($offset);
212
    }
213
214
    /**
215
     * @param string $cursor
216
     *
217
     * @return int
218
     */
219
    protected function decodeCursor($cursor): int
220
    {
221
        [, $offset] = explode(':', base64_decode($cursor));
222
223
        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...
224
    }
225
226
    /**
227
     * @param string $offset
228
     *
229
     * @return string
230
     */
231
    protected function encodeCursor($offset): string
232
    {
233
        return base64_encode(sprintf('cursor:%s', $offset));
234
    }
235
}
236