Passed
Push — feature-FRAM-87-replace-where-... ( 9ff6e0 )
by Vincent
29:09
created

KeyWalkStrategy::next()   A

Complexity

Conditions 6
Paths 20

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.0852

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
c 1
b 0
f 0
dl 0
loc 31
ccs 13
cts 15
cp 0.8667
rs 9.1111
cc 6
nc 20
nop 1
crap 6.0852
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\Clause;
8
use Bdf\Prime\Query\CompilableClause;
9
use Bdf\Prime\Query\Contract\Limitable;
10
use Bdf\Prime\Query\Contract\Orderable;
11
use Bdf\Prime\Query\Contract\ReadOperation;
12
use Bdf\Prime\Query\Contract\Whereable;
13
use Bdf\Prime\Query\ReadCommandInterface;
14
use InvalidArgumentException;
15
16
use function method_exists;
17
18
/**
19
 * Walk strategy using a primary key (or any unique key) as cursor
20
 * 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
21
 * Any sort on other attribute are not supported
22
 *
23
 * @template E as object
24
 * @implements WalkStrategyInterface<E>
25
 */
26
final class KeyWalkStrategy implements WalkStrategyInterface
27
{
28
    /**
29
     * @var KeyInterface<E>
30
     */
31
    private $key;
32
33
    /**
34
     * PrimaryKeyWalkStrategy constructor.
35
     *
36
     * @param KeyInterface<E> $key
37
     */
38 19
    public function __construct(KeyInterface $key)
39
    {
40 19
        $this->key = $key;
41
    }
42
43
    /**
44
     * {@inheritdoc}
45
     */
46 16
    public function initialize(ReadCommandInterface $query, int $chunkSize, int $startPage): WalkCursor
47
    {
48 16
        if (!self::supports($query, $startPage, $this->key->name())) {
49 3
            throw new InvalidArgumentException('KeyWalkStrategy is not supported by this query');
50
        }
51
52
        /** @var Limitable&Orderable&ReadCommandInterface<ConnectionInterface, E> $query */
53 13
        $query = clone $query;
54
55 13
        if (!isset($query->getOrders()[$this->key->name()])) {
56 13
            $query->order($this->key->name(), Orderable::ORDER_ASC);
57
        }
58
59 13
        $query->limit($chunkSize);
60
61
        /** @var WalkCursor<E> */
62 13
        return new WalkCursor($query);
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68
    #[ReadOperation]
69 13
    public function next(WalkCursor $cursor): WalkCursor
70
    {
71 13
        $cursor = clone $cursor;
72
73 13
        if ($cursor->entities) {
74 11
            $cursor->cursor = $this->getLastKeyOfEntities($cursor);
75
        }
76
77 13
        if ($cursor->cursor !== null) {
78
            /** @var ReadCommandInterface<ConnectionInterface, E>&Orderable&Whereable $query */
79 11
            $query = $cursor->query;
80 11
            $column = $this->key->name();
81 11
            $operator = $query->getOrders()[$column] === 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

81
            $operator = $query->/** @scrutinizer ignore-call */ getOrders()[$column] === Orderable::ORDER_ASC ? '>' : '<';
Loading history...
82
83
            // #FRAM-86 : reset where clause
84
            // @todo remove method_exists check on prime 3.0
85 11
            if (method_exists($query, 'whereReplace')) {
86 11
                $query->whereReplace($column, $operator, $cursor->cursor);
87
            } else {
88
                $query->where($column, $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

88
                $query->/** @scrutinizer ignore-call */ 
89
                        where($column, $operator, $cursor->cursor);
Loading history...
89
            }
90
        }
91
92 13
        $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...
93
94 13
        if ($cursor->entities instanceof CollectionInterface) {
95
            $cursor->entities = $cursor->entities->all();
96
        }
97
98 13
        return $cursor;
99
    }
100
101
    /**
102
     * Check if the strategy supports the given parameters
103
     *
104
     * @param ReadCommandInterface $query The query
105
     * @param int|null $startPage The start page
106
     * @param string $key The cursor key
107
     *
108
     * @return bool
109
     *
110
     * @psalm-assert-if-true Orderable&Limitable&Whereable $query
111
     */
112 19
    public static function supports(ReadCommandInterface $query, ?int $startPage, string $key): bool
113
    {
114 19
        if ($startPage !== null && $startPage !== 1) {
115 3
            return false;
116
        }
117
118 17
        if (!($query instanceof Orderable && $query instanceof Limitable && $query instanceof Whereable)) {
119 2
            return false;
120
        }
121
122 16
        $orders = $query->getOrders();
123
124 16
        return empty($orders) || (count($orders) === 1 && isset($orders[$key]));
125
    }
126
127
    /**
128
     * Find the last key to be set as cursor value
129
     *
130
     * @param WalkCursor $cursor
131
     * @return mixed
132
     *
133
     * @see WalkCursor::$cursor
134
     */
135 11
    private function getLastKeyOfEntities(WalkCursor $cursor)
136
    {
137 11
        $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

137
        $lastEntity = end(/** @scrutinizer ignore-type */ $cursor->entities);
Loading history...
138
139
        // Basic select query : results are an ordered list, so the last key is always the key of the last entity
140 11
        if (array_is_list($cursor->entities)) {
141 8
            return $this->key->get($lastEntity);
142
        }
143
144
        // group by query
145
        // Because index can be overridden (or value are added), order is not guaranteed
146
        // So we should iterate other entities to find the "max" key
147
        // 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
148 3
        $lastKey = $this->key->get(is_array($lastEntity) ? end($lastEntity) : $lastEntity);
149
150
        /** @var ReadCommandInterface<ConnectionInterface, E>&Orderable&Whereable $query */
151 3
        $query = $cursor->query;
152 3
        $asc = $query->getOrders()[$this->key->name()] === Orderable::ORDER_ASC;
153
154 3
        foreach ($cursor->entities as $entity) {
155 3
            $key = $this->key->get(is_array($entity) ? end($entity) : $entity);
156 3
            $gt = $key > $lastKey;
157
158
            // order is ascendant and key is > lastKey
159
            // or order is descendant and key is < lastKey
160 3
            if ($asc === $gt) {
161 2
                $lastKey = $key;
162
            }
163
        }
164
165 3
        return $lastKey;
166
    }
167
}
168