Test Failed
Push — fix-FRAM-68-walker-by-combine ( 5af682 )
by Vincent
19:46
created

KeyWalkStrategy   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 133
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 23
eloc 40
c 1
b 0
f 0
dl 0
loc 133
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A next() 0 24 5
B supports() 0 13 8
A initialize() 0 17 3
A getLastKeyOfEntities() 0 31 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
    public function __construct(KeyInterface $key)
35
    {
36
        $this->key = $key;
37
    }
38
39
    /**
40
     * {@inheritdoc}
41
     */
42
    public function initialize(ReadCommandInterface $query, int $chunkSize, int $startPage): WalkCursor
43
    {
44
        if (!self::supports($query, $startPage, $this->key->name())) {
45
            throw new InvalidArgumentException('KeyWalkStrategy is not supported by this query');
46
        }
47
48
        /** @var Limitable&Orderable&ReadCommandInterface<ConnectionInterface, E> $query */
49
        $query = clone $query;
50
51
        if (!isset($query->getOrders()[$this->key->name()])) {
52
            $query->order($this->key->name(), Orderable::ORDER_ASC);
53
        }
54
55
        $query->limit($chunkSize);
56
57
        /** @var WalkCursor<E> */
58
        return new WalkCursor($query);
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    #[ReadOperation]
65
    public function next(WalkCursor $cursor): WalkCursor
66
    {
67
        $cursor = clone $cursor;
68
69
        if ($cursor->entities) {
70
            $cursor->cursor = $this->getLastKeyOfEntities($cursor);
71
        }
72
73
        if ($cursor->cursor !== null) {
74
            /** @var ReadCommandInterface<ConnectionInterface, E>&Orderable&Whereable $query */
75
            $query = $cursor->query;
76
            $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
            $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
        $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
        if ($cursor->entities instanceof CollectionInterface) {
84
            $cursor->entities = $cursor->entities->all();
85
        }
86
87
        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
    public static function supports(ReadCommandInterface $query, ?int $startPage, string $key): bool
102
    {
103
        if ($startPage !== null && $startPage !== 1) {
104
            return false;
105
        }
106
107
        if (!($query instanceof Orderable && $query instanceof Limitable && $query instanceof Whereable)) {
108
            return false;
109
        }
110
111
        $orders = $query->getOrders();
112
113
        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
    private function getLastKeyOfEntities(WalkCursor $cursor)
125
    {
126
        $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
        if (array_is_list($cursor->entities)) {
130
            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
        $lastKey = $this->key->get(is_array($lastEntity) ? end($lastEntity) : $lastEntity);
138
139
        /** @var ReadCommandInterface<ConnectionInterface, E>&Orderable&Whereable $query */
140
        $query = $cursor->query;
141
        $asc = $query->getOrders()[$this->key->name()] === Orderable::ORDER_ASC;
142
143
        foreach ($cursor->entities as $entity) {
144
            $key = $this->key->get(is_array($entity) ? end($entity) : $entity);
145
            $gt = $key > $lastKey;
146
147
            // order is ascendant and key is > lastKey
148
            // or order is descendant and key is < lastKey
149
            if ($asc === $gt) {
150
                $lastKey = $key;
151
            }
152
        }
153
154
        return $lastKey;
155
    }
156
}
157