1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace AlgoWeb\PODataLaravel\Query; |
4
|
|
|
|
5
|
|
|
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider; |
6
|
|
|
use AlgoWeb\PODataLaravel\Enums\ActionVerb; |
7
|
|
|
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface; |
8
|
|
|
use Illuminate\Database\Eloquent\Model; |
9
|
|
|
use Illuminate\Database\Eloquent\Relations\Relation; |
10
|
|
|
use Illuminate\Support\Facades\App; |
11
|
|
|
use POData\Common\ODataException; |
12
|
|
|
use Symfony\Component\Process\Exception\InvalidArgumentException; |
13
|
|
|
use POData\Providers\Metadata\ResourceProperty; |
14
|
|
|
use POData\Providers\Metadata\ResourceSet; |
15
|
|
|
use POData\Providers\Query\QueryResult; |
16
|
|
|
use POData\Providers\Query\QueryType; |
17
|
|
|
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo; |
18
|
|
|
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor; |
19
|
|
|
|
20
|
|
|
class LaravelReadQuery |
21
|
|
|
{ |
22
|
|
|
protected $auth; |
23
|
|
|
|
24
|
|
|
public function __construct(AuthInterface $auth = null) |
25
|
|
|
{ |
26
|
|
|
$this->auth = isset($auth) ? $auth : new NullAuthProvider(); |
27
|
|
|
} |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Gets collection of entities belongs to an entity set |
31
|
|
|
* IE: http://host/EntitySet |
32
|
|
|
* http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value |
33
|
|
|
* |
34
|
|
|
* @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count |
35
|
|
|
* @param ResourceSet $resourceSet The entity set containing the entities to fetch |
36
|
|
|
* @param FilterInfo $filterInfo represents the $filter parameter of the OData query. NULL if no $filter specified |
37
|
|
|
* @param mixed $orderBy sorted order if we want to get the data in some specific order |
38
|
|
|
* @param int $top number of records which need to be skip |
39
|
|
|
* @param String $skipToken value indicating what records to skip |
40
|
|
|
* @param Model|Relation|null $sourceEntityInstance Starting point of query |
41
|
|
|
* |
42
|
|
|
* @return QueryResult |
43
|
|
|
*/ |
44
|
|
|
public function getResourceSet( |
45
|
|
|
QueryType $queryType, |
46
|
|
|
ResourceSet $resourceSet, |
47
|
|
|
$filterInfo = null, |
48
|
|
|
$orderBy = null, |
49
|
|
|
$top = null, |
50
|
|
|
$skipToken = null, |
51
|
|
|
$sourceEntityInstance = null |
52
|
|
|
) { |
53
|
|
|
if (null != $filterInfo && !($filterInfo instanceof FilterInfo)) { |
54
|
|
|
throw new InvalidArgumentException('Filter info must be either null or instance of FilterInfo.'); |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
$this->checkSourceInstance($sourceEntityInstance); |
58
|
|
|
if (null == $sourceEntityInstance) { |
59
|
|
|
$sourceEntityInstance = $this->getSourceEntityInstance($resourceSet); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
$checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null; |
63
|
|
View Code Duplication |
if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $checkInstance)) { |
|
|
|
|
64
|
|
|
throw new ODataException("Access denied", 403); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
$result = new QueryResult(); |
68
|
|
|
$result->results = null; |
69
|
|
|
$result->count = null; |
70
|
|
|
|
71
|
|
|
if (null != $orderBy) { |
72
|
|
|
foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) { |
73
|
|
|
foreach ($order->getSubPathSegments() as $subOrder) { |
74
|
|
|
$sourceEntityInstance = $sourceEntityInstance->orderBy( |
75
|
|
|
$subOrder->getName(), |
76
|
|
|
$order->isAscending() ? 'asc' : 'desc' |
77
|
|
|
); |
78
|
|
|
} |
79
|
|
|
} |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
if (!isset($skipToken)) { |
83
|
|
|
$skipToken = 0; |
84
|
|
|
} |
85
|
|
|
if (!isset($top)) { |
86
|
|
|
$top = PHP_INT_MAX; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
$nullFilter = true; |
90
|
|
|
$isvalid = null; |
91
|
|
|
if (isset($filterInfo)) { |
92
|
|
|
$method = "return ".$filterInfo->getExpressionAsString().";"; |
93
|
|
|
$clln = "$".$resourceSet->getResourceType()->getName(); |
94
|
|
|
$isvalid = create_function($clln, $method); |
|
|
|
|
95
|
|
|
$nullFilter = false; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
$bulkSetCount = $sourceEntityInstance->count(); |
99
|
|
|
$bigSet = 20000 < $bulkSetCount; |
100
|
|
|
|
101
|
|
|
if ($nullFilter) { |
102
|
|
|
// default no-filter case, palm processing off to database engine - is a lot faster |
103
|
|
|
$resultSet = $sourceEntityInstance->skip($skipToken)->take($top)->get(); |
104
|
|
|
$resultCount = $bulkSetCount; |
105
|
|
|
} elseif ($bigSet) { |
106
|
|
|
assert(isset($isvalid), "Filter closure not set"); |
107
|
|
|
$resultSet = collect([]); |
108
|
|
|
$rawCount = 0; |
109
|
|
|
$rawTop = null === $top ? $bulkSetCount : $top; |
110
|
|
|
|
111
|
|
|
// loop thru, chunk by chunk, to reduce chances of exhausting memory |
112
|
|
|
$sourceEntityInstance->chunk( |
113
|
|
|
5000, |
114
|
|
|
function ($results) use ($isvalid, &$skipToken, &$resultSet, &$rawCount, $rawTop) { |
115
|
|
|
// apply filter |
116
|
|
|
$results = $results->filter($isvalid); |
117
|
|
|
// need to iterate through full result set to find count of items matching filter, |
118
|
|
|
// so we can't bail out early |
119
|
|
|
$rawCount += $results->count(); |
120
|
|
|
// now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz |
121
|
|
|
if ($rawTop > $resultSet->count() + $skipToken) { |
122
|
|
|
$resultSet = collect(array_merge($resultSet->all(), $results->all())); |
123
|
|
|
$sliceAmount = min($skipToken, $resultSet->count()); |
124
|
|
|
$resultSet = $resultSet->slice($sliceAmount); |
125
|
|
|
$skipToken -= $sliceAmount; |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
); |
129
|
|
|
|
130
|
|
|
// clean up residual to-be-skipped records |
131
|
|
|
$resultSet = $resultSet->slice($skipToken); |
132
|
|
|
$resultCount = $rawCount; |
133
|
|
|
} else { |
134
|
|
|
$resultSet = $sourceEntityInstance->get(); |
135
|
|
|
$resultSet = $resultSet->filter($isvalid); |
136
|
|
|
$resultCount = $resultSet->count(); |
137
|
|
|
|
138
|
|
|
if (isset($skipToken)) { |
139
|
|
|
$resultSet = $resultSet->slice($skipToken); |
140
|
|
|
} |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
if (isset($top)) { |
144
|
|
|
$resultSet = $resultSet->take($top); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
|
148
|
|
|
if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) { |
149
|
|
|
$result->results = array(); |
150
|
|
|
foreach ($resultSet as $res) { |
151
|
|
|
$result->results[] = $res; |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) { |
155
|
|
|
$result->count = $resultCount; |
156
|
|
|
} |
157
|
|
|
return $result; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Get related resource set for a resource |
162
|
|
|
* IE: http://host/EntitySet(1L)/NavigationPropertyToCollection |
163
|
|
|
* http://host/EntitySet?$expand=NavigationPropertyToCollection |
164
|
|
|
* |
165
|
|
|
* @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count |
166
|
|
|
* @param ResourceSet $sourceResourceSet The entity set containing the source entity |
167
|
|
|
* @param object $sourceEntityInstance The source entity instance. |
168
|
|
|
* @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property |
169
|
|
|
* @param ResourceProperty $targetProperty The navigation property to retrieve |
170
|
|
|
* @param FilterInfo $filter represents the $filter parameter of the OData query. NULL if no $filter specified |
171
|
|
|
* @param mixed $orderBy sorted order if we want to get the data in some specific order |
172
|
|
|
* @param int $top number of records which need to be skip |
173
|
|
|
* @param String $skip value indicating what records to skip |
174
|
|
|
* |
175
|
|
|
* @return QueryResult |
176
|
|
|
* |
177
|
|
|
*/ |
178
|
|
|
public function getRelatedResourceSet( |
179
|
|
|
QueryType $queryType, |
180
|
|
|
ResourceSet $sourceResourceSet, |
181
|
|
|
$sourceEntityInstance, |
182
|
|
|
ResourceSet $targetResourceSet, |
183
|
|
|
ResourceProperty $targetProperty, |
184
|
|
|
$filter = null, |
185
|
|
|
$orderBy = null, |
186
|
|
|
$top = null, |
187
|
|
|
$skip = null |
188
|
|
|
) { |
189
|
|
|
if (!($sourceEntityInstance instanceof Model)) { |
190
|
|
|
throw new InvalidArgumentException('Source entity must be an Eloquent model.'); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
assert(null != $sourceEntityInstance, "Source instance must not be null"); |
194
|
|
|
$this->checkSourceInstance($sourceEntityInstance); |
195
|
|
|
|
196
|
|
View Code Duplication |
if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $sourceEntityInstance)) { |
|
|
|
|
197
|
|
|
throw new ODataException("Access denied", 403); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
$propertyName = $targetProperty->getName(); |
201
|
|
|
$results = $sourceEntityInstance->$propertyName(); |
202
|
|
|
|
203
|
|
|
return $this->getResourceSet( |
204
|
|
|
$queryType, |
205
|
|
|
$sourceResourceSet, |
206
|
|
|
$filter, |
207
|
|
|
$orderBy, |
208
|
|
|
$top, |
209
|
|
|
$skip, |
210
|
|
|
$results |
211
|
|
|
); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* Gets an entity instance from an entity set identified by a key |
216
|
|
|
* IE: http://host/EntitySet(1L) |
217
|
|
|
* http://host/EntitySet(KeyA=2L,KeyB='someValue') |
218
|
|
|
* |
219
|
|
|
* @param ResourceSet $resourceSet The entity set containing the entity to fetch |
220
|
|
|
* @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch |
221
|
|
|
* |
222
|
|
|
* @return object|null Returns entity instance if found else null |
223
|
|
|
*/ |
224
|
|
|
public function getResourceFromResourceSet( |
225
|
|
|
ResourceSet $resourceSet, |
226
|
|
|
KeyDescriptor $keyDescriptor = null |
227
|
|
|
) { |
228
|
|
|
return $this->getResource($resourceSet, $keyDescriptor); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet() |
234
|
|
|
* @param ResourceSet|null $resourceSet |
235
|
|
|
* @param KeyDescriptor|null $keyDescriptor |
236
|
|
|
* @param Model|Relation|null $sourceEntityInstance Starting point of query |
237
|
|
|
*/ |
238
|
|
|
public function getResource( |
239
|
|
|
ResourceSet $resourceSet = null, |
240
|
|
|
KeyDescriptor $keyDescriptor = null, |
241
|
|
|
array $whereCondition = [], |
242
|
|
|
$sourceEntityInstance = null |
243
|
|
|
) { |
244
|
|
|
if (null == $resourceSet && null == $sourceEntityInstance) { |
245
|
|
|
throw new \Exception('Must supply at least one of a resource set and source entity.'); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
$this->checkSourceInstance($sourceEntityInstance); |
249
|
|
|
|
250
|
|
|
if (null == $sourceEntityInstance) { |
251
|
|
|
assert(null != $resourceSet); |
252
|
|
|
$sourceEntityInstance = $this->getSourceEntityInstance($resourceSet); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
View Code Duplication |
if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $sourceEntityInstance)) { |
|
|
|
|
256
|
|
|
throw new ODataException("Access denied", 403); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
if ($keyDescriptor) { |
260
|
|
|
foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) { |
261
|
|
|
$trimValue = trim($value[0], "\"'"); |
262
|
|
|
$sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue); |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
foreach ($whereCondition as $fieldName => $fieldValue) { |
266
|
|
|
$sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue); |
267
|
|
|
} |
268
|
|
|
$sourceEntityInstance = $sourceEntityInstance->get(); |
269
|
|
|
return (0 == $sourceEntityInstance->count()) ? null : $sourceEntityInstance->first(); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Get related resource for a resource |
274
|
|
|
* IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity |
275
|
|
|
* http://host/EntitySet?$expand=NavigationPropertyToSingleEntity |
276
|
|
|
* |
277
|
|
|
* @param ResourceSet $sourceResourceSet The entity set containing the source entity |
278
|
|
|
* @param object $sourceEntityInstance The source entity instance. |
279
|
|
|
* @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property |
280
|
|
|
* @param ResourceProperty $targetProperty The navigation property to fetch |
281
|
|
|
* |
282
|
|
|
* @return object|null The related resource if found else null |
283
|
|
|
*/ |
284
|
|
|
public function getRelatedResourceReference( |
285
|
|
|
ResourceSet $sourceResourceSet, |
286
|
|
|
$sourceEntityInstance, |
287
|
|
|
ResourceSet $targetResourceSet, |
288
|
|
|
ResourceProperty $targetProperty |
289
|
|
|
) { |
290
|
|
|
if (!($sourceEntityInstance instanceof Model)) { |
291
|
|
|
throw new InvalidArgumentException('Source entity must be an Eloquent model.'); |
292
|
|
|
} |
293
|
|
|
$this->checkSourceInstance($sourceEntityInstance); |
294
|
|
|
|
295
|
|
View Code Duplication |
if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $sourceEntityInstance)) { |
|
|
|
|
296
|
|
|
throw new ODataException("Access denied", 403); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
$propertyName = $targetProperty->getName(); |
300
|
|
|
return $sourceEntityInstance->$propertyName; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Gets a related entity instance from an entity set identified by a key |
305
|
|
|
* IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33) |
306
|
|
|
* |
307
|
|
|
* @param ResourceSet $sourceResourceSet The entity set containing the source entity |
308
|
|
|
* @param object $sourceEntityInstance The source entity instance. |
309
|
|
|
* @param ResourceSet $targetResourceSet The entity set containing the entity to fetch |
310
|
|
|
* @param ResourceProperty $targetProperty The metadata of the target property. |
311
|
|
|
* @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch |
312
|
|
|
* |
313
|
|
|
* @return object|null Returns entity instance if found else null |
314
|
|
|
*/ |
315
|
|
|
public function getResourceFromRelatedResourceSet( |
316
|
|
|
ResourceSet $sourceResourceSet, |
317
|
|
|
$sourceEntityInstance, |
318
|
|
|
ResourceSet $targetResourceSet, |
319
|
|
|
ResourceProperty $targetProperty, |
320
|
|
|
KeyDescriptor $keyDescriptor |
321
|
|
|
) { |
322
|
|
|
if (!($sourceEntityInstance instanceof Model)) { |
323
|
|
|
throw new InvalidArgumentException('Source entity must be an Eloquent model.'); |
324
|
|
|
} |
325
|
|
|
$propertyName = $targetProperty->getName(); |
326
|
|
|
return $this->getResource(null, $keyDescriptor, [], $sourceEntityInstance->$propertyName); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* @param ResourceSet $resourceSet |
332
|
|
|
* @return mixed |
333
|
|
|
*/ |
334
|
|
|
protected function getSourceEntityInstance(ResourceSet $resourceSet) |
335
|
|
|
{ |
336
|
|
|
$entityClassName = $resourceSet->getResourceType()->getInstanceType()->name; |
337
|
|
|
return App::make($entityClassName); |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* @param Model|Relation|null $source |
342
|
|
|
*/ |
343
|
|
|
protected function checkSourceInstance($source) |
344
|
|
|
{ |
345
|
|
|
if (!(null == $source || $source instanceof Model || $source instanceof Relation)) { |
346
|
|
|
throw new InvalidArgumentException('Source entity instance must be null, a model, or a relation.'); |
347
|
|
|
} |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
protected function getAuth() |
351
|
|
|
{ |
352
|
|
|
return $this->auth; |
353
|
|
|
} |
354
|
|
|
} |
355
|
|
|
|
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.