Passed
Branch master (950424)
by Christopher
11:06
created

getIndexOfFirstEntryInTheNextPage()   D

Complexity

Conditions 9
Paths 10

Size

Total Lines 45
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 27
nc 10
nop 1
dl 0
loc 45
rs 4.909
c 0
b 0
f 0
1
<?php
2
3
namespace POData\UriProcessor\QueryProcessor\SkipTokenParser;
4
5
use InvalidArgumentException;
6
use POData\Common\Messages;
7
use POData\Common\ODataException;
8
use POData\Providers\Metadata\ResourceType;
9
use POData\Providers\Metadata\Type\IType;
10
use POData\Providers\Metadata\Type\Null1;
11
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
12
13
/**
14
 * Class InternalSkipTokenInfo.
15
 *
16
 * Type which holds information about processed skiptoken value, this type
17
 * also provide method to search the given result set for the skiptoken
18
 * and to build skiptoken from an entry object.
19
 */
20
class InternalSkipTokenInfo
21
{
22
    /**
23
     * Reference to an instance of InternalOrderByInfo which holds
24
     * sorter function(s) generated from orderby clause.
25
     *
26
     * @var InternalOrderByInfo
27
     */
28
    private $internalOrderByInfo;
29
30
    /**
31
     * Holds collection of values in the skiptoken corresponds to the orderby
32
     * path segments.
33
     *
34
     * @var array<array<IType>>
35
     */
36
    private $orderByValuesInSkipToken;
37
38
    /**
39
     * Holds reference to the type of the resource pointed by the request uri.
40
     *
41
     * @var ResourceType
42
     */
43
    private $resourceType;
44
45
    /**
46
     * Reference to the object holding parsed skiptoken value, this information
47
     * can be used by the IDSQP implementor for custom paging.
48
     *
49
     * @var SkipTokenInfo
50
     */
51
    private $skipTokenInfo;
52
53
    /**
54
     * Object which is used as a key for searching the sorted result, this object
55
     * will be an instance of type described by the resource type pointed by the
56
     * request uri.
57
     *
58
     * @var mixed
59
     */
60
    private $keyObject;
61
62
    /**
63
     * Creates a new instance of InternalSkipTokenInfo.
64
     *
65
     * @param InternalOrderByInfo &$internalOrderByInfo     Reference to an instance of InternalOrderByInfo which holds
66
     *                                                      sorter function(s) generated from orderby clause
67
     * @param array<array<IType>> $orderByValuesInSkipToken Collection of values in the skiptoken corresponds to the
68
     *                                                      orderby path segments
69
     * @param ResourceType        &$resourceType            Reference to the resource type pointed to by the request uri
70
     */
71
    public function __construct(
72
        InternalOrderByInfo &$internalOrderByInfo,
73
        $orderByValuesInSkipToken,
74
        ResourceType &$resourceType
75
    ) {
76
        $this->internalOrderByInfo = $internalOrderByInfo;
77
        $this->orderByValuesInSkipToken = $orderByValuesInSkipToken;
78
        $this->resourceType = $resourceType;
79
        $this->skipTokenInfo = null;
80
        $this->keyObject = null;
81
    }
82
83
    /**
84
     * Gets reference to the SkipTokenInfo object holding result of
85
     * skiptoken parsing, which used by the IDSQP implementor for
86
     * custom paging.
87
     *
88
     * @return SkipTokenInfo
89
     */
90
    public function getSkipTokenInfo()
91
    {
92
        if (null === $this->skipTokenInfo) {
93
            $orderbyInfo = $this->getInternalOrderByInfo()->getOrderByInfo();
94
            $this->skipTokenInfo = new SkipTokenInfo(
95
                $orderbyInfo,
96
                $this->orderByValuesInSkipToken
97
            );
98
        }
99
100
        return $this->skipTokenInfo;
101
    }
102
103
    /**
104
     * Get reference to the InternalOrderByInfo object holding orderBy details.
105
     *
106
     * @return InternalOrderByInfo
107
     */
108
    public function getInternalOrderByInfo()
109
    {
110
        return $this->internalOrderByInfo;
111
    }
112
113
    /**
114
     * Search the sorted array of result set for key object created from the
115
     * skip token key values and returns index of first entry in the next page.
116
     *
117
     * @param array &$searchArray The sorted array to search
118
     *
119
     * @throws InvalidArgumentException
120
     *
121
     * @return int (1) If the array is empty then return -1,
122
     *             (2) If the key object found then return index of first record
123
     *             in the next page,
124
     *             (3) If partial matching found (means found matching for first
125
     *             m keys where m < n, where n is total number of positional
126
     *             keys, then return the index of the object which has most matching
127
     */
128
    public function getIndexOfFirstEntryInTheNextPage(&$searchArray)
129
    {
130
        if (!is_array($searchArray)) {
131
            $msg = Messages::internalSkipTokenInfoBinarySearchRequireArray('searchArray');
132
            throw new \InvalidArgumentException($msg);
133
        }
134
135
        if (empty($searchArray)) {
136
            return -1;
137
        }
138
139
        $comparer = $this->getInternalOrderByInfo()->getSorterFunction();
140
        //Gets the key object initialized from skiptoken
141
        $keyObject = $this->getKeyObject();
142
        $low = 0;
143
        $searchArraySize = count($searchArray) - 1;
144
        $high = $searchArraySize;
145
        do {
146
            $mid = $low + round(($high - $low)/2);
147
            $result = $comparer($keyObject, $searchArray[$mid]);
148
            if ($result > 0) {
149
                $low = $mid + 1;
150
            } elseif ($result < 0) {
151
                $high = $mid - 1;
152
            } else {
153
                //Now we found record the matches with skiptoken value, so first record of next page will at $mid + 1
154
                if ($mid == $searchArraySize) {
155
                    //Check skiptoken points to last record, in this case no more records available for next page
156
                    return -1;
157
                }
158
159
                return $mid + 1;
160
            }
161
        } while ($low <= $high);
162
163
        if ($mid >= $searchArraySize) {
164
            //If key object does not match with last object, then no more page
165
            return -1;
166
        } elseif ($mid <= 0) {
167
            //If key object is less than first object, then paged result start from 0
168
            return 0;
169
        }
170
171
        //return index of the most matching object
172
        return $mid;
173
    }
174
175
    /**
176
     * Gets the key object for searching, if the object is not initialized,
177
     * then do it from skiptoken positional values.
178
     *
179
     * @throws ODataException If reflection exception occurs while accessing or setting property
180
     *
181
     * @return mixed
182
     */
183
    public function getKeyObject()
184
    {
185
        if (null === $this->keyObject) {
186
            $this->keyObject = $this->getInternalOrderByInfo()->getDummyObject();
187
            $i = 0;
188
            foreach ($this->getInternalOrderByInfo()->getOrderByPathSegments() as $orderByPathSegment) {
189
                $index = 0;
190
                $currentObject = $this->keyObject;
191
                $subPathSegments = $orderByPathSegment->getSubPathSegments();
192
                $subPathCount = count($subPathSegments);
193
                foreach ($subPathSegments as &$subPathSegment) {
194
                    $isLastSegment = ($index == $subPathCount - 1);
195
                    try {
196
                        // if currentObject = null means, previous iteration did a
197
                        // ReflectionProperty::getValue where ReflectionProperty
198
                        // represents a complex/navigation, but its null, which means
199
                        // the property is not set in the dummy object by OrderByParser,
200
                        // an unexpected state.
201
                        $subSegName = $subPathSegment->getName();
202
                        if (!$isLastSegment) {
203
                            $currentObject = $this->resourceType->getPropertyValue($currentObject, $subSegName);
204
                        } else {
205
                            if ($this->orderByValuesInSkipToken[$i][1] instanceof Null1) {
206
                                $this->resourceType->setPropertyValue($currentObject, $subPathSegment->getName(), null);
207
                            } else {
208
                                // The Lexer's Token::Text value will be always
209
                                // string, convert the string to
210
                                // required type i.e. int, float, double etc..
211
                                $value
212
                                    = $this->orderByValuesInSkipToken[$i][1]->convert(
213
                                        $this->orderByValuesInSkipToken[$i][0]
214
                                    );
215
                                $this->resourceType->setPropertyValue($currentObject, $subSegName, $value);
216
                            }
217
                        }
218
                    } catch (\ReflectionException $reflectionException) {
219
                        throw ODataException::createInternalServerError(
220
                            Messages::internalSkipTokenInfoFailedToAccessOrInitializeProperty(
221
                                $subPathSegment->getName()
222
                            )
223
                        );
224
                    }
225
226
                    ++$index;
227
                }
228
229
                ++$i;
230
            }
231
        }
232
233
        return $this->keyObject;
234
    }
235
236
    /**
237
     * Build nextpage link from the given object which will be the last object
238
     * in the page.
239
     *
240
     * @param mixed $lastObject Entity instance to build next page link from
241
     *
242
     * @throws ODataException If reflection exception occurs while accessing
243
     *                        property
244
     *
245
     * @return string
246
     */
247
    public function buildNextPageLink($lastObject)
248
    {
249
        $nextPageLink = null;
250
        foreach ($this->getInternalOrderByInfo()->getOrderByPathSegments() as $orderByPathSegment) {
251
            $index = 0;
252
            $currentObject = $lastObject;
253
            $subPathSegments = $orderByPathSegment->getSubPathSegments();
254
            $subPathCount = count($subPathSegments);
255
            foreach ($subPathSegments as &$subPathSegment) {
256
                $isLastSegment = ($index == $subPathCount - 1);
257
                try {
258
                    $currentObject = $this->resourceType->getPropertyValue($currentObject, $subPathSegment->getName());
259
                    if (null === $currentObject) {
260
                        $nextPageLink .= 'null, ';
261
                        break;
262 View Code Duplication
                    } elseif ($isLastSegment) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
263
                        $type = $subPathSegment->getInstanceType();
264
                        assert($type instanceof IType, get_class($type));
265
                        $value = $type->convertToOData($currentObject);
0 ignored issues
show
Bug introduced by
The method convertToOData() does not exist on ReflectionClass. ( Ignorable by Annotation )

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

265
                        /** @scrutinizer ignore-call */ 
266
                        $value = $type->convertToOData($currentObject);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
266
                        $nextPageLink .= $value . ', ';
267
                    }
268
                } catch (\ReflectionException $reflectionException) {
269
                    throw ODataException::createInternalServerError(
270
                        Messages::internalSkipTokenInfoFailedToAccessOrInitializeProperty(
271
                            $subPathSegment->getName()
272
                        )
273
                    );
274
                }
275
276
                ++$index;
277
            }
278
        }
279
280
        return rtrim($nextPageLink, ', ');
281
    }
282
}
283