Passed
Pull Request — 2.0 (#56)
by Vincent
16:03 queued 09:35
created

KeyWalkStrategy::getLastKeyOfEntities()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 31
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 31
ccs 13
cts 13
cp 1
rs 9.2222
cc 6
nc 4
nop 1
crap 6
1
<?php
2
3
namespace Bdf\Prime\Query\Pagination\WalkStrategy;
4
5
use Bdf\Prime\Collection\CollectionInterface;
6
use Bdf\Prime\Connection\ConnectionInterface;
7
use Bdf\Prime\Query\Contract\Limitable;
8
use Bdf\Prime\Query\Contract\Orderable;
9
use Bdf\Prime\Query\Contract\ReadOperation;
10
use Bdf\Prime\Query\Contract\Whereable;
11
use Bdf\Prime\Query\ReadCommandInterface;
12
use InvalidArgumentException;
13
14
/**
15
 * Walk strategy using a primary key (or any unique key) as cursor
16
 * This strategy supports deleting entities during the walk, but the entity must contains a single primary key, and the query must be ordered by this key
17
 * Any sort on other attribute are not supported
18
 *
19
 * @template E as object
20
 * @implements WalkStrategyInterface<E>
21
 */
22
final class KeyWalkStrategy implements WalkStrategyInterface
23
{
24
    /**
25
     * @var KeyInterface<E>
26
     */
27
    private $key;
28
29
    /**
30
     * PrimaryKeyWalkStrategy constructor.
31
     *
32
     * @param KeyInterface<E> $key
33
     */
34 18
    public function __construct(KeyInterface $key)
35
    {
36 18
        $this->key = $key;
37
    }
38
39
    /**
40
     * {@inheritdoc}
41
     */
42 15
    public function initialize(ReadCommandInterface $query, int $chunkSize, int $startPage): WalkCursor
43
    {
44 15
        if (!self::supports($query, $startPage, $this->key->name())) {
45 3
            throw new InvalidArgumentException('KeyWalkStrategy is not supported by this query');
46
        }
47
48
        /** @var Limitable&Orderable&ReadCommandInterface<ConnectionInterface, E> $query */
49 12
        $query = clone $query;
50
51 12
        if (!isset($query->getOrders()[$this->key->name()])) {
52 12
            $query->order($this->key->name(), Orderable::ORDER_ASC);
53
        }
54
55 12
        $query->limit($chunkSize);
56
57
        /** @var WalkCursor<E> */
58 12
        return new WalkCursor($query);
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    #[ReadOperation]
65 12
    public function next(WalkCursor $cursor): WalkCursor
66
    {
67 12
        $cursor = clone $cursor;
68
69 12
        if ($cursor->entities) {
70 10
            $cursor->cursor = $this->getLastKeyOfEntities($cursor);
71
        }
72
73 12
        if ($cursor->cursor !== null) {
74
            /** @var ReadCommandInterface<ConnectionInterface, E>&Orderable&Whereable $query */
75 10
            $query = $cursor->query;
76 10
            $operator = $query->getOrders()[$this->key->name()] === Orderable::ORDER_ASC ? '>' : '<';
0 ignored issues
show
Bug introduced by
The method getOrders() does not exist on Bdf\Prime\Query\ReadCommandInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Bdf\Prime\Query\Contract...\KeyValueQueryInterface or Bdf\Prime\Query\QueryInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

76
            $operator = $query->/** @scrutinizer ignore-call */ getOrders()[$this->key->name()] === Orderable::ORDER_ASC ? '>' : '<';
Loading history...
77
78 10
            $query->where($this->key->name(), $operator, $cursor->cursor);
0 ignored issues
show
Bug introduced by
The method where() does not exist on Bdf\Prime\Query\ReadCommandInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Query\ReadCommandInterface. ( Ignorable by Annotation )

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

78
            $query->/** @scrutinizer ignore-call */ 
79
                    where($this->key->name(), $operator, $cursor->cursor);
Loading history...
79
        }
80
81 12
        $cursor->entities = $cursor->query->all();
0 ignored issues
show
Documentation Bug introduced by
It seems like $cursor->query->all() of type Bdf\Prime\Query\R[]&Bdf\...ion\CollectionInterface is incompatible with the declared type Bdf\Prime\Query\Pagination\WalkStrategy\R[]|null of property $entities.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
82
83 12
        if ($cursor->entities instanceof CollectionInterface) {
84
            $cursor->entities = $cursor->entities->all();
85
        }
86
87 12
        return $cursor;
88
    }
89
90
    /**
91
     * Check if the strategy supports the given parameters
92
     *
93
     * @param ReadCommandInterface $query The query
94
     * @param int|null $startPage The start page
95
     * @param string $key The cursor key
96
     *
97
     * @return bool
98
     *
99
     * @psalm-assert-if-true Orderable&Limitable&Whereable $query
100
     */
101 18
    public static function supports(ReadCommandInterface $query, ?int $startPage, string $key): bool
102
    {
103 18
        if ($startPage !== null && $startPage !== 1) {
104 3
            return false;
105
        }
106
107 16
        if (!($query instanceof Orderable && $query instanceof Limitable && $query instanceof Whereable)) {
108 2
            return false;
109
        }
110
111 15
        $orders = $query->getOrders();
112
113 15
        return empty($orders) || (count($orders) === 1 && isset($orders[$key]));
114
    }
115
116
    /**
117
     * Find the last key to be set as cursor value
118
     *
119
     * @param WalkCursor $cursor
120
     * @return mixed
121
     *
122
     * @see WalkCursor::$cursor
123
     */
124 10
    private function getLastKeyOfEntities(WalkCursor $cursor)
125
    {
126 10
        $lastEntity = end($cursor->entities);
0 ignored issues
show
Bug introduced by
It seems like $cursor->entities can also be of type null; however, parameter $array of end() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

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

126
        $lastEntity = end(/** @scrutinizer ignore-type */ $cursor->entities);
Loading history...
127
128
        // Basic select query : results are an ordered list, so the last key is always the key of the last entity
129 10
        if (array_is_list($cursor->entities)) {
130 7
            return $this->key->get($lastEntity);
131
        }
132
133
        // group by query
134
        // Because index can be overridden (or value are added), order is not guaranteed
135
        // So we should iterate other entities to find the "max" key
136
        // In case of "by combine", values of each key are ordered list, so we simply need to take the last entity's key of each index
137 3
        $lastKey = $this->key->get(is_array($lastEntity) ? end($lastEntity) : $lastEntity);
138
139
        /** @var ReadCommandInterface<ConnectionInterface, E>&Orderable&Whereable $query */
140 3
        $query = $cursor->query;
141 3
        $asc = $query->getOrders()[$this->key->name()] === Orderable::ORDER_ASC;
142
143 3
        foreach ($cursor->entities as $entity) {
144 3
            $key = $this->key->get(is_array($entity) ? end($entity) : $entity);
145 3
            $gt = $key > $lastKey;
146
147
            // order is ascendant and key is > lastKey
148
            // or order is descendant and key is < lastKey
149 3
            if ($asc === $gt) {
150 2
                $lastKey = $key;
151
            }
152
        }
153
154 3
        return $lastKey;
155
    }
156
}
157