Completed
Push — master ( 540370...4c31d8 )
by Rafael
06:09
created

DoctrineOffsetCursorPaginator::decodeCursor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
c 0
b 0
f 0
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
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\QueryBuilder;
14
use Ynlo\GraphQLBundle\Model\ConnectionInterface;
15
use Ynlo\GraphQLBundle\Model\NodeConnection;
16
17
/**
18
 * DoctrineOffsetCursorPaginator
19
 */
20
class DoctrineOffsetCursorPaginator implements DoctrineCursorPaginatorInterface
21
{
22
    /**
23
     * @var NodeConnection
24
     */
25
    protected $connection;
26
27
    /**
28
     * {@inheritdoc}
29
     */
30 6
    public function paginate(QueryBuilder $query, PaginationRequest $pagination, ConnectionInterface $connection)
31
    {
32 6
        $count = $this->getQueryTotal($query);
33 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...
34 6
        $this->connection->setTotalCount($count);
35
36 6
        $this->applyCursor($query, $count, $pagination);
37
38 6
        $limit = $pagination->getFirst() ?? $pagination->getLast();
39 6
        $query->setMaxResults($limit);
40
41 6
        $results = $query->getQuery()->execute();
42
43 6
        $offset = $query->getFirstResult();
44
45 6
        $cursorOffset = $offset - 1;
46 6
        foreach ($results as $result) {
47 6
            $cursorOffset ++;
48
49 6
            if (!$this->connection->getPageInfo()->getStartCursor()) {
50 6
                $this->connection->getPageInfo()->setStartCursor($this->encodeCursor($offset));
51
            }
52
53 6
            $cursor = $this->encodeCursor($cursorOffset);
54 6
            $this->connection->addEdge($this->connection->createEdge($result, $cursor));
55 6
            if ($limit > 0) {
56 6
                $this->connection->setPages((int) ceil($count / $limit));
57 6
                if ($offset > 0) {
58 3
                    $this->connection->getPageInfo()->setPage((int) ceil($offset / $limit) + 1);
59
                } else {
60 6
                    $this->connection->getPageInfo()->setPage(1);
61
                }
62
            } else {
63
                $this->connection->setPages(0);
64
                $this->connection->getPageInfo()->setPage(0);
65
            }
66 6
            $this->connection->getPageInfo()->setEndCursor($cursor);
67
        }
68 6
    }
69
70
    /**
71
     * @param QueryBuilder $qb
72
     *
73
     * @return int
74
     *
75
     * @throws \Doctrine\ORM\NonUniqueResultException
76
     */
77 6
    protected function getQueryTotal(QueryBuilder $qb): int
78
    {
79 6
        $countQuery = clone $qb;
80
81 6
        if (\count($qb->getParameters()) > 0) {
82
            $countQuery->setParameters($qb->getParameters());
83
        }
84
85 6
        if ($countQuery->getDQLPart('orderBy')) {
86
            $countQuery->resetDQLPart('orderBy');
87
        }
88
89 6
        $countQuery->setMaxResults(null);
90 6
        $countQuery->setFirstResult(0);
91
92 6
        $queryAlias = $qb->getAllAliases()[0];
93 6
        $countQuery->select(sprintf('count(DISTINCT %s.%s) as total', $queryAlias, 'id'));
94
95 6
        return $countQuery->getQuery()->getSingleScalarResult();
96
    }
97
98
    /**
99
     * @param QueryBuilder      $qb
100
     * @param int               $count
101
     * @param PaginationRequest $pagination
102
     */
103 6
    protected function applyCursor(QueryBuilder $qb, $count, PaginationRequest $pagination): void
104
    {
105 6
        $limit = $pagination->getFirst() ?? $pagination->getLast();
106 6
        $offset = 0;
107 6
        if (null !== $pagination->getBefore()) {
108 2
            $offset = $this->decodeCursor($pagination->getBefore()) - $limit;
109
110
            //when the offset is less than 0,
111
            //the limit of records will be modified to start in 0
112 2
            if ($offset < 0) {
113 2
                if ($pagination->getFirst()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pagination->getFirst() of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
114 1
                    $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

114
                    $pagination->setFirst(/** @scrutinizer ignore-type */ $pagination->getFirst() - abs($offset));
Loading history...
115
                } else {
116 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

116
                    $pagination->setLast(/** @scrutinizer ignore-type */ $pagination->getLast() - abs($offset));
Loading history...
117
                }
118 2
                $offset = 0;
119
            }
120
121
            //first records before any cursor always start in 0
122 2
            if ($pagination->getFirst()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pagination->getFirst() of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
123 2
                $offset = 0;
124
            }
125 4
        } elseif (null !== $pagination->getAfter()) {
126
            //last records after any cursor always start in ($count - $limit)
127 2
            if ($pagination->getLast()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pagination->getLast() of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
128 1
                $offset = $count - $pagination->getLast();
129 1
                if ($offset < $this->decodeCursor($pagination->getAfter())) {
130 1
                    $offset =  $this->decodeCursor($pagination->getAfter()) + 1;
131
                }
132
            } else {
133 1
                $offset = $this->decodeCursor($pagination->getAfter()) + 1;
134
            }
135
        }
136
137 6
        if ($pagination->getLast() && !$pagination->getBefore() && !$pagination->getAfter()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pagination->getLast() of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
138 1
            $offset = $count - $pagination->getLast();
139 1
            if ($offset < 0) {
140
                $offset = 0;
141
            }
142
        }
143
144 6
        $page = $pagination->getPage();
145 6
        if ($page > 0) {
146
            $pages = (int) ceil($count / $limit);
147
            if ($page > $pages) {
148
                $page = $pages;
149
            }
150
151
            $offset = ($page - 1) * $limit;
152
            if ($offset < 0) {
153
                $offset = 0;
154
            }
155
        }
156
157 6
        if (0 === $offset) {
158 3
            $this->connection->getPageInfo()->setHasPreviousPage(false);
159
        } else {
160 3
            $this->connection->getPageInfo()->setHasPreviousPage(true);
161
        }
162
163 6
        if ($offset + $limit >= $count) {
164 3
            $this->connection->getPageInfo()->setHasNextPage(false);
165
        } else {
166 3
            $this->connection->getPageInfo()->setHasNextPage(true);
167
        }
168
169 6
        $qb->setFirstResult($offset);
170 6
    }
171
172
    /**
173
     * @param string $cursor
174
     *
175
     * @return int
176
     */
177 4
    protected function decodeCursor($cursor): int
178
    {
179 4
        [, $offset] = explode(':', base64_decode($cursor));
180
181 4
        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...
182
    }
183
184
    /**
185
     * @param string $offset
186
     *
187
     * @return string
188
     */
189 6
    protected function encodeCursor($offset): string
190
    {
191 6
        return base64_encode(sprintf('cursor:%s', $offset));
192
    }
193
}
194