├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml
├── src
├── Constants.php
├── ContentType.php
├── Core
│ ├── Enum.php
│ └── helpers.php
├── Entity.php
├── Exception
│ ├── ApplicationException.php
│ ├── MassAssignmentException.php
│ ├── ODataException.php
│ └── ODataQueryException.php
├── GuzzleHttpProvider.php
├── HeaderOption.php
├── HttpMethod.php
├── HttpRequestMessage.php
├── HttpStatusCode.php
├── IAuthenticationProvider.php
├── IHttpProvider.php
├── IODataClient.php
├── IODataRequest.php
├── ODataClient.php
├── ODataRequest.php
├── ODataResponse.php
├── Option.php
├── Preference.php
├── Query
│ ├── Builder.php
│ ├── ExpandClause.php
│ ├── Grammar.php
│ ├── IGrammar.php
│ ├── IProcessor.php
│ └── Processor.php
├── QueryOption.php
├── QueryOptions.php
├── RequestHeader.php
├── ResponseHeader.php
└── Uri.php
└── tests
├── Core
└── HelpersTest.php
├── ODataClientTest.php
└── Query
└── BuilderTest.php
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | name: PHP ${{ matrix.php-versions }}
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | operating-system: [ubuntu-latest]
12 | php-versions: ['7.3', '7.4', '8.0', '8.1', '8.2']
13 | steps:
14 | - uses: actions/checkout@v3
15 | - run: echo "The ${{ github.repository }} repository has been cloned to the runner."
16 | - name: Install PHP
17 | uses: shivammathur/setup-php@v2
18 | with:
19 | php-version: ${{ matrix.php-versions }}
20 | - name: Check PHP Version
21 | run: php -v
22 | - uses: php-actions/composer@v6
23 | with:
24 | php_version: ${{ matrix.php-versions }}
25 | - run: echo "Composer dependencies have been installed"
26 | - name: Run Tests
27 | run: vendor/bin/phpunit
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.phar
2 | composer.lock
3 | /vendor/
4 | /coverage
5 | .idea/
6 | /storage
7 | .env
8 | .DS_Store
9 | Thumbs.db
10 | /tests/testConfig.json
11 | .vscode
12 | .phpunit.result.cache
13 | release.sh
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Saint Systems, LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Get started with the OData Client for PHP
2 |
3 | A fluent library for calling OData REST services inspired by and based on the [Laravel Query Builder](https://laravel.com/docs/5.4/queries).
4 |
5 | *This library is currently in preview. Please continue to provide [feedback](https://github.com/saintsystems/odata-client-php/issues/new) as we iterate towards a production-supported library.*
6 |
7 | [](https://github.com/saintsystems/odata-client-php/actions/workflows/ci.yml)
8 | [](https://packagist.org/packages/saintsystems/odata-client)
9 | [](https://packagist.org/packages/saintsystems/odata-client)
10 |
11 | For WordPress users, please see our [Gravity Forms Dynamics 365 Add-On](https://www.saintsystems.com/products/gravity-forms-dynamics-crm-add-on/).
12 |
13 | ## Install the SDK
14 | You can install the PHP SDK with Composer.
15 | ```
16 | composer require saintsystems/odata-client
17 | ```
18 | ### Call an OData Service
19 |
20 | The following is an example that shows how to call an OData service.
21 |
22 | ```php
23 | from('People')->get();
39 |
40 | // Or retrieve a specific entity by the Entity ID/Key
41 | try {
42 | $person = $odataClient->from('People')->find('russellwhyte');
43 | echo "Hello, I am $person->FirstName ";
44 | } catch (Exception $e) {
45 | echo $e->getMessage();
46 | }
47 |
48 | // Want to only select a few properties/columns?
49 | $people = $odataClient->from('People')->select('FirstName','LastName')->get();
50 | }
51 | }
52 |
53 | $example = new UsageExample();
54 | ```
55 |
56 | ## Develop
57 |
58 | ### Run Tests
59 |
60 | Run ```vendor/bin/phpunit``` from the base directory.
61 |
62 |
63 | ## Documentation and resources
64 |
65 | * [Documentation](https://github.com/saintsystems/odata-client-php/wiki/Example-Calls)
66 |
67 | * [Wiki](https://github.com/saintsystems/odata-client-php/wiki)
68 |
69 | * [Examples](https://github.com/saintsystems/odata-client-php/wiki/Example-calls)
70 |
71 | * [OData website](http://www.odata.org)
72 |
73 | * [OASIS OData Version 4.0 Documentation](http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part1-protocol.html)
74 |
75 | ## Issues
76 |
77 | View or log issues on the [Issues](https://github.com/saintsystems/odata-client-php/issues) tab in the repo.
78 |
79 | ## Copyright and license
80 |
81 | Copyright (c) Saint Systems, LLC. All Rights Reserved. Licensed under the MIT [license](LICENSE).
82 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "saintsystems/odata-client",
3 | "version": "0.7.4",
4 | "description": "Saint Systems OData Client for PHP",
5 | "keywords": [
6 | "odata",
7 | "rest",
8 | "php"
9 | ],
10 | "homepage": "https://github.com/saintsystems/odata-client-php",
11 | "license": "MIT",
12 | "type": "library",
13 | "authors": [
14 | {
15 | "name": "Saint Systems",
16 | "email": "contact@saintsystems.com"
17 | }
18 | ],
19 | "require": {
20 | "php": "^7.3 || ^8.0",
21 | "guzzlehttp/guzzle": "^7.0",
22 | "nesbot/carbon": "^2.0 || ^3.0",
23 | "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0"
24 | },
25 | "require-dev": {
26 | "phpunit/phpunit": "^9.0 || ^10.5"
27 | },
28 | "autoload": {
29 | "files": [
30 | "src/Core/helpers.php"
31 | ],
32 | "psr-4": {
33 | "SaintSystems\\OData\\": "src"
34 | }
35 | },
36 | "config": {
37 | "preferred-install": "dist"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | src
6 |
7 |
8 | src/Model
9 |
10 |
11 |
12 |
13 | tests
14 |
15 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Constants.php:
--------------------------------------------------------------------------------
1 | value();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Core/Enum.php:
--------------------------------------------------------------------------------
1 | _value = $value;
50 | }
51 |
52 | /**
53 | * Check if the enum has the given value
54 | *
55 | * @param string $value
56 | * @return bool the enum has the value
57 | */
58 | public function has($value)
59 | {
60 | return in_array($value, self::toArray(), true);
61 | }
62 |
63 | /**
64 | * Check if the enum is defined
65 | *
66 | * @param string $value the value of the enum
67 | *
68 | * @return bool True if the value is defined
69 | */
70 | public function is($value)
71 | {
72 | return $this->_value === $value;
73 | }
74 |
75 | /**
76 | * Create a new class for the enum in question
77 | *
78 | * @return mixed
79 | */
80 | public function toArray()
81 | {
82 | $class = get_called_class();
83 |
84 | if (!(array_key_exists($class, self::$constants)))
85 | {
86 | $reflectionObj = new \ReflectionClass($class);
87 | self::$constants[$class] = $reflectionObj->getConstants();
88 | }
89 | return self::$constants[$class];
90 | }
91 |
92 | /**
93 | * Get the value of the enum
94 | *
95 | * @return string value of the enum
96 | */
97 | public function value()
98 | {
99 | return $this->_value;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Core/helpers.php:
--------------------------------------------------------------------------------
1 | string)
79 | */
80 | protected $properties = [];
81 |
82 | /**
83 | * The model property's original state.
84 | *
85 | * @var array
86 | */
87 | protected $original = [];
88 |
89 | /**
90 | * The loaded relationships for the model.
91 | *
92 | * @var array
93 | */
94 | protected $relations = [];
95 |
96 | /**
97 | * The properties that should be hidden for arrays.
98 | *
99 | * @var array
100 | */
101 | protected $hidden = [];
102 |
103 | /**
104 | * The properties that should be visible in arrays.
105 | *
106 | * @var array
107 | */
108 | protected $visible = [];
109 |
110 | /**
111 | * The accessors to append to the model's array form.
112 | *
113 | * @var array
114 | */
115 | protected $appends = [];
116 |
117 | /**
118 | * The properties that are mass assignable.
119 | *
120 | * @var array
121 | */
122 | protected $fillable = [];
123 |
124 | /**
125 | * The properties that aren't mass assignable.
126 | *
127 | * @var array
128 | */
129 | protected $guarded = []; //['*'];
130 |
131 | /**
132 | * The properties that should be mutated to dates.
133 | *
134 | * @var array
135 | */
136 | protected $dates = [];
137 |
138 | /**
139 | * The storage format of the model's date columns.
140 | *
141 | * @var string
142 | */
143 | protected $dateFormat;
144 |
145 | /**
146 | * The properties that should be cast to native types.
147 | *
148 | * @var array
149 | */
150 | protected $casts = [];
151 |
152 | /**
153 | * The relations to eager load on every call.
154 | *
155 | * @var array
156 | */
157 | protected $with = [];
158 |
159 | /**
160 | * The array of booted entities.
161 | *
162 | * @var array
163 | */
164 | protected static $booted = [];
165 |
166 | /**
167 | * Indicates if all mass assignment is enabled.
168 | *
169 | * @var bool
170 | */
171 | protected static $unguarded = false;
172 |
173 | /**
174 | * The cache of the mutated properties for each class.
175 | *
176 | * @var array
177 | */
178 | protected static $mutatorCache = [];
179 |
180 | /**
181 | * @var bool
182 | */
183 | private $exists;
184 |
185 | /**
186 | * @var string
187 | */
188 | private $entity;
189 |
190 | /**
191 | * Construct a new Entity
192 | *
193 | * @param array $properties A list of properties to set
194 | *
195 | * @return Entity
196 | */
197 | function __construct($properties = array())
198 | {
199 | $this->bootIfNotBooted();
200 |
201 | $this->syncOriginal();
202 |
203 | $this->fill($properties);
204 |
205 | return $this;
206 | }
207 |
208 | /**
209 | * Check if the entity needs to be booted and if so, do it.
210 | *
211 | * @return void
212 | */
213 | protected function bootIfNotBooted()
214 | {
215 | if (!isset(static::$booted[static::class])) {
216 | static::$booted[static::class] = true;
217 |
218 | // $this->fireModelEvent('booting', false);
219 |
220 | static::boot();
221 |
222 | // $this->fireModelEvent('booted', false);
223 | }
224 | }
225 |
226 | /**
227 | * The "booting" method of the entity.
228 | *
229 | * @return void
230 | */
231 | protected static function boot()
232 | {
233 | static::bootTraits();
234 | }
235 |
236 | /**
237 | * Boot all of the bootable traits on the entity.
238 | *
239 | * @return void
240 | */
241 | protected static function bootTraits()
242 | {
243 | $class = static::class;
244 |
245 | foreach (class_uses_recursive($class) as $trait) {
246 | if (method_exists($class, $method = 'boot' . class_basename($trait))) {
247 | forward_static_call([$class, $method]);
248 | }
249 | }
250 | }
251 |
252 | /**
253 | * Clear the list of booted entities so they will be re-booted.
254 | *
255 | * @return void
256 | */
257 | public static function clearBootedModels()
258 | {
259 | static::$booted = [];
260 | static::$globalScopes = [];
261 | }
262 |
263 | /**
264 | * Fill the entity with an array of properties.
265 | *
266 | * @param array $properties
267 | * @return $this
268 | *
269 | * @throws MassAssignmentException
270 | */
271 | public function fill(array $properties)
272 | {
273 | $totallyGuarded = $this->totallyGuarded();
274 |
275 | foreach ($this->fillableFromArray($properties) as $key => $value) {
276 | // $key = $this->removeTableFromKey($key);
277 |
278 | // The developers may choose to place some properties in the "fillable"
279 | // array, which means only those properties may be set through mass
280 | // assignment to the model, and all others will just be ignored.
281 | if ($this->isFillable($key)) {
282 | $this->setProperty($key, $value);
283 | } elseif ($totallyGuarded) {
284 | throw new MassAssignmentException($key);
285 | }
286 | }
287 |
288 | return $this;
289 | }
290 |
291 | /**
292 | * Fill the model with an array of properties. Force mass assignment.
293 | *
294 | * @param array $properties
295 | * @return $this
296 | */
297 | public function forceFill(array $properties)
298 | {
299 | return static::unguarded(function () use ($properties) {
300 | return $this->fill($properties);
301 | });
302 | }
303 |
304 | /**
305 | * Get the fillable properties of a given array.
306 | *
307 | * @param array $properties
308 | * @return array
309 | */
310 | protected function fillableFromArray(array $properties)
311 | {
312 | if (count($this->getFillable()) > 0 && !static::$unguarded) {
313 | return array_intersect_key($properties, array_flip($this->getFillable()));
314 | }
315 |
316 | return $properties;
317 | }
318 |
319 | /**
320 | * Create a new instance of the given model.
321 | *
322 | * @param array $properties
323 | * @param bool $exists
324 | * @return static
325 | */
326 | public function newInstance($properties = [], $exists = false)
327 | {
328 | // This method just provides a convenient way for us to generate fresh model
329 | // instances of this current model. It is particularly useful during the
330 | // hydration of new objects via the Eloquent query builder instances.
331 | $model = new static((array) $properties);
332 |
333 | $model->exists = $exists;
334 |
335 | return $model;
336 | }
337 |
338 | /**
339 | * Get the entity name associated with the entity.
340 | *
341 | * @return string
342 | */
343 | public function getEntity()
344 | {
345 | if (isset($this->entity)) {
346 | return $this->entity;
347 | }
348 |
349 | return str_replace('\\', '', Str::snake(Str::plural(class_basename($this))));
350 | }
351 |
352 | /**
353 | * Set the entity name associated with the model.
354 | *
355 | * @param string $entity
356 | *
357 | * @return $this
358 | */
359 | public function setEntity($entity)
360 | {
361 | $this->entity = $entity;
362 |
363 | return $this;
364 | }
365 |
366 | /**
367 | * Get the value of the entity's primary key.
368 | *
369 | * @return mixed
370 | */
371 | public function getKey()
372 | {
373 | return $this->getAttribute($this->getKeyName());
374 | }
375 |
376 | /**
377 | * Get the primary key for the entity.
378 | *
379 | * @return string
380 | */
381 | public function getKeyName()
382 | {
383 | return $this->primaryKey;
384 | }
385 |
386 | /**
387 | * Set the primary key for the entity.
388 | *
389 | * @param string $key
390 | * @return $this
391 | */
392 | public function setKeyName($key)
393 | {
394 | $this->primaryKey = $key;
395 |
396 | return $this;
397 | }
398 |
399 | /**
400 | * Get the number of entities to return per page.
401 | *
402 | * @return int
403 | */
404 | public function getPerPage()
405 | {
406 | return $this->perPage;
407 | }
408 |
409 | /**
410 | * Set the number of entities to return per page.
411 | *
412 | * @param int $perPage
413 | * @return $this
414 | */
415 | public function setPerPage($perPage)
416 | {
417 | $this->perPage = $perPage;
418 |
419 | return $this;
420 | }
421 |
422 | /**
423 | * Get the hidden properties for the model.
424 | *
425 | * @return array
426 | */
427 | public function getHidden()
428 | {
429 | return $this->hidden;
430 | }
431 |
432 | /**
433 | * Set the hidden properties for the model.
434 | *
435 | * @param array $hidden
436 | * @return $this
437 | */
438 | public function setHidden(array $hidden)
439 | {
440 | $this->hidden = $hidden;
441 |
442 | return $this;
443 | }
444 |
445 | /**
446 | * Add hidden properties for the model.
447 | *
448 | * @param array|string|null $properties
449 | * @return void
450 | */
451 | public function addHidden($properties = null)
452 | {
453 | $properties = is_array($properties) ? $properties : func_get_args();
454 |
455 | $this->hidden = array_merge($this->hidden, $properties);
456 | }
457 |
458 | /**
459 | * Make the given, typically hidden, properties visible.
460 | *
461 | * @param array|string $properties
462 | * @return $this
463 | */
464 | public function makeVisible($properties)
465 | {
466 | $this->hidden = array_diff($this->hidden, (array) $properties);
467 |
468 | if (!empty($this->visible)) {
469 | $this->addVisible($properties);
470 | }
471 |
472 | return $this;
473 | }
474 |
475 | /**
476 | * Make the given, typically visible, properties hidden.
477 | *
478 | * @param array|string $properties
479 | * @return $this
480 | */
481 | public function makeHidden($properties)
482 | {
483 | $properties = (array) $properties;
484 |
485 | $this->visible = array_diff($this->visible, $properties);
486 |
487 | $this->hidden = array_unique(array_merge($this->hidden, $properties));
488 |
489 | return $this;
490 | }
491 |
492 | /**
493 | * Get the visible properties for the model.
494 | *
495 | * @return array
496 | */
497 | public function getVisible()
498 | {
499 | return $this->visible;
500 | }
501 |
502 | /**
503 | * Set the visible properties for the model.
504 | *
505 | * @param array $visible
506 | * @return $this
507 | */
508 | public function setVisible(array $visible)
509 | {
510 | $this->visible = $visible;
511 |
512 | return $this;
513 | }
514 |
515 | /**
516 | * Add visible properties for the model.
517 | *
518 | * @param array|string|null $properties
519 | * @return void
520 | */
521 | public function addVisible($properties = null)
522 | {
523 | $properties = is_array($properties) ? $properties : func_get_args();
524 |
525 | $this->visible = array_merge($this->visible, $properties);
526 | }
527 |
528 | /**
529 | * Set the accessors to append to entity arrays.
530 | *
531 | * @param array $appends
532 | * @return $this
533 | */
534 | public function setAppends(array $appends)
535 | {
536 | $this->appends = $appends;
537 |
538 | return $this;
539 | }
540 |
541 | /**
542 | * Get the mutated properties for a given instance.
543 | *
544 | * @return array
545 | */
546 | public function getMutatedProperties()
547 | {
548 | $class = static::class;
549 |
550 | if (!isset(static::$mutatorCache[$class])) {
551 | static::cacheMutatedProperties($class);
552 | }
553 |
554 | return static::$mutatorCache[$class];
555 | }
556 |
557 | /**
558 | * Extract and cache all the mutated properties of a class.
559 | *
560 | * @param string $class
561 | * @return void
562 | */
563 | public static function cacheMutatedProperties($class)
564 | {
565 | static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->map(function ($match) {
566 | return lcfirst(static::$snakePropreties ? Str::snake($match) : $match);
567 | })->all();
568 | }
569 |
570 | /**
571 | * Get all of the property mutator methods.
572 | *
573 | * @param mixed $class
574 | * @return array
575 | */
576 | protected static function getMutatorMethods($class)
577 | {
578 | preg_match_all('/(?<=^|;)get([^;]+?)Property(;|$)/', implode(';', get_class_methods($class)), $matches);
579 |
580 | return $matches[1];
581 | }
582 |
583 | /**
584 | * Get the fillable properties for the model.
585 | *
586 | * @return array
587 | */
588 | public function getFillable()
589 | {
590 | return $this->fillable;
591 | }
592 |
593 | /**
594 | * Set the fillable properties for the model.
595 | *
596 | * @param array $fillable
597 | * @return $this
598 | */
599 | public function fillable(array $fillable)
600 | {
601 | $this->fillable = $fillable;
602 |
603 | return $this;
604 | }
605 |
606 | /**
607 | * Get the guarded properties for the model.
608 | *
609 | * @return array
610 | */
611 | public function getGuarded()
612 | {
613 | return $this->guarded;
614 | }
615 |
616 | /**
617 | * Set the guarded properties for the model.
618 | *
619 | * @param array $guarded
620 | * @return $this
621 | */
622 | public function guard(array $guarded)
623 | {
624 | $this->guarded = $guarded;
625 |
626 | return $this;
627 | }
628 |
629 | /**
630 | * Disable all mass assignable restrictions.
631 | *
632 | * @param bool $state
633 | * @return void
634 | */
635 | public static function unguard($state = true)
636 | {
637 | static::$unguarded = $state;
638 | }
639 |
640 | /**
641 | * Enable the mass assignment restrictions.
642 | *
643 | * @return void
644 | */
645 | public static function reguard()
646 | {
647 | static::$unguarded = false;
648 | }
649 |
650 | /**
651 | * Determine if current state is "unguarded".
652 | *
653 | * @return bool
654 | */
655 | public static function isUnguarded()
656 | {
657 | return static::$unguarded;
658 | }
659 |
660 | /**
661 | * Run the given callable while being unguarded.
662 | *
663 | * @param callable $callback
664 | * @return mixed
665 | */
666 | public static function unguarded(callable $callback)
667 | {
668 | if (static::$unguarded) {
669 | return $callback();
670 | }
671 |
672 | static::unguard();
673 |
674 | try {
675 | return $callback();
676 | } finally {
677 | static::reguard();
678 | }
679 | }
680 |
681 | /**
682 | * Determine if the given attribute may be mass assigned.
683 | *
684 | * @param string $key
685 | * @return bool
686 | */
687 | public function isFillable($key)
688 | {
689 | if (static::$unguarded) {
690 | return true;
691 | }
692 |
693 | // If the key is in the "fillable" array, we can of course assume that it's
694 | // a fillable attribute. Otherwise, we will check the guarded array when
695 | // we need to determine if the attribute is black-listed on the model.
696 | if (in_array($key, $this->getFillable())) {
697 | return true;
698 | }
699 |
700 | if ($this->isGuarded($key)) {
701 | return false;
702 | }
703 |
704 | return empty($this->getFillable());
705 | }
706 |
707 | /**
708 | * Determine if the given key is guarded.
709 | *
710 | * @param string $key
711 | * @return bool
712 | */
713 | public function isGuarded($key)
714 | {
715 | return in_array($key, $this->getGuarded()) || $this->getGuarded() == ['*'];
716 | }
717 |
718 | /**
719 | * Determine if the model is totally guarded.
720 | *
721 | * @return bool
722 | */
723 | public function totallyGuarded()
724 | {
725 | return count($this->getFillable()) == 0 && $this->getGuarded() == ['*'];
726 | }
727 |
728 | /**
729 | * Gets the property dictionary of the Entity
730 | *
731 | * @return array The list of properties
732 | */
733 | public function getProperties()
734 | {
735 | return $this->properties;
736 | }
737 |
738 | /**
739 | * Set the array of entity properties. No checking is done.
740 | *
741 | * @param array $properties
742 | * @param bool $sync
743 | * @return $this
744 | */
745 | public function setRawProperties(array $properties, $sync = false)
746 | {
747 | $this->properties = $properties;
748 |
749 | if ($sync) {
750 | $this->syncOriginal();
751 | }
752 |
753 | return $this;
754 | }
755 |
756 | /**
757 | * Get the entity's original property values.
758 | *
759 | * @param string|null $key
760 | * @param mixed $default
761 | * @return mixed|array
762 | */
763 | public function getOriginal($key = null, $default = null)
764 | {
765 | return Arr::get($this->original, $key, $default);
766 | }
767 |
768 | /**
769 | * Sync the original properties with the current.
770 | *
771 | * @return $this
772 | */
773 | public function syncOriginal()
774 | {
775 | $this->original = $this->properties;
776 |
777 | return $this;
778 | }
779 |
780 | /**
781 | * Sync a single original property with its current value.
782 | *
783 | * @param string $property
784 | * @return $this
785 | */
786 | public function syncOriginalProperty($property)
787 | {
788 | $this->original[$property] = $this->properties[$property];
789 |
790 | return $this;
791 | }
792 |
793 | /**
794 | * Dynamically retrieve properties on the entity.
795 | *
796 | * @param string $key
797 | * @return mixed
798 | */
799 | public function __get($key)
800 | {
801 | return $this->getProperty($key);
802 | }
803 |
804 | /**
805 | * Dynamically set properties on the entity.
806 | *
807 | * @param string $key
808 | * @param mixed $value
809 | * @return void
810 | */
811 | public function __set($key, $value)
812 | {
813 | $this->setProperty($key, $value);
814 | }
815 |
816 | /**
817 | * Determine if the given attribute exists.
818 | *
819 | * @param mixed $offset
820 | * @return bool
821 | */
822 | public function offsetExists($offset): bool
823 | {
824 | return isset($this->$offset);
825 | }
826 |
827 | /**
828 | * Get the value for a given offset.
829 | *
830 | * @param mixed $offset
831 | * @return mixed
832 | */
833 | #[\ReturnTypeWillChange]
834 | public function offsetGet($offset)
835 | {
836 | return $this->$offset;
837 | }
838 |
839 | /**
840 | * Set the value for a given offset.
841 | *
842 | * @param mixed $offset
843 | * @param mixed $value
844 | * @return void
845 | */
846 | public function offsetSet($offset, $value): void
847 | {
848 | $this->$offset = $value;
849 | }
850 |
851 | /**
852 | * Unset the value for a given offset.
853 | *
854 | * @param mixed $offset
855 | * @return void
856 | */
857 | public function offsetUnset($offset): void
858 | {
859 | unset($this->$offset);
860 | }
861 |
862 | /**
863 | * Determine if a property or relation exists on the model.
864 | *
865 | * @param string $key
866 | * @return bool
867 | */
868 | public function __isset($key)
869 | {
870 | return !is_null($this->getProperty($key));
871 | }
872 |
873 | /**
874 | * Unset a property on the model.
875 | *
876 | * @param string $key
877 | * @return void
878 | */
879 | public function __unset($key)
880 | {
881 | unset($this->properties[$key], $this->relations[$key]);
882 | }
883 |
884 | /**
885 | * Determine whether a property should be cast to a native type.
886 | *
887 | * @param string $key
888 | * @param array|string|null $types
889 | * @return bool
890 | */
891 | public function hasCast($key, $types = null)
892 | {
893 | if (array_key_exists($key, $this->getCasts())) {
894 | return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
895 | }
896 |
897 | return false;
898 | }
899 |
900 | /**
901 | * Get the casts array.
902 | *
903 | * @return array
904 | */
905 | public function getCasts()
906 | {
907 | // if ($this->getIncrementing()) {
908 | // return array_merge([
909 | // $this->getKeyName() => $this->keyType,
910 | // ], $this->casts);
911 | // }
912 |
913 | return $this->casts;
914 | }
915 |
916 | /**
917 | * Determine whether a value is Date / DateTime castable for inbound manipulation.
918 | *
919 | * @param string $key
920 | * @return bool
921 | */
922 | protected function isDateCastable($key)
923 | {
924 | return $this->hasCast($key, ['date', 'datetime']);
925 | }
926 |
927 | /**
928 | * Determine whether a value is JSON castable for inbound manipulation.
929 | *
930 | * @param string $key
931 | * @return bool
932 | */
933 | protected function isJsonCastable($key)
934 | {
935 | return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
936 | }
937 |
938 | /**
939 | * Get the type of cast for a entity property.
940 | *
941 | * @param string $key
942 | * @return string
943 | */
944 | protected function getCastType($key)
945 | {
946 | return trim(strtolower($this->getCasts()[$key]));
947 | }
948 |
949 | /**
950 | * Cast a property to a native PHP type.
951 | *
952 | * @param string $key
953 | * @param mixed $value
954 | * @return mixed
955 | */
956 | protected function castProperty($key, $value)
957 | {
958 | if (is_null($value)) {
959 | return $value;
960 | }
961 |
962 | switch ($this->getCastType($key)) {
963 | case 'int':
964 | case 'integer':
965 | return (int) $value;
966 | case 'real':
967 | case 'float':
968 | case 'double':
969 | return $this->fromFloat($value);
970 | case 'decimal':
971 | return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
972 | case 'string':
973 | return (string) $value;
974 | case 'bool':
975 | case 'boolean':
976 | return (bool) $value;
977 | case 'object':
978 | return $this->fromJson($value, true);
979 | case 'array':
980 | case 'json':
981 | return $this->fromJson($value);
982 | //case 'collection':
983 | //return new BaseCollection($this->fromJson($value));
984 | case 'date':
985 | return $this->asDate($value);
986 | case 'datetime':
987 | case 'custom_datetime':
988 | return $this->asDateTime($value);
989 | case 'timestamp':
990 | return $this->asTimeStamp($value);
991 | default:
992 | return $value;
993 | }
994 | }
995 |
996 | /**
997 | * Set a given property on the entity.
998 | *
999 | * @param string $key
1000 | * @param mixed $value
1001 | * @return $this
1002 | */
1003 | public function setProperty($key, $value)
1004 | {
1005 | // First we will check for the presence of a mutator for the set operation
1006 | // which simply lets the developers tweak the property as it is set on
1007 | // the entity, such as "json_encoding" a listing of data for storage.
1008 | if ($this->hasSetMutator($key)) {
1009 | $method = 'set' . Str::studly($key) . 'Property';
1010 |
1011 | return $this->{$method}($value);
1012 | }
1013 |
1014 | // If an attribute is listed as a "date", we'll convert it from a DateTime
1015 | // instance into a form proper for storage on the database tables using
1016 | // the connection grammar's date format. We will auto set the values.
1017 | elseif ($value && (in_array($key, $this->getDates()) || $this->isDateCastable($key))) {
1018 | $value = $this->fromDateTime($value);
1019 | }
1020 |
1021 | if ($this->isJsonCastable($key) && !is_null($value)) {
1022 | $value = $this->asJson($value);
1023 | }
1024 |
1025 | // If this attribute contains a JSON ->, we'll set the proper value in the
1026 | // attribute's underlying array. This takes care of properly nesting an
1027 | // attribute in the array's value in the case of deeply nested items.
1028 | if (Str::contains($key, '->')) {
1029 | return $this->fillJsonAttribute($key, $value);
1030 | }
1031 |
1032 | $this->properties[$key] = $value;
1033 |
1034 | return $this;
1035 | }
1036 |
1037 | /**
1038 | * Determine if a set mutator exists for a property.
1039 | *
1040 | * @param string $key
1041 | * @return bool
1042 | */
1043 | public function hasSetMutator($key)
1044 | {
1045 | return method_exists($this, 'set' . Str::studly($key) . 'Property');
1046 | }
1047 |
1048 | /**
1049 | * Get the properties that should be converted to dates.
1050 | *
1051 | * @return array
1052 | */
1053 | public function getDates()
1054 | {
1055 | // $defaults = [static::CREATED_AT, static::UPDATED_AT];
1056 |
1057 | //return $this->timestamps ? array_merge($this->dates, $defaults) : $this->dates;
1058 | return $this->dates;
1059 | }
1060 |
1061 | /**
1062 | * Return a timestamp as DateTime object with time set to 00:00:00.
1063 | *
1064 | * @param mixed $value
1065 | * @return \Illuminate\Support\Carbon
1066 | */
1067 | protected function asDate($value)
1068 | {
1069 | return $this->asDateTime($value)->startOfDay();
1070 | }
1071 |
1072 | /**
1073 | * Return a timestamp as DateTime object.
1074 | *
1075 | * @param mixed $value
1076 | * @return \Illuminate\Support\Carbon
1077 | */
1078 | protected function asDateTime($value)
1079 | {
1080 | // If this value is already a Carbon instance, we shall just return it as is.
1081 | // This prevents us having to re-instantiate a Carbon instance when we know
1082 | // it already is one, which wouldn't be fulfilled by the DateTime check.
1083 | if ($value instanceof CarbonInterface) {
1084 | return Date::instance($value);
1085 | }
1086 | // If the value is already a DateTime instance, we will just skip the rest of
1087 | // these checks since they will be a waste of time, and hinder performance
1088 | // when checking the field. We will just return the DateTime right away.
1089 | if ($value instanceof DateTimeInterface) {
1090 | return Date::parse(
1091 | $value->format('Y-m-d H:i:s.u'),
1092 | $value->getTimezone()
1093 | );
1094 | }
1095 | // If this value is an integer, we will assume it is a UNIX timestamp's value
1096 | // and format a Carbon object from this timestamp. This allows flexibility
1097 | // when defining your date fields as they might be UNIX timestamps here.
1098 | if (is_numeric($value)) {
1099 | return Date::createFromTimestamp($value);
1100 | }
1101 | // If the value is in simply year, month, day format, we will instantiate the
1102 | // Carbon instances from that format. Again, this provides for simple date
1103 | // fields on the database, while still supporting Carbonized conversion.
1104 | if ($this->isStandardDateFormat($value)) {
1105 | return Date::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay());
1106 | }
1107 | $format = $this->getDateFormat();
1108 | // https://bugs.php.net/bug.php?id=75577
1109 | if (version_compare(PHP_VERSION, '7.3.0-dev', '<')) {
1110 | $format = str_replace('.v', '.u', $format);
1111 | }
1112 | // Finally, we will just assume this date is in the format used by default on
1113 | // the database connection and use that format to create the Carbon object
1114 | // that is returned back out to the developers after we convert it here.
1115 | return Date::createFromFormat($format, $value);
1116 | }
1117 |
1118 | /**
1119 | * Determine if the given value is a standard date format.
1120 | *
1121 | * @param string $value
1122 | * @return bool
1123 | */
1124 | protected function isStandardDateFormat($value)
1125 | {
1126 | return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value);
1127 | }
1128 |
1129 | /**
1130 | * Convert a DateTime to a storable string.
1131 | *
1132 | * @param mixed $value
1133 | * @return string|null
1134 | */
1135 | public function fromDateTime($value)
1136 | {
1137 | return empty($value) ? $value : $this->asDateTime($value)->format(
1138 | $this->getDateFormat()
1139 | );
1140 | }
1141 |
1142 | /**
1143 | * Return a timestamp as unix timestamp.
1144 | *
1145 | * @param mixed $value
1146 | * @return int
1147 | */
1148 | protected function asTimeStamp($value)
1149 | {
1150 | return $this->asDateTime($value)->getTimestamp();
1151 | }
1152 |
1153 | /**
1154 | * Prepare a date for array / JSON serialization.
1155 | *
1156 | * @param \DateTimeInterface $date
1157 | * @return string
1158 | */
1159 | protected function serializeDate(DateTimeInterface $date)
1160 | {
1161 | return $date->format($this->getDateFormat());
1162 | }
1163 |
1164 | /**
1165 | * Get the format for database stored dates.
1166 | *
1167 | * @return string
1168 | */
1169 | protected function getDateFormat()
1170 | {
1171 | return $this->dateFormat; // ?: $this->getConnection()->getQueryGrammar()->getDateFormat();
1172 | }
1173 |
1174 | /**
1175 | * Set the date format used by the model.
1176 | *
1177 | * @param string $format
1178 | * @return $this
1179 | */
1180 | public function setDateFormat($format)
1181 | {
1182 | $this->dateFormat = $format;
1183 |
1184 | return $this;
1185 | }
1186 |
1187 | /**
1188 | * Encode the given value as JSON.
1189 | *
1190 | * @param mixed $value
1191 | * @return string
1192 | */
1193 | protected function asJson($value)
1194 | {
1195 | return json_encode($value);
1196 | }
1197 |
1198 | /**
1199 | * Decode the given JSON back into an array or object.
1200 | *
1201 | * @param string $value
1202 | * @param bool $asObject
1203 | * @return mixed
1204 | */
1205 | public function fromJson($value, $asObject = false)
1206 | {
1207 | return json_decode($value, !$asObject);
1208 | }
1209 |
1210 | /**
1211 | * Decode the given float.
1212 | *
1213 | * @param mixed $value
1214 | * @return mixed
1215 | */
1216 | public function fromFloat($value)
1217 | {
1218 | switch ((string) $value) {
1219 | case 'Infinity':
1220 | return INF;
1221 | case '-Infinity':
1222 | return -INF;
1223 | case 'NaN':
1224 | return NAN;
1225 | default:
1226 | return (float) $value;
1227 | }
1228 | }
1229 |
1230 | /**
1231 | * Return a decimal as string.
1232 | *
1233 | * @param float $value
1234 | * @param int $decimals
1235 | * @return string
1236 | */
1237 | protected function asDecimal($value, $decimals)
1238 | {
1239 | return number_format($value, $decimals, '.', '');
1240 | }
1241 |
1242 | /**
1243 | * Convert the model instance to JSON.
1244 | *
1245 | * @param int $options
1246 | * @return string
1247 | */
1248 | public function toJson($options = 0)
1249 | {
1250 | return json_encode($this->jsonSerialize(), $options);
1251 | }
1252 |
1253 | /**
1254 | * Convert the object into something JSON serializable.
1255 | *
1256 | * @return array
1257 | */
1258 | public function jsonSerialize()
1259 | {
1260 | return $this->toArray();
1261 | }
1262 |
1263 | /**
1264 | * Convert the model instance to an array.
1265 | *
1266 | * @return array
1267 | */
1268 | public function toArray()
1269 | {
1270 | return array_merge($this->propertiesToArray(), $this->relationsToArray());
1271 | }
1272 |
1273 | /**
1274 | * Convert the model's properties to an array.
1275 | *
1276 | * @return array
1277 | */
1278 | public function propertiesToArray()
1279 | {
1280 | // If a property is a date, we will cast it to a string after converting it
1281 | // to a DateTime / Carbon instance. This is so we will get some consistent
1282 | // formatting while accessing properties vs. arraying / JSONing a model.
1283 | $properties = $this->addDatePropertiesToArray(
1284 | $properties = $this->getArrayableProperties()
1285 | );
1286 |
1287 | $properties = $this->addMutatedPropertiesToArray(
1288 | $properties,
1289 | $mutatedProperties = $this->getMutatedProperties()
1290 | );
1291 |
1292 | // Next we will handle any casts that have been setup for this entity and cast
1293 | // the values to their appropriate type. If the property has a mutator we
1294 | // will not perform the cast on those properties to avoid any confusion.
1295 | $properties = $this->addCastPropertiesToArray(
1296 | $properties,
1297 | $mutatedProperties
1298 | );
1299 |
1300 | // Here we will grab all of the appended, calculated properties to this model
1301 | // as these properties are not really in the properties array, but are run
1302 | // when we need to array or JSON the model for convenience to the coder.
1303 | foreach ($this->getArrayableAppends() as $key) {
1304 | $properties[$key] = $this->mutatePropertyForArray($key, null);
1305 | }
1306 |
1307 | return $properties;
1308 | }
1309 |
1310 | /**
1311 | * Add the date properties to the properties array.
1312 | *
1313 | * @param array $properties
1314 | * @return array
1315 | */
1316 | protected function addDatePropertiesToArray(array $properties)
1317 | {
1318 | foreach ($this->getDates() as $key) {
1319 | if (!isset($properties[$key])) {
1320 | continue;
1321 | }
1322 |
1323 | $properties[$key] = $this->serializeDate(
1324 | $this->asDateTime($properties[$key])
1325 | );
1326 | }
1327 |
1328 | return $properties;
1329 | }
1330 |
1331 | /**
1332 | * Add the mutated properties to the properties array.
1333 | *
1334 | * @param array $properties
1335 | * @param array $mutatedProperties
1336 | * @return array
1337 | */
1338 | protected function addMutatedPropertiesToArray(array $properties, array $mutatedProperties)
1339 | {
1340 | foreach ($mutatedProperties as $key) {
1341 | // We want to spin through all the mutated properties for this model and call
1342 | // the mutator for the properties. We cache off every mutated properties so
1343 | // we don't have to constantly check on properties that actually change.
1344 | if (!array_key_exists($key, $properties)) {
1345 | continue;
1346 | }
1347 |
1348 | // Next, we will call the mutator for this properties so that we can get these
1349 | // mutated property's actual values. After we finish mutating each of the
1350 | // properties we will return this final array of the mutated properties.
1351 | $properties[$key] = $this->mutatePropertyForArray(
1352 | $key,
1353 | $properties[$key]
1354 | );
1355 | }
1356 |
1357 | return $properties;
1358 | }
1359 |
1360 | /**
1361 | * Add the casted properties to the properties array.
1362 | *
1363 | * @param array $properties
1364 | * @param array $mutatedProperties
1365 | * @return array
1366 | */
1367 | protected function addCastPropertiesToArray(array $properties, array $mutatedProperties)
1368 | {
1369 | foreach ($this->getCasts() as $key => $value) {
1370 | if (!array_key_exists($key, $properties) || in_array($key, $mutatedProperties)) {
1371 | continue;
1372 | }
1373 |
1374 | // Here we will cast the property. Then, if the cast is a date or datetime cast
1375 | // then we will serialize the date for the array. This will convert the dates
1376 | // to strings based on the date format specified for these Entity models.
1377 | $properties[$key] = $this->castProperty(
1378 | $key,
1379 | $properties[$key]
1380 | );
1381 |
1382 | // If the property cast was a date or a datetime, we will serialize the date as
1383 | // a string. This allows the developers to customize how dates are serialized
1384 | // into an array without affecting how they are persisted into the storage.
1385 | if (
1386 | $properties[$key] &&
1387 | ($value === 'date' || $value === 'datetime')
1388 | ) {
1389 | $properties[$key] = $this->serializeDate($properties[$key]);
1390 | }
1391 | }
1392 |
1393 | return $properties;
1394 | }
1395 |
1396 | /**
1397 | * Get a property array of all arrayable properties.
1398 | *
1399 | * @return array
1400 | */
1401 | protected function getArrayableProperties()
1402 | {
1403 | return $this->getArrayableItems($this->properties);
1404 | }
1405 |
1406 | /**
1407 | * Get all of the appendable values that are arrayable.
1408 | *
1409 | * @return array
1410 | */
1411 | protected function getArrayableAppends()
1412 | {
1413 | if (!count($this->appends)) {
1414 | return [];
1415 | }
1416 |
1417 | return $this->getArrayableItems(
1418 | array_combine($this->appends, $this->appends)
1419 | );
1420 | }
1421 |
1422 | /**
1423 | * Get the model's relationships in array form.
1424 | *
1425 | * @return array
1426 | */
1427 | public function relationsToArray()
1428 | {
1429 | $properties = [];
1430 |
1431 | foreach ($this->getArrayableRelations() as $key => $value) {
1432 | // If the values implements the Arrayable interface we can just call this
1433 | // toArray method on the instances which will convert both models and
1434 | // collections to their proper array form and we'll set the values.
1435 | if ($value instanceof Arrayable) {
1436 | $relation = $value->toArray();
1437 | }
1438 |
1439 | // If the value is null, we'll still go ahead and set it in this list of
1440 | // properties since null is used to represent empty relationships if
1441 | // if it a has one or belongs to type relationships on the models.
1442 | elseif (is_null($value)) {
1443 | $relation = $value;
1444 | }
1445 |
1446 | // If the relationships snake-casing is enabled, we will snake case this
1447 | // key so that the relation property is snake cased in this returned
1448 | // array to the developers, making this consistent with properties.
1449 | // if (static::$snakeproperties) {
1450 | // $key = Str::snake($key);
1451 | // }
1452 |
1453 | // If the relation value has been set, we will set it on this properties
1454 | // list for returning. If it was not arrayable or null, we'll not set
1455 | // the value on the array because it is some type of invalid value.
1456 | if (isset($relation) || is_null($value)) {
1457 | $properties[$key] = $relation;
1458 | }
1459 |
1460 | unset($relation);
1461 | }
1462 |
1463 | return $properties;
1464 | }
1465 |
1466 | /**
1467 | * Get a property array of all arrayable relations.
1468 | *
1469 | * @return array
1470 | */
1471 | protected function getArrayableRelations()
1472 | {
1473 | return $this->getArrayableItems($this->relations);
1474 | }
1475 |
1476 | /**
1477 | * Get a property array of all arrayable values.
1478 | *
1479 | * @param array $values
1480 | * @return array
1481 | */
1482 | protected function getArrayableItems(array $values)
1483 | {
1484 | if (count($this->getVisible()) > 0) {
1485 | $values = array_intersect_key($values, array_flip($this->getVisible()));
1486 | }
1487 |
1488 | if (count($this->getHidden()) > 0) {
1489 | $values = array_diff_key($values, array_flip($this->getHidden()));
1490 | }
1491 |
1492 | return $values;
1493 | }
1494 |
1495 | /**
1496 | * Get a property from the entity.
1497 | *
1498 | * @param string $key
1499 | * @return mixed
1500 | */
1501 | public function getProperty($key)
1502 | {
1503 | if (!$key) {
1504 | return;
1505 | }
1506 |
1507 | if ($key === 'id') {
1508 | $key = $this->primaryKey;
1509 | }
1510 |
1511 | // If the property exists in the properties array or has a "get" mutator we will
1512 | // get the property's value. Otherwise, we will proceed as if the developers
1513 | // are asking for a relationship's value. This covers both types of values.
1514 | if (
1515 | array_key_exists($key, $this->properties) ||
1516 | $this->hasGetMutator($key)
1517 | ) {
1518 | return $this->getPropertyValue($key);
1519 | }
1520 |
1521 | // Here we will determine if the model base class itself contains this given key
1522 | // since we don't want to treat any of those methods as relationships because
1523 | // they are all intended as helper methods and none of these are relations.
1524 | if (method_exists(self::class, $key)) {
1525 | return;
1526 | }
1527 |
1528 | // return $this->getRelationValue($key);
1529 | return null;
1530 | }
1531 |
1532 | /**
1533 | * Get a plain property (not a relationship).
1534 | *
1535 | * @param string $key
1536 | * @return mixed
1537 | */
1538 | public function getPropertyValue($key)
1539 | {
1540 | $value = $this->getPropertyFromArray($key);
1541 |
1542 | // If the property has a get mutator, we will call that then return what
1543 | // it returns as the value, which is useful for transforming values on
1544 | // retrieval from the model to a form that is more useful for usage.
1545 | if ($this->hasGetMutator($key)) {
1546 | return $this->mutateProperty($key, $value);
1547 | }
1548 |
1549 | // If the property exists within the cast array, we will convert it to
1550 | // an appropriate native PHP type dependant upon the associated value
1551 | // given with the key in the pair. Dayle made this comment line up.
1552 | if ($this->hasCast($key)) {
1553 | return $this->castProperty($key, $value);
1554 | }
1555 |
1556 | // If the property is listed as a date, we will convert it to a DateTime
1557 | // instance on retrieval, which makes it quite convenient to work with
1558 | // date fields without having to create a mutator for each property.
1559 | if (
1560 | in_array($key, $this->getDates()) &&
1561 | !is_null($value)
1562 | ) {
1563 | return $this->asDateTime($value);
1564 | }
1565 |
1566 | return $value;
1567 | }
1568 |
1569 | /**
1570 | * Get a property from the $properties array.
1571 | *
1572 | * @param string $key
1573 | * @return mixed
1574 | */
1575 | protected function getPropertyFromArray($key)
1576 | {
1577 | if (isset($this->properties[$key])) {
1578 | return $this->properties[$key];
1579 | }
1580 | }
1581 |
1582 | /**
1583 | * Determine if a get mutator exists for a property.
1584 | *
1585 | * @param string $key
1586 | * @return bool
1587 | */
1588 | public function hasGetMutator($key)
1589 | {
1590 | return method_exists($this, 'get' . Str::studly($key) . 'Property');
1591 | //return method_exists($this, 'get_'.$key);
1592 | }
1593 |
1594 | /**
1595 | * Get the value of a property using its mutator.
1596 | *
1597 | * @param string $key
1598 | * @param mixed $value
1599 | * @return mixed
1600 | */
1601 | protected function mutateProperty($key, $value)
1602 | {
1603 | return $this->{'get' . Str::studly($key) . 'Property'}($value);
1604 | // return $this->{'get_'.$key}($value);
1605 | }
1606 |
1607 | /**
1608 | * Get the value of a property using its mutator for array conversion.
1609 | *
1610 | * @param string $key
1611 | * @param mixed $value
1612 | * @return mixed
1613 | */
1614 | protected function mutatePropertyForArray($key, $value)
1615 | {
1616 | $value = $this->mutateProperty($key, $value);
1617 |
1618 | return $value instanceof Arrayable ? $value->toArray() : $value;
1619 | }
1620 | }
1621 |
--------------------------------------------------------------------------------
/src/Exception/ApplicationException.php:
--------------------------------------------------------------------------------
1 | code}]: {$this->message}\n";
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Exception/MassAssignmentException.php:
--------------------------------------------------------------------------------
1 | code}]: {$this->message}\n";
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Exception/ODataQueryException.php:
--------------------------------------------------------------------------------
1 | code}]: {$this->message}\n";
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/GuzzleHttpProvider.php:
--------------------------------------------------------------------------------
1 | http = new Client($config);
33 | $this->timeout = 0;
34 | $this->extra_options = array();
35 | }
36 |
37 | /**
38 | * Gets the timeout limit of the cURL request
39 | * @return integer The timeout in ms
40 | */
41 | public function getTimeout()
42 | {
43 | return $this->timeout;
44 | }
45 |
46 | /**
47 | * Sets the timeout limit of the cURL request
48 | *
49 | * @param integer $timeout The timeout in ms
50 | *
51 | * @return $this
52 | */
53 | public function setTimeout($timeout)
54 | {
55 | $this->timeout = $timeout;
56 | return $this;
57 | }
58 |
59 | /**
60 | * Configures the default options for the client.
61 | *
62 | * @param array $config
63 | */
64 | public function configureDefaults($config)
65 | {
66 | $this->http->configureDefaults($config);
67 | }
68 |
69 | public function setExtraOptions($options)
70 | {
71 | $this->extra_options = $options;
72 | }
73 |
74 | /**
75 | * Executes the HTTP request using Guzzle
76 | *
77 | * @param HttpRequestMessage $request
78 | *
79 | * @return mixed object or array of objects
80 | * of class $returnType
81 | */
82 | public function send(HttpRequestMessage $request)
83 | {
84 | $options = [
85 | 'headers' => $request->headers,
86 | 'stream' => $request->returnsStream,
87 | 'timeout' => $this->timeout
88 | ];
89 |
90 | foreach ($this->extra_options as $key => $value)
91 | {
92 | $options[$key] = $value;
93 | }
94 |
95 | if ($request->method == HttpMethod::POST || $request->method == HttpMethod::PUT || $request->method == HttpMethod::PATCH) {
96 | $options['body'] = $request->body;
97 | }
98 |
99 | $result = $this->http->request(
100 | $request->method,
101 | $request->requestUri,
102 | $options
103 | );
104 |
105 | return $result;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/HeaderOption.php:
--------------------------------------------------------------------------------
1 | value;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/HttpMethod.php:
--------------------------------------------------------------------------------
1 | value();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/HttpRequestMessage.php:
--------------------------------------------------------------------------------
1 | method = (string)$method;
52 | $this->requestUri = $requestUri;
53 | $this->headers = [];
54 | $this->returnsStream = false;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/HttpStatusCode.php:
--------------------------------------------------------------------------------
1 | value();
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/IAuthenticationProvider.php:
--------------------------------------------------------------------------------
1 |
8 | /// Gets a serializer for serializing and deserializing JSON objects.
9 | ///
10 | //ISerializer Serializer { get; }
11 |
12 | /**
13 | * Sends the request.
14 | * @param HttpRequestMessage $request The HttpRequestMessage to send.
15 | *
16 | * @return mixed object or array of objects
17 | */
18 | public function send(HttpRequestMessage $request);
19 |
20 | ///
21 | /// Sends the request.
22 | ///
23 | /// The to send.
24 | /// The to pass to the on send.
25 | /// The for the request.
26 | /// The .
27 | // public function sendAsync(
28 | // HttpRequestMessage request,
29 | // HttpCompletionOption completionOption,
30 | // CancellationToken cancellationToken);
31 | }
32 |
--------------------------------------------------------------------------------
/src/IODataClient.php:
--------------------------------------------------------------------------------
1 |
8 | /// Gets or sets the content type for the request.
9 | ///
10 | // string ContentType { get; set; }
11 |
12 | // ///
13 | // /// Gets the collection for the request.
14 | // ///
15 | // IList Headers { get; }
16 |
17 | // ///
18 | // /// Gets the for handling requests.
19 | // ///
20 | // IBaseClient Client { get; }
21 |
22 | // ///
23 | // /// Gets or sets the HTTP method string for the request.
24 | // ///
25 | // string Method { get; }
26 |
27 | // ///
28 | // /// Gets the URL for the request, without query string.
29 | // ///
30 | // string RequestUrl { get; }
31 |
32 | ///
33 | /// Gets the collection for the request.
34 | ///
35 | //public function getQueryOptions();
36 |
37 | ///
38 | /// Gets the representation of the request.
39 | ///
40 | /// The representation of the request.
41 | public function getHttpRequestMessage();
42 | }
43 |
--------------------------------------------------------------------------------
/src/ODataClient.php:
--------------------------------------------------------------------------------
1 | setBaseUrl($baseUrl);
81 | $this->authenticationProvider = $authenticationProvider;
82 | $this->httpProvider = $httpProvider ?: new GuzzleHttpProvider();
83 |
84 | // We need to initialize a query grammar and the query post processors
85 | // which are both very important parts of the OData abstractions
86 | // so we initialize these to their default values while starting.
87 | $this->useDefaultQueryGrammar();
88 |
89 | $this->useDefaultPostProcessor();
90 | }
91 |
92 | /**
93 | * Set the query grammar to the default implementation.
94 | *
95 | * @return void
96 | */
97 | public function useDefaultQueryGrammar()
98 | {
99 | $this->queryGrammar = $this->getDefaultQueryGrammar();
100 | }
101 |
102 | /**
103 | * Get the default query grammar instance.
104 | *
105 | * @return IGrammar
106 | */
107 | protected function getDefaultQueryGrammar()
108 | {
109 | return new Grammar;
110 | }
111 |
112 | /**
113 | * Set the query post processor to the default implementation.
114 | *
115 | * @return void
116 | */
117 | public function useDefaultPostProcessor()
118 | {
119 | $this->postProcessor = $this->getDefaultPostProcessor();
120 | }
121 |
122 | /**
123 | * Get the default post processor instance.
124 | *
125 | * @return IProcessor
126 | */
127 | protected function getDefaultPostProcessor()
128 | {
129 | return new Processor();
130 | }
131 |
132 | /**
133 | * Gets the IAuthenticationProvider for authenticating requests.
134 | *
135 | * @return Closure|IAuthenticationProvider
136 | */
137 | public function getAuthenticationProvider()
138 | {
139 | return $this->authenticationProvider;
140 | }
141 |
142 | /**
143 | * Gets the base URL for requests of the client.
144 | *
145 | * @return string
146 | */
147 | public function getBaseUrl()
148 | {
149 | return $this->baseUrl;
150 | }
151 |
152 | /**
153 | * Sets the base URL for requests of the client.
154 | * @param mixed $value
155 | *
156 | * @throws ODataException
157 | */
158 | public function setBaseUrl($value)
159 | {
160 | if (empty($value)) {
161 | throw new ODataException(Constants::BASE_URL_MISSING);
162 | }
163 |
164 | $this->baseUrl = rtrim($value, '/') . '/';
165 | }
166 |
167 | /**
168 | * Gets the IHttpProvider for sending HTTP requests.
169 | *
170 | * @return IHttpProvider
171 | */
172 | public function getHttpProvider()
173 | {
174 | return $this->httpProvider;
175 | }
176 |
177 | /**
178 | * Begin a fluent query against an odata service
179 | *
180 | * @param string $entitySet
181 | *
182 | * @return Builder
183 | */
184 | public function from($entitySet)
185 | {
186 | return $this->query()->from($entitySet);
187 | }
188 |
189 | /**
190 | * Begin a fluent query against an odata service
191 | *
192 | * @param array $properties
193 | *
194 | * @return Builder
195 | */
196 | public function select($properties = [])
197 | {
198 | $properties = is_array($properties) ? $properties : func_get_args();
199 |
200 | return $this->query()->select($properties);
201 | }
202 |
203 | /**
204 | * Get a new query builder instance.
205 | *
206 | * @return Builder
207 | */
208 | public function query()
209 | {
210 | return new Builder(
211 | $this, $this->getQueryGrammar(), $this->getPostProcessor()
212 | );
213 | }
214 |
215 | /**
216 | * Run a GET HTTP request against the service.
217 | *
218 | * @param string $requestUri
219 | * @param array $bindings
220 | *
221 | * @return IODataRequest
222 | */
223 | public function get($requestUri, $bindings = [])
224 | {
225 | list($response, $nextPage) = $this->getNextPage($requestUri, $bindings);
226 | return $response;
227 | }
228 |
229 | /**
230 | * Run a GET HTTP request against the service.
231 | *
232 | * @param string $requestUri
233 | * @param array $bindings
234 | *
235 | * @return IODataRequest
236 | */
237 | public function getNextPage($requestUri, $bindings = [])
238 | {
239 | return $this->request(HttpMethod::GET, $requestUri, $bindings);
240 | }
241 |
242 | /**
243 | * Run a GET HTTP request against the service and return a generator.
244 | *
245 | * @param string $requestUri
246 | * @param array $bindings
247 | *
248 | * @return \Illuminate\Support\LazyCollection
249 | */
250 | public function cursor($requestUri, $bindings = [])
251 | {
252 | return LazyCollection::make(function() use($requestUri, $bindings) {
253 |
254 | $nextPage = $requestUri;
255 |
256 | while (!is_null($nextPage)) {
257 | list($data, $nextPage) = $this->getNextPage($nextPage, $bindings);
258 |
259 | if (!is_null($nextPage)) {
260 | $nextPage = str_replace($this->baseUrl, '', $nextPage);
261 | }
262 |
263 | yield from $data;
264 | }
265 | });
266 | }
267 |
268 | /**
269 | * Run a POST request against the service.
270 | *
271 | * @param string $requestUri
272 | * @param mixed $postData
273 | *
274 | * @return IODataRequest
275 | */
276 | public function post($requestUri, $postData)
277 | {
278 | return $this->request(HttpMethod::POST, $requestUri, $postData);
279 | }
280 |
281 | /**
282 | * Run a PATCH request against the service.
283 | *
284 | * @param string $requestUri
285 | * @param mixed $body
286 | *
287 | * @return IODataRequest
288 | */
289 | public function patch($requestUri, $body)
290 | {
291 | return $this->request(HttpMethod::PATCH, $requestUri, $body);
292 | }
293 |
294 | /**
295 | * Run a DELETE request against the service.
296 | *
297 | * @param string $requestUri
298 | *
299 | * @return IODataRequest
300 | */
301 | public function delete($requestUri)
302 | {
303 | return $this->request(HttpMethod::DELETE, $requestUri);
304 | }
305 |
306 | /**
307 | * Return an ODataRequest
308 | *
309 | * @param string $method
310 | * @param string $requestUri
311 | * @param mixed $body
312 | *
313 | * @return IODataRequest
314 | *
315 | * @throws ODataException
316 | */
317 | public function request($method, $requestUri, $body = null)
318 | {
319 | $request = new ODataRequest($method, $this->baseUrl.$requestUri, $this, $this->entityReturnType);
320 |
321 | if ($body) {
322 | $request->attachBody($body);
323 | }
324 |
325 | return $request->execute();
326 | }
327 |
328 | /**
329 | * Get the query grammar used by the connection.
330 | *
331 | * @return IGrammar
332 | */
333 | public function getQueryGrammar()
334 | {
335 | return $this->queryGrammar;
336 | }
337 |
338 | /**
339 | * Set the query grammar used by the connection.
340 | *
341 | * @param IGrammar $grammar
342 | *
343 | * @return void
344 | */
345 | public function setQueryGrammar(IGrammar $grammar)
346 | {
347 | $this->queryGrammar = $grammar;
348 | }
349 |
350 | /**
351 | * Get the query post processor used by the connection.
352 | *
353 | * @return IProcessor
354 | */
355 | public function getPostProcessor()
356 | {
357 | return $this->postProcessor;
358 | }
359 |
360 | /**
361 | * Set the query post processor used by the connection.
362 | *
363 | * @param IProcessor $processor
364 | *
365 | * @return void
366 | */
367 | public function setPostProcessor(IProcessor $processor)
368 | {
369 | $this->postProcessor = $processor;
370 | }
371 |
372 | /**
373 | * Set the entity return type
374 | *
375 | * @param string $entityReturnType
376 | */
377 | public function setEntityReturnType($entityReturnType)
378 | {
379 | $this->entityReturnType = $entityReturnType;
380 | }
381 |
382 | /**
383 | * Set the odata.maxpagesize value of the request.
384 | *
385 | * @param int $pageSize
386 | *
387 | * @return IODataClient
388 | */
389 | public function setPageSize($pageSize) {
390 | $this->pageSize = $pageSize;
391 | return $this;
392 | }
393 |
394 | /**
395 | * Gets the page size
396 | *
397 | * @return int
398 | */
399 | public function getPageSize() {
400 | return $this->pageSize;
401 | }
402 |
403 | /**
404 | * Set the entityKey to be found.
405 | *
406 | * @param mixed $entityKey
407 | *
408 | * @return IODataClient
409 | */
410 | public function setEntityKey($entityKey) {
411 | $this->entityKey = $entityKey;
412 | return $this;
413 | }
414 |
415 | /**
416 | * Gets the entity key
417 | *
418 | * @return mixed
419 | */
420 | public function getEntityKey() {
421 | return $this->entityKey;
422 | }
423 | }
424 |
--------------------------------------------------------------------------------
/src/ODataRequest.php:
--------------------------------------------------------------------------------
1 | string)
31 | */
32 | protected $headers;
33 |
34 | /**
35 | * The body of the request (optional)
36 | *
37 | * @var string
38 | */
39 | protected $requestBody;
40 |
41 | /**
42 | * The type of request to make ("GET", "POST", etc.)
43 | *
44 | * @var object
45 | */
46 | protected $method;
47 |
48 | /**
49 | * True if the response should be returned as
50 | * a stream
51 | *
52 | * @var bool
53 | */
54 | protected $returnsStream;
55 |
56 | /**
57 | * The return type to cast the response as
58 | *
59 | * @var object
60 | */
61 | protected $returnType;
62 |
63 | /**
64 | * The timeout, in seconds
65 | *
66 | * @var string
67 | */
68 | protected $timeout;
69 |
70 | /**
71 | * @var IODataClient
72 | */
73 | private $client;
74 |
75 | /**
76 | * Constructs a new ODataRequest object
77 | * @param string $method The HTTP method to use, e.g. "GET" or "POST"
78 | * @param string $requestUrl The URL for the OData request
79 | * @param IODataClient $client The ODataClient used to make the request
80 | * @param [type] $returnType Optional return type for the OData request (defaults to Entity)
81 | *
82 | * @throws ODataException
83 | */
84 | public function __construct(
85 | $method,
86 | $requestUrl,
87 | IODataClient $client,
88 | $returnType = null
89 | ) {
90 | $this->method = $method;
91 | $this->requestUrl = $requestUrl;
92 | $this->client = $client;
93 | $this->setReturnType($returnType);
94 |
95 | if (empty($this->requestUrl)) {
96 | throw new ODataException(Constants::REQUEST_URL_MISSING);
97 | }
98 | $this->timeout = 0;
99 | $this->headers = $this->getDefaultHeaders();
100 | $pageSize = $this->client->getPageSize();
101 | if (!is_null($pageSize) && is_int($pageSize)) {
102 | $this->setPageSize($pageSize);
103 | }
104 | }
105 |
106 | /**
107 | * Undocumented function
108 | *
109 | * @param [type] $pageSize
110 | * @return void
111 | */
112 | public function setPageSize($pageSize) {
113 | $this->headers[RequestHeader::PREFER] = Constants::ODATA_MAX_PAGE_SIZE . '=' . $pageSize;
114 | }
115 |
116 | /**
117 | * Sets the return type of the response object
118 | *
119 | * @param mixed $returnClass The object class to use
120 | *
121 | * @return ODataRequest object
122 | */
123 | public function setReturnType($returnClass)
124 | {
125 | if (is_null($returnClass)) return $this;
126 | $this->returnType = $returnClass;
127 | if (strcasecmp($this->returnType, 'stream') == 0) {
128 | $this->returnsStream = true;
129 | } else {
130 | $this->returnsStream = false;
131 | }
132 | return $this;
133 | }
134 |
135 | /**
136 | * Adds custom headers to the request
137 | *
138 | * @param array $headers An array of custom headers
139 | *
140 | * @return ODataRequest object
141 | */
142 | public function addHeaders($headers)
143 | {
144 | $this->headers = array_merge($this->headers, $headers);
145 | return $this;
146 | }
147 |
148 | /**
149 | * Get the request headers
150 | *
151 | * @return array of headers
152 | */
153 | public function getHeaders()
154 | {
155 | return $this->headers;
156 | }
157 |
158 | /**
159 | * Attach a body to the request. Will JSON encode
160 | * any SaintSystems\OData\Entity objects as well as arrays
161 | *
162 | * @param mixed $obj The object to include in the request
163 | *
164 | * @return ODataRequest object
165 | */
166 | public function attachBody($obj)
167 | {
168 | // Attach streams & JSON automatically
169 | if (is_string($obj) || is_a($obj, 'GuzzleHttp\\Psr7\\Stream')) {
170 | $this->requestBody = $obj;
171 | }
172 | // JSON-encode the model object's property dictionary
173 | else if (is_object($obj) && method_exists($obj, 'getProperties')) {
174 | $class = get_class($obj);
175 | $class = explode("\\", $class);
176 | $model = strtolower(end($class));
177 |
178 | $body = $this->flattenDictionary($obj->getProperties());
179 | $this->requestBody = "{" . $model . ":" . json_encode($body) . "}";
180 | }
181 | // By default, JSON-encode (i.e. arrays)
182 | else {
183 | $this->requestBody = json_encode($obj);
184 | }
185 | return $this;
186 | }
187 |
188 | /**
189 | * Get the body of the request
190 | *
191 | * @return mixed request body of any type
192 | */
193 | public function getBody()
194 | {
195 | return $this->requestBody;
196 | }
197 |
198 | /**
199 | * Sets the timeout limit of the HTTP request
200 | *
201 | * @param string $timeout The timeout in ms
202 | *
203 | * @return ODataRequest object
204 | */
205 | public function setTimeout($timeout)
206 | {
207 | $this->timeout = $timeout;
208 | return $this;
209 | }
210 |
211 | /**
212 | * Executes the HTTP request using Guzzle
213 | *
214 | * @throws ODataException if response is invalid
215 | *
216 | * @return array array of objects
217 | * of class $returnType if $returnType !== false
218 | * of class ODataResponse if $returnType === false
219 | */
220 | public function execute()
221 | {
222 | if (empty($this->requestUrl))
223 | {
224 | throw new ODataException(Constants::REQUEST_URL_MISSING);
225 | }
226 |
227 | $request = $this->getHttpRequestMessage();
228 | $request->body = $this->requestBody;
229 |
230 | $this->authenticateRequest($request);
231 |
232 | // if (strpos($this->requestUrl, '$skiptoken') !== false) {
233 | // echo PHP_EOL;
234 | // echo 'Sending request: '. $this->requestUrl;
235 | // echo PHP_EOL;
236 | // }
237 | $result = $this->client->getHttpProvider()->send($request);
238 |
239 | // Reset
240 | $this->client->setEntityKey(null);
241 |
242 | //Send back the bare response
243 | if ($this->returnsStream) {
244 | return $result;
245 | }
246 |
247 | if ($this->isAggregate()) {
248 | return [(string) $result->getBody(), null];
249 | }
250 |
251 | // Wrap response in ODataResponse layer
252 | try {
253 | $response = new ODataResponse(
254 | $this,
255 | (string) $result->getBody(),
256 | $result->getStatusCode(),
257 | $result->getHeaders()
258 | );
259 | } catch (\Exception $e) {
260 | throw new ODataException(Constants::UNABLE_TO_PARSE_RESPONSE);
261 | }
262 |
263 | // If no return type is specified, return ODataResponse
264 | $returnObj = [$response];
265 |
266 | $returnType = is_null($this->returnType) ? Entity::class : $this->returnType;
267 |
268 | if ($returnType) {
269 | $returnObj = $response->getResponseAsObject($returnType);
270 | }
271 | $nextLink = $response->getNextLink();
272 |
273 | return [$returnObj, $nextLink];
274 | }
275 |
276 | /**
277 | * Executes the HTTP request asynchronously using Guzzle
278 | *
279 | * @param mixed $client The Http client to use in the request
280 | *
281 | * @return mixed object or array of objects
282 | * of class $returnType
283 | */
284 | public function executeAsync($client = null)
285 | {
286 | if (is_null($client)) {
287 | $client = $this->createHttpClient();
288 | }
289 |
290 | $promise = $client->requestAsync(
291 | $this->requestType,
292 | $this->getRequestUrl(),
293 | [
294 | 'body' => $this->requestBody,
295 | 'stream' => $this->returnsStream,
296 | 'timeout' => $this->timeout
297 | ]
298 | )->then(
299 | // On success, return the result/response
300 | function ($result) {
301 | $response = new ODataResponse(
302 | $this,
303 | (string) $result->getBody(),
304 | $result->getStatusCode(),
305 | $result->getHeaders()
306 | );
307 | $returnObject = $response;
308 | if ($this->returnType) {
309 | $returnObject = $response->getResponseAsObject(
310 | $this->returnType
311 | );
312 | }
313 | return $returnObject;
314 | },
315 | // On fail, log the error and return null
316 | function ($reason) {
317 | trigger_error("Async call failed: " . $reason->getMessage());
318 | return null;
319 | }
320 | );
321 | return $promise;
322 | }
323 |
324 | /**
325 | * Get a list of headers for the request
326 | *
327 | * @return array The headers for the request
328 | */
329 | private function getDefaultHeaders()
330 | {
331 | $headers = [
332 | RequestHeader::CONTENT_TYPE => ContentType::APPLICATION_JSON,
333 | RequestHeader::ODATA_MAX_VERSION => Constants::MAX_ODATA_VERSION,
334 | RequestHeader::ODATA_VERSION => Constants::ODATA_VERSION,
335 | RequestHeader::PREFER => Constants::ODATA_MAX_PAGE_SIZE . '=' . Constants::ODATA_MAX_PAGE_SIZE_DEFAULT,
336 | RequestHeader::USER_AGENT => 'odata-sdk-php-' . Constants::SDK_VERSION,
337 | ];
338 |
339 | if (!$this->isAggregate()) {
340 | $headers[RequestHeader::ACCEPT] = ContentType::APPLICATION_JSON ;
341 | }
342 | return $headers;
343 | }
344 |
345 | /**
346 | * Gets the representation of the request.
347 | *
348 | * The representation of the request.
349 | */
350 | public function getHttpRequestMessage()
351 | {
352 | $request = new HttpRequestMessage(new HttpMethod($this->method), $this->requestUrl);
353 |
354 | $this->addHeadersToRequest($request);
355 |
356 | return $request;
357 | }
358 |
359 | /**
360 | * Returns whether or not the request is an OData aggregate request ($count, etc.)
361 | */
362 | private function isAggregate()
363 | {
364 | return strpos($this->requestUrl, '/$count') !== false;
365 | }
366 |
367 | /**
368 | * Adds all of the headers from the header collection to the request.
369 | * @param \SaintSystems\OData\HttpRequestMessage $request The HttpRequestMessage representation of the request.
370 | */
371 | private function addHeadersToRequest(HttpRequestMessage $request)
372 | {
373 | $request->headers = array_merge($this->headers, $request->headers);
374 | if (strpos($request->requestUri, '/$count') !== false || !is_null($this->client->getEntityKey())) {
375 | $request->headers = array_filter($request->headers, function($key) {
376 | return $key !== RequestHeader::PREFER;
377 | }, ARRAY_FILTER_USE_KEY);
378 | }
379 | }
380 |
381 | /**
382 | * Adds the authentication header to the request.
383 | *
384 | * @param HttpRequestMessage $request The representation of the request.
385 | *
386 | * @return
387 | */
388 | private function authenticateRequest(HttpRequestMessage $request)
389 | {
390 | $authenticationProvider = $this->client->getAuthenticationProvider();
391 | if ( ! is_null($authenticationProvider) && is_callable($authenticationProvider)) {
392 | return $authenticationProvider($request);
393 | }
394 | }
395 |
396 | /**
397 | * Flattens the property dictionaries into
398 | * JSON-friendly arrays
399 | *
400 | * @param mixed $obj the object to flatten
401 | *
402 | * @return array flattened object
403 | */
404 | protected function flattenDictionary($obj) {
405 | foreach ($obj as $arrayKey => $arrayValue) {
406 | if (method_exists($arrayValue, 'getProperties')) {
407 | $data = $arrayValue->getProperties();
408 | $obj[$arrayKey] = $data;
409 | } else {
410 | $data = $arrayValue;
411 | }
412 | if (is_array($data)) {
413 | $newItem = $this->flattenDictionary($data);
414 | $obj[$arrayKey] = $newItem;
415 | }
416 | }
417 | return $obj;
418 | }
419 | }
420 |
--------------------------------------------------------------------------------
/src/ODataResponse.php:
--------------------------------------------------------------------------------
1 | request = $request;
77 | $this->body = $body;
78 | $this->httpStatusCode = $httpStatusCode;
79 | $this->headers = $headers;
80 | $this->decodedBody = $this->body ? $this->decodeBody() : [];
81 | }
82 |
83 | /**
84 | * Decode the JSON response into an array
85 | *
86 | * @return array The decoded response
87 | */
88 | private function decodeBody()
89 | {
90 | $decodedBody = json_decode($this->body, true);
91 | if ($decodedBody === null) {
92 | $matches = null;
93 | preg_match('~\{(?:[^{}]|(?R))*\}~', $this->body, $matches);
94 | $decodedBody = json_decode($matches[0], true);
95 | if ($decodedBody === null) {
96 | $decodedBody = array();
97 | }
98 | }
99 | return $decodedBody;
100 | }
101 |
102 | /**
103 | * Get the decoded body of the HTTP response
104 | *
105 | * @return array The decoded body
106 | */
107 | public function getBody()
108 | {
109 | return $this->decodedBody;
110 | }
111 |
112 | /**
113 | * Get the undecoded body of the HTTP response
114 | *
115 | * @return string The undecoded body
116 | */
117 | public function getRawBody()
118 | {
119 | return $this->body;
120 | }
121 |
122 | /**
123 | * Get the status of the HTTP response
124 | *
125 | * @return string The HTTP status
126 | */
127 | public function getStatus()
128 | {
129 | return $this->httpStatusCode;
130 | }
131 |
132 | /**
133 | * Get the headers of the response
134 | *
135 | * @return array The response headers
136 | */
137 | public function getHeaders()
138 | {
139 | return $this->headers;
140 | }
141 |
142 | /**
143 | * Converts the response JSON object to a OData SDK object
144 | *
145 | * @param mixed $returnType The type to convert the object(s) to
146 | *
147 | * @return mixed object or array of objects of type $returnType
148 | */
149 | public function getResponseAsObject($returnType)
150 | {
151 | $class = $returnType;
152 | $result = $this->getBody();
153 |
154 | //If more than one object is returned
155 | if (array_key_exists(Constants::ODATA_VALUE, $result)) {
156 | $objArray = array();
157 | foreach ($result[Constants::ODATA_VALUE] as $obj) {
158 | $objArray[] = new $class($obj);
159 | }
160 | return $objArray;
161 | } else {
162 | return [new $class($result)];
163 | }
164 | }
165 |
166 | /**
167 | * Gets the @odata.nextLink of a response object from OData
168 | *
169 | * @return string next link, if provided
170 | */
171 | public function getNextLink()
172 | {
173 | if (array_key_exists(Constants::ODATA_NEXT_LINK, $this->getBody())) {
174 | $nextLink = $this->getBody()[Constants::ODATA_NEXT_LINK];
175 | return $nextLink;
176 | }
177 | return null;
178 | }
179 |
180 | /**
181 | * Gets the skip token of a response object from OData
182 | *
183 | * @return string skip token, if provided
184 | */
185 | public function getSkipToken()
186 | {
187 | $nextLink = $this->getNextLink();
188 | if (is_null($nextLink)) {
189 | return null;
190 | };
191 | $url = explode("?", $nextLink)[1];
192 | $url = explode("skiptoken=", $url);
193 | if (count($url) > 1) {
194 | return $url[1];
195 | }
196 | return null;
197 | }
198 |
199 | /**
200 | * Gets the Id of response object (if set) from OData
201 | *
202 | * @return mixed id if this was an insert, if provided
203 | */
204 | public function getId()
205 | {
206 | if (array_key_exists(Constants::ODATA_ID, $this->getHeaders())) {
207 | $id = $this->getBody()[Constants::ODATA_ID];
208 | return $id;
209 | }
210 | return null;
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/Option.php:
--------------------------------------------------------------------------------
1 | name = $name;
24 | $this->value = $value;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Preference.php:
--------------------------------------------------------------------------------
1 | value();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Query/Builder.php:
--------------------------------------------------------------------------------
1 | [],
42 | 'where' => [],
43 | 'order' => [],
44 | ];
45 |
46 | /**
47 | * The entity set which the query is targeting.
48 | *
49 | * @var string
50 | */
51 | public $entitySet;
52 |
53 | /**
54 | * The entity key of the entity set which the query is targeting.
55 | *
56 | * @var string
57 | */
58 | public $entityKey;
59 |
60 | /**
61 | * The placeholder property for the ? operator in the OData querystring
62 | *
63 | * @var string
64 | */
65 | public $queryString = '?';
66 |
67 | /**
68 | * An aggregate function to be run.
69 | *
70 | * @var boolean
71 | */
72 | public $count;
73 |
74 | /**
75 | * Whether to include a total count of items matching
76 | * the request be returned along with the result
77 | *
78 | * @var boolean
79 | */
80 | public $totalCount;
81 |
82 | /**
83 | * The specific set of properties to return for this entity or complex type
84 | * http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752360
85 | *
86 | * @var array
87 | */
88 | public $properties;
89 |
90 | /**
91 | * The where constraints for the query.
92 | *
93 | * @var array
94 | */
95 | public $wheres;
96 |
97 | /**
98 | * The groupings for the query.
99 | *
100 | * @var array
101 | */
102 | public $groups;
103 |
104 | /**
105 | * The orderings for the query.
106 | *
107 | * @var array
108 | */
109 | public $orders;
110 |
111 | /**
112 | * The maximum number of records to return.
113 | *
114 | * @var int
115 | */
116 | public $take;
117 |
118 | /**
119 | * The desired page size.
120 | *
121 | * @var int
122 | */
123 | public $pageSize;
124 |
125 | /**
126 | * The number of records to skip.
127 | *
128 | * @var int
129 | */
130 | public $skip;
131 |
132 | /**
133 | * The skiptoken.
134 | *
135 | * @var int
136 | */
137 | public $skiptoken;
138 |
139 | /**
140 | * All of the available clause operators.
141 | *
142 | * @var array
143 | */
144 | public $operators = [
145 | '=', '<', '>', '<=', '>=', '<>', '!=',
146 | 'like', 'like binary', 'not like', 'between', 'ilike',
147 | '&', '|', '^', '<<', '>>',
148 | 'rlike', 'regexp', 'not regexp',
149 | '~', '~*', '!~', '!~*', 'similar to',
150 | 'not similar to', 'not ilike', '~~*', '!~~*',
151 | ];
152 |
153 | /**
154 | * @var array
155 | */
156 | public $select = [];
157 |
158 | /**
159 | * @var array
160 | */
161 | public $expands;
162 |
163 | /**
164 | * @var IProcessor
165 | */
166 | private $processor;
167 |
168 | /**
169 | * @var IGrammar
170 | */
171 | private $grammar;
172 |
173 | /**
174 | * Create a new query builder instance.
175 | *
176 | * @param IODataClient $client
177 | * @param IGrammar $grammar
178 | * @param IProcessor $processor
179 | */
180 | public function __construct(
181 | IODataClient $client,
182 | IGrammar $grammar = null,
183 | IProcessor $processor = null
184 | ) {
185 | $this->client = $client;
186 | $this->grammar = $grammar ?: $client->getQueryGrammar();
187 | $this->processor = $processor ?: $client->getPostProcessor();
188 | }
189 |
190 | /**
191 | * Set the properties to be selected.
192 | *
193 | * @param array|mixed $properties
194 | *
195 | * @return $this
196 | */
197 | public function select($properties = [])
198 | {
199 | $this->properties = is_array($properties) ? $properties : func_get_args();
200 |
201 | return $this;
202 | }
203 |
204 | /**
205 | * Add a new properties to the $select query option.
206 | *
207 | * @param array|mixed $select
208 | *
209 | * @return $this
210 | */
211 | public function addSelect($select)
212 | {
213 | $select = is_array($select) ? $select : func_get_args();
214 |
215 | $this->select = array_merge((array) $this->select, $select);
216 |
217 | return $this;
218 | }
219 |
220 | /**
221 | * Set the entity set which the query is targeting.
222 | *
223 | * @param string $entitySet
224 | *
225 | * @return $this
226 | */
227 | public function from($entitySet)
228 | {
229 | $this->entitySet = $entitySet;
230 |
231 | return $this;
232 | }
233 |
234 | /**
235 | * Filter the entity set on the primary key.
236 | *
237 | * @param string $id
238 | *
239 | * @return $this
240 | */
241 | public function whereKey($id)
242 | {
243 | $this->entityKey = $id;
244 | $this->client->setEntityKey($this->entityKey);
245 | return $this;
246 | }
247 |
248 | /**
249 | * Add an $expand clause to the query.
250 | *
251 | * @param array $properties
252 | * @return $this
253 | */
254 | public function expand($properties = [])
255 | {
256 | $this->expands = is_array($properties) ? $properties : func_get_args();
257 |
258 | return $this;
259 | }
260 |
261 | /*
262 | * TODO: do we still need this? lots of bugs in here!!!
263 | *
264 | public function expand($property, $first, $operator = null, $second = null, $type = 'inner', $ref = false, $count = false)
265 | {
266 | //TODO: need to flush out this method as it will work much like the where and join methods
267 | $expand = new ExpandClause($this, $type, $property);
268 |
269 | // If the first "column" of the join is really a Closure instance the developer
270 | // is trying to build a join with a complex "on" clause containing more than
271 | // one condition, so we'll add the join and call a Closure with the query.
272 | if ($first instanceof Closure) {
273 | call_user_func($first, $expand);
274 |
275 | $this->expands[] = $expand;
276 |
277 | $this->addBinding($expand->getBindings(), 'expand');
278 | }
279 |
280 | // If the column is simply a string, we can assume the join simply has a basic
281 | // "expand" clause with a single condition. So we will just build the expand with
282 | // this simple expand clauses attached to it. There is not an expand callback.
283 | else {
284 | $method = $where ? 'where' : 'on';
285 |
286 | $this->expands[] = $expand->$method($first, $operator, $second);
287 |
288 | $this->addBinding($expand->getBindings(), 'expand');
289 | }
290 |
291 | return $this;
292 | }
293 | */
294 |
295 | /**
296 | * Apply the callback's query changes if the given "value" is true.
297 | *
298 | * @param bool $value
299 | * @param \Closure $callback
300 | * @param \Closure $default
301 | *
302 | * @return Builder
303 | */
304 | public function when($value, $callback, $default = null)
305 | {
306 | $builder = $this;
307 |
308 | if ($value) {
309 | $builder = call_user_func($callback, $builder);
310 | } elseif ($default) {
311 | $builder = call_user_func($default, $builder);
312 | }
313 |
314 | return $builder;
315 | }
316 |
317 | /**
318 | * Set the properties to be ordered.
319 | *
320 | * @param array|mixed $properties
321 | *
322 | * @return $this
323 | */
324 | public function order($properties = [])
325 | {
326 | $order = is_array($properties) && count(func_get_args()) === 1 ? $properties : func_get_args();
327 |
328 | if (!(isset($order[0]) && is_array($order[0]))) {
329 | $order = array($order);
330 | }
331 |
332 | $this->orders = $this->buildOrders($order);
333 |
334 | return $this;
335 | }
336 |
337 | /**
338 | * Set the sql property to be ordered.
339 | *
340 | * @param string $sql
341 | *
342 | * @return $this
343 | */
344 | public function orderBySQL($sql = '')
345 | {
346 | $this->orders = array(['sql' => $sql]);
347 |
348 | return $this;
349 | }
350 |
351 | /**
352 | * Reformat array to match grammar structure
353 | *
354 | * @param array $orders
355 | *
356 | * @return array
357 | */
358 | private function buildOrders($orders = [])
359 | {
360 | $_orders = [];
361 |
362 | foreach ($orders as &$order) {
363 | $column = isset($order['column']) ? $order['column'] : $order[0];
364 | $direction = isset($order['direction']) ? $order['direction'] : (isset($order[1]) ? $order[1] : 'asc');
365 |
366 | array_push($_orders, [
367 | 'column' => $column,
368 | 'direction' => $direction
369 | ]);
370 | }
371 |
372 | return $_orders;
373 | }
374 |
375 | /**
376 | * Merge an array of where clauses and bindings.
377 | *
378 | * @param array $wheres
379 | * @param array $bindings
380 | * @return void
381 | */
382 | public function mergeWheres($wheres, $bindings)
383 | {
384 | $this->wheres = array_merge((array) $this->wheres, (array) $wheres);
385 |
386 | $this->bindings['where'] = array_values(
387 | array_merge($this->bindings['where'], (array) $bindings)
388 | );
389 | }
390 |
391 | /**
392 | * Add a basic where ($filter) clause to the query.
393 | *
394 | * @param string|array|\Closure $column
395 | * @param string $operator
396 | * @param mixed $value
397 | * @param string $boolean
398 | *
399 | * @return $this
400 | */
401 | public function where($column, $operator = null, $value = null, $boolean = 'and')
402 | {
403 | // If the column is an array, we will assume it is an array of key-value pairs
404 | // and can add them each as a where clause. We will maintain the boolean we
405 | // received when the method was called and pass it into the nested where.
406 | if (is_array($column)) {
407 | return $this->addArrayOfWheres($column, $boolean);
408 | }
409 |
410 | // Here we will make some assumptions about the operator. If only 2 values are
411 | // passed to the method, we will assume that the operator is an equals sign
412 | // and keep going. Otherwise, we'll require the operator to be passed in.
413 | list($value, $operator) = $this->prepareValueAndOperator(
414 | $value, $operator, func_num_args() == 2
415 | );
416 |
417 | // If the columns is actually a Closure instance, we will assume the developer
418 | // wants to begin a nested where statement which is wrapped in parenthesis.
419 | // We'll add that Closure to the query then return back out immediately.
420 | if ($column instanceof Closure) {
421 | return $this->whereNested($column, $boolean);
422 | }
423 |
424 | // If the given operator is not found in the list of valid operators we will
425 | // assume that the developer is just short-cutting the '=' operators and
426 | // we will set the operators to '=' and set the values appropriately.
427 | if ($this->invalidOperator($operator)) {
428 | list($value, $operator) = [$operator, '='];
429 | }
430 |
431 | // If the value is a Closure, it means the developer is performing an entire
432 | // sub-select within the query and we will need to compile the sub-select
433 | // within the where clause to get the appropriate query record results.
434 | if ($value instanceof Closure) {
435 | return $this->whereSub($column, $operator, $value, $boolean);
436 | }
437 |
438 | // If the value is "null", we will just assume the developer wants to add a
439 | // where null clause to the query. So, we will allow a short-cut here to
440 | // that method for convenience so the developer doesn't have to check.
441 | if (is_null($value)) {
442 | return $this->whereNull($column, $boolean, $operator != '=');
443 | }
444 |
445 | // If the column is making a JSON reference we'll check to see if the value
446 | // is a boolean. If it is, we'll add the raw boolean string as an actual
447 | // value to the query to ensure this is properly handled by the query.
448 | // if (Str::contains($column, '->') && is_bool($value)) {
449 | // $value = new Expression($value ? 'true' : 'false');
450 | // }
451 |
452 | // Now that we are working with just a simple query we can put the elements
453 | // in our array and add the query binding to our array of bindings that
454 | // will be bound to each SQL statements when it is finally executed.
455 | $type = 'Basic';
456 | if($this->isOperatorAFunction($operator)){
457 | $type = 'Function';
458 | }
459 |
460 | $this->wheres[] = compact(
461 | 'type', 'column', 'operator', 'value', 'boolean'
462 | );
463 |
464 | if (! $value instanceof Expression) {
465 | $this->addBinding($value, 'where');
466 | }
467 |
468 | return $this;
469 | }
470 |
471 | /**
472 | * Add an array of where clauses to the query.
473 | *
474 | * @param array $column
475 | * @param string $boolean
476 | * @param string $method
477 | *
478 | * @return $this
479 | */
480 | protected function addArrayOfWheres($column, $boolean, $method = 'where')
481 | {
482 | return $this->whereNested(function ($query) use ($column, $method) {
483 | foreach ($column as $key => $value) {
484 | if (is_numeric($key) && is_array($value)) {
485 | $query->{$method}(...array_values($value));
486 | } else {
487 | $query->$method($key, '=', $value);
488 | }
489 | }
490 | }, $boolean);
491 | }
492 |
493 | /**
494 | * Determine if the given operator is actually a function.
495 | *
496 | * @param string $operator
497 | * @return bool
498 | */
499 | protected function isOperatorAFunction($operator)
500 | {
501 | return in_array(strtolower($operator), $this->grammar->getFunctions(), true);
502 | }
503 |
504 | /**
505 | * Prepare the value and operator for a where clause.
506 | *
507 | * @param string $value
508 | * @param string $operator
509 | * @param bool $useDefault
510 | *
511 | * @return array
512 | *
513 | * @throws \InvalidArgumentException
514 | */
515 | protected function prepareValueAndOperator($value, $operator, $useDefault = false)
516 | {
517 | if ($useDefault) {
518 | return [$operator, '='];
519 | } elseif ($this->invalidOperatorAndValue($operator, $value)) {
520 | throw new \InvalidArgumentException('Illegal operator and value combination.');
521 | }
522 |
523 | return [$value, $operator];
524 | }
525 |
526 | /**
527 | * Determine if the given operator and value combination is legal.
528 | *
529 | * Prevents using Null values with invalid operators.
530 | *
531 | * @param string $operator
532 | * @param mixed $value
533 | *
534 | * @return bool
535 | */
536 | protected function invalidOperatorAndValue($operator, $value)
537 | {
538 | return is_null($value) && in_array($operator, $this->operators) &&
539 | ! in_array($operator, ['=', '<>', '!=']);
540 | }
541 |
542 | /**
543 | * Determine if the given operator is supported.
544 | *
545 | * @param string $operator
546 | * @return bool
547 | */
548 | protected function invalidOperator($operator)
549 | {
550 | return ! in_array(strtolower($operator), $this->operators, true) &&
551 | ! in_array(strtolower($operator), $this->grammar->getOperatorsAndFunctions(), true);
552 | }
553 |
554 | /**
555 | * Add an "or where" clause to the query.
556 | *
557 | * @param \Closure|string $column
558 | * @param string $operator
559 | * @param mixed $value
560 | *
561 | * @return Builder|static
562 | */
563 | public function orWhere($column, $operator = null, $value = null)
564 | {
565 | return $this->where($column, $operator, $value, 'or');
566 | }
567 |
568 | public function whereRaw($rawString, $boolean = 'and')
569 | {
570 | // We will add this where clause into this array of clauses that we
571 | // are building for the query. All of them will be compiled via a grammar
572 | // once the query is about to be executed and run against the database.
573 | $type = 'Raw';
574 |
575 | $this->wheres[] = compact(
576 | 'type', 'rawString', 'boolean'
577 | );
578 |
579 | return $this;
580 | }
581 |
582 | public function orWhereRaw($rawString)
583 | {
584 | return $this->whereRaw($rawString, 'or');
585 | }
586 |
587 | /**
588 | * Add a "where" clause comparing two columns to the query.
589 | *
590 | * @param string|array $first
591 | * @param string|null $operator
592 | * @param string|null $second
593 | * @param string|null $boolean
594 | * @return $this
595 | */
596 | public function whereColumn($first, $operator = null, $second = null, $boolean = 'and')
597 | {
598 | // If the column is an array, we will assume it is an array of key-value pairs
599 | // and can add them each as a where clause. We will maintain the boolean we
600 | // received when the method was called and pass it into the nested where.
601 | if (is_array($first)) {
602 | return $this->addArrayOfWheres($first, $boolean, 'whereColumn');
603 | }
604 |
605 | // Here we will make some assumptions about the operator. If only 2 values are
606 | // passed to the method, we will assume that the operator is an equals sign
607 | // and keep going. Otherwise, we'll require the operator to be passed in.
608 | list($second, $operator) = $this->prepareValueAndOperator(
609 | $second, $operator, func_num_args() == 2
610 | );
611 |
612 | // If the given operator is not found in the list of valid operators we will
613 | // assume that the developer is just short-cutting the '=' operators and
614 | // we will set the operators to '=' and set the values appropriately.
615 | if ($this->invalidOperator($operator)) {
616 | [$second, $operator] = [$operator, '='];
617 | }
618 |
619 | // Finally, we will add this where clause into this array of clauses that we
620 | // are building for the query. All of them will be compiled via a grammar
621 | // once the query is about to be executed and run against the database.
622 | $type = 'Column';
623 |
624 | $this->wheres[] = compact(
625 | 'type', 'first', 'operator', 'second', 'boolean'
626 | );
627 |
628 | return $this;
629 | }
630 |
631 | /**
632 | * Add an "or where" clause comparing two columns to the query.
633 | *
634 | * @param string|array $first
635 | * @param string|null $operator
636 | * @param string|null $second
637 | * @return $this
638 | */
639 | public function orWhereColumn($first, $operator = null, $second = null)
640 | {
641 | return $this->whereColumn($first, $operator, $second, 'or');
642 | }
643 |
644 | /**
645 | * Add a nested where statement to the query.
646 | *
647 | * @param \Closure $callback
648 | * @param string $boolean
649 | *
650 | * @return Builder|static
651 | */
652 | public function whereNested(Closure $callback, $boolean = 'and')
653 | {
654 | call_user_func($callback, $query = $this->forNestedWhere());
655 |
656 | return $this->addNestedWhereQuery($query, $boolean);
657 | }
658 |
659 | /**
660 | * Create a new query instance for nested where condition.
661 | *
662 | * @return Builder
663 | */
664 | public function forNestedWhere()
665 | {
666 | return $this->newQuery()->from($this->entitySet);
667 | }
668 |
669 | /**
670 | * Add another query builder as a nested where to the query builder.
671 | *
672 | * @param Builder|static $query
673 | * @param string $boolean
674 | *
675 | * @return $this
676 | */
677 | public function addNestedWhereQuery($query, $boolean = 'and')
678 | {
679 | if (count($query->wheres)) {
680 | $type = 'Nested';
681 |
682 | $this->wheres[] = compact('type', 'query', 'boolean');
683 |
684 | $this->addBinding($query->getBindings(), 'where');
685 | }
686 |
687 | return $this;
688 | }
689 |
690 | /**
691 | * Add a full sub-select to the query.
692 | *
693 | * @param string $column
694 | * @param string $operator
695 | * @param \Closure $callback
696 | * @param string $boolean
697 | *
698 | * @return $this
699 | */
700 | protected function whereSub($column, $operator, Closure $callback, $boolean)
701 | {
702 | $type = 'Sub';
703 |
704 | // Once we have the query instance we can simply execute it so it can add all
705 | // of the sub-select's conditions to itself, and then we can cache it off
706 | // in the array of where clauses for the "main" parent query instance.
707 | call_user_func($callback, $query = $this->newQuery());
708 |
709 | $this->wheres[] = compact(
710 | 'type', 'column', 'operator', 'query', 'boolean'
711 | );
712 |
713 | $this->addBinding($query->getBindings(), 'where');
714 |
715 | return $this;
716 | }
717 |
718 | /**
719 | * Add a "where null" clause to the query.
720 | *
721 | * @param string $column
722 | * @param string $boolean
723 | * @param bool $not
724 | * @return $this
725 | */
726 | public function whereNull($column, $boolean = 'and', $not = false)
727 | {
728 | $type = $not ? 'NotNull' : 'Null';
729 |
730 | $this->wheres[] = compact('type', 'column', 'boolean');
731 |
732 | return $this;
733 | }
734 |
735 | /**
736 | * Add an "or where null" clause to the query.
737 | *
738 | * @param string $column
739 | * @return Builder|static
740 | */
741 | public function orWhereNull($column)
742 | {
743 | return $this->whereNull($column, 'or');
744 | }
745 |
746 | /**
747 | * Add a "where not null" clause to the query.
748 | *
749 | * @param string $column
750 | * @param string $boolean
751 | * @return Builder|static
752 | */
753 | public function whereNotNull($column, $boolean = 'and')
754 | {
755 | return $this->whereNull($column, $boolean, true);
756 | }
757 |
758 | /**
759 | * Add an "or where not null" clause to the query.
760 | *
761 | * @param string $column
762 | * @return Builder|static
763 | */
764 | public function orWhereNotNull($column)
765 | {
766 | return $this->whereNotNull($column, 'or');
767 | }
768 |
769 |
770 |
771 |
772 | /**
773 | * Add a "where in" clause to the query.
774 | *
775 | * @param string $column
776 | * @param array $list
777 | * @param string $boolean
778 | * @param bool $not
779 | * @return $this
780 | */
781 | public function whereIn($column, $list, $boolean = 'and', $not = false)
782 | {
783 | $type = $not ? 'NotIn' : 'In';
784 |
785 | $this->wheres[] = compact('type', 'column', 'list', 'boolean');
786 |
787 | return $this;
788 | }
789 |
790 | /**
791 | * Add an "or where in" clause to the query.
792 | *
793 | * @param string $column
794 | * @param array $list
795 | * @return Builder|static
796 | */
797 | public function orWhereIn($column, $list)
798 | {
799 | return $this->whereIn($column, $list, 'or');
800 | }
801 |
802 | /**
803 | * Add a "where not in" clause to the query.
804 | *
805 | * @param string $column
806 | * @param array $list
807 | * @param string $boolean
808 | * @return Builder|static
809 | */
810 | public function whereNotIn($column, $list, $boolean = 'and')
811 | {
812 | return $this->whereIn($column, $list, $boolean, true);
813 | }
814 |
815 | /**
816 | * Add an "or where not in" clause to the query.
817 | *
818 | * @param string $column
819 | * @param array $list
820 | * @return Builder|static
821 | */
822 | public function orWhereNotIn($column, $list)
823 | {
824 | return $this->whereNotIn($column, $list, 'or');
825 | }
826 |
827 |
828 |
829 | /**
830 | * Get the HTTP Request representation of the query.
831 | *
832 | * @return string
833 | */
834 | public function toRequest()
835 | {
836 | return $this->grammar->compileSelect($this);
837 | }
838 |
839 | /**
840 | * Execute a query for a single record by ID. Single and multi-part IDs are supported.
841 | *
842 | * @param int|string|array $id the value of the ID or an associative array in case of multi-part IDs
843 | * @param array $properties
844 | *
845 | * @return \stdClass|array|null
846 | *
847 | * @throws ODataQueryException
848 | */
849 | public function find($id, $properties = [])
850 | {
851 | if (!isset($this->entitySet)) {
852 | throw new ODataQueryException(Constants::ENTITY_SET_REQUIRED);
853 | }
854 | return $this->whereKey($id)->first($properties);
855 | }
856 |
857 | /**
858 | * Get a single property's value from the first result of a query.
859 | *
860 | * @param string $property
861 | *
862 | * @return mixed
863 | */
864 | public function value($property)
865 | {
866 | $result = (array) $this->first([$property]);
867 |
868 | return count($result) > 0 ? reset($result) : null;
869 | }
870 |
871 | /**
872 | * Execute the query and get the first result.
873 | *
874 | * @param array $properties
875 | *
876 | * @return \stdClass|array|null
877 | */
878 | public function first($properties = [])
879 | {
880 | return $this->take(1)->get($properties)->first();
881 | //return $this->take(1)->get($properties);
882 | }
883 |
884 | /**
885 | * Set the "$skip" value of the query.
886 | *
887 | * @param int $value
888 | *
889 | * @return Builder|static
890 | */
891 | public function skip($value)
892 | {
893 | $this->skip = $value;
894 | return $this;
895 | }
896 |
897 | /**
898 | * Set the "$skiptoken" value of the query.
899 | *
900 | * @param int $value
901 | *
902 | * @return Builder|static
903 | */
904 | public function skipToken($value)
905 | {
906 | $this->skiptoken = $value;
907 | return $this;
908 | }
909 |
910 | /**
911 | * Set the "$top" value of the query.
912 | *
913 | * @param int $value
914 | *
915 | * @return Builder|static
916 | */
917 | public function take($value)
918 | {
919 | $this->take = $value;
920 | return $this;
921 | }
922 |
923 | /**
924 | * Set the desired pagesize of the query;
925 | *
926 | * @param int $value
927 | *
928 | * @return Builder|static
929 | */
930 | public function pageSize($value)
931 | {
932 | $this->pageSize = $value;
933 | $this->client->setPageSize($this->pageSize);
934 | return $this;
935 | }
936 |
937 | /**
938 | * Execute the query as a "GET" request.
939 | *
940 | * @param array $properties
941 | * @param array $options
942 | *
943 | * @return Collection
944 | */
945 | public function get($properties = [], $options = null)
946 | {
947 | if (is_numeric($properties)) {
948 | $options = $properties;
949 | $properties = [];
950 | }
951 |
952 | if (isset($options)) {
953 | $include_count = $options & QueryOptions::INCLUDE_COUNT;
954 |
955 | if ($include_count) {
956 | $this->totalCount = true;
957 | }
958 | }
959 |
960 | $original = $this->properties;
961 |
962 | if (is_null($original)) {
963 | $this->properties = $properties;
964 | }
965 |
966 | $results = $this->processor->processSelect($this, $this->runGet());
967 |
968 | $this->properties = $original;
969 |
970 | return collect($results);
971 | //return $results;
972 | }
973 |
974 | /**
975 | * Execute the query as a "POST" request.
976 | *
977 | * @param array $body
978 | * @param array $properties
979 | * @param array $options
980 | *
981 | * @return Collection
982 | */
983 | public function post($body = [], $properties = [], $options = null)
984 | {
985 | if (is_numeric($properties)) {
986 | $options = $properties;
987 | $properties = [];
988 | }
989 |
990 | if (isset($options)) {
991 | $include_count = $options & QueryOptions::INCLUDE_COUNT;
992 |
993 | if ($include_count) {
994 | $this->totalCount = true;
995 | }
996 | }
997 |
998 | $original = $this->properties;
999 |
1000 | if (is_null($original)) {
1001 | $this->properties = $properties;
1002 | }
1003 |
1004 | $results = $this->processor->processSelect($this, $this->runPost($body));
1005 |
1006 | $this->properties = $original;
1007 |
1008 | return collect($results);
1009 | }
1010 |
1011 | /**
1012 | * Execute the query as a "DELETE" request.
1013 | *
1014 | * @return boolean
1015 | */
1016 | public function delete($options = null)
1017 | {
1018 | $results = $this->processor->processSelect($this, $this->runDelete());
1019 |
1020 | return true;
1021 | }
1022 |
1023 | /**
1024 | * Execute the query as a "PATCH" request.
1025 | *
1026 | * @param array $properties
1027 | * @param array $options
1028 | *
1029 | * @return Collection
1030 | */
1031 | public function patch($body, $properties = [], $options = null)
1032 | {
1033 | if (is_numeric($properties)) {
1034 | $options = $properties;
1035 | $properties = [];
1036 | }
1037 |
1038 | if (isset($options)) {
1039 | $include_count = $options & QueryOptions::INCLUDE_COUNT;
1040 |
1041 | if ($include_count) {
1042 | $this->totalCount = true;
1043 | }
1044 | }
1045 |
1046 | $original = $this->properties;
1047 |
1048 | if (is_null($original)) {
1049 | $this->properties = $properties;
1050 | }
1051 |
1052 | $results = $this->processor->processSelect($this, $this->runPatch($body));
1053 |
1054 | $this->properties = $original;
1055 |
1056 | return collect($results);
1057 | //return $results;
1058 | }
1059 |
1060 | /**
1061 | * Run the query as a "GET" request against the client.
1062 | *
1063 | * @return IODataRequest
1064 | */
1065 | protected function runGet()
1066 | {
1067 | return $this->client->get(
1068 | $this->grammar->compileSelect($this), $this->getBindings()
1069 | );
1070 | }
1071 |
1072 | /**
1073 | * Get a lazy collection for the given request.
1074 | *
1075 | * @return \Illuminate\Support\LazyCollection
1076 | */
1077 | public function cursor()
1078 | {
1079 | return new LazyCollection(function() {
1080 | yield from $this->client->cursor(
1081 | $this->grammar->compileSelect($this), $this->getBindings()
1082 | );
1083 | });
1084 | }
1085 |
1086 | /**
1087 | * Run the query as a "GET" request against the client.
1088 | *
1089 | * @return IODataRequest
1090 | */
1091 | protected function runPatch($body)
1092 | {
1093 | return $this->client->patch(
1094 | $this->grammar->compileSelect($this), $body
1095 | );
1096 | }
1097 |
1098 | /**
1099 | * Run the query as a "GET" request against the client.
1100 | *
1101 | * @return IODataRequest
1102 | */
1103 | protected function runPost($body)
1104 | {
1105 | return $this->client->post(
1106 | $this->grammar->compileSelect($this), $body
1107 | );
1108 | }
1109 |
1110 | /**
1111 | * Run the query as a "GET" request against the client.
1112 | *
1113 | * @return IODataRequest
1114 | */
1115 | protected function runDelete()
1116 | {
1117 | return $this->client->delete(
1118 | $this->grammar->compileSelect($this)
1119 | );
1120 | }
1121 |
1122 | /**
1123 | * Retrieve the "count" result of the query.
1124 | *
1125 | * @return int
1126 | */
1127 | public function count()
1128 | {
1129 | $this->count = true;
1130 | $results = $this->get();
1131 |
1132 | if (! $results->isEmpty()) {
1133 | // replace all none numeric characters before casting it as int
1134 | return (int) preg_replace('/[^0-9,.]/', '', $results[0]);
1135 | }
1136 | }
1137 |
1138 | /**
1139 | * Insert a new record into the database.
1140 | *
1141 | * @param array $values
1142 | *
1143 | * @return bool
1144 | */
1145 | public function insert(array $values)
1146 | {
1147 | // Since every insert gets treated like a batch insert, we will make sure the
1148 | // bindings are structured in a way that is convenient when building these
1149 | // inserts statements by verifying these elements are actually an array.
1150 | if (empty($values)) {
1151 | return true;
1152 | }
1153 |
1154 | if (! is_array(reset($values))) {
1155 | $values = [$values];
1156 | }
1157 |
1158 | // Here, we will sort the insert keys for every record so that each insert is
1159 | // in the same order for the record. We need to make sure this is the case
1160 | // so there are not any errors or problems when inserting these records.
1161 | else {
1162 | foreach ($values as $key => $value) {
1163 | ksort($value);
1164 |
1165 | $values[$key] = $value;
1166 | }
1167 | }
1168 |
1169 | // Finally, we will run this query against the database connection and return
1170 | // the results. We will need to also flatten these bindings before running
1171 | // the query so they are all in one huge, flattened array for execution.
1172 | return $this->client->post(
1173 | $this->grammar->compileInsert($this, $values),
1174 | $this->cleanBindings(Arr::flatten($values, 1))
1175 | );
1176 | }
1177 |
1178 | /**
1179 | * Insert a new record and get the value of the primary key.
1180 | *
1181 | * @param array $values
1182 | *
1183 | * @return mixed
1184 | */
1185 | public function insertGetId(array $values)
1186 | {
1187 | $results = $this->insert($values);
1188 |
1189 | return $results->getId();
1190 | }
1191 |
1192 | /**
1193 | * Get a new instance of the query builder.
1194 | *
1195 | * @return Builder
1196 | */
1197 | public function newQuery()
1198 | {
1199 | return new static($this->client, $this->grammar, $this->processor);
1200 | }
1201 |
1202 | /**
1203 | * Get the current query value bindings in a flattened array.
1204 | *
1205 | * @return array
1206 | */
1207 | public function getBindings()
1208 | {
1209 | return Arr::flatten($this->bindings);
1210 | }
1211 |
1212 | /**
1213 | * Remove all of the expressions from a list of bindings.
1214 | *
1215 | * @param array $bindings
1216 | *
1217 | * @return array
1218 | */
1219 | protected function cleanBindings(array $bindings)
1220 | {
1221 | return array_values(array_filter($bindings, function ($binding) {
1222 | return true;//! $binding instanceof Expression;
1223 | }));
1224 | }
1225 |
1226 | /**
1227 | * Add a binding to the query.
1228 | *
1229 | * @param mixed $value
1230 | * @param string $type
1231 | *
1232 | * @return $this
1233 | *
1234 | * @throws \InvalidArgumentException
1235 | */
1236 | public function addBinding($value, $type = 'where')
1237 | {
1238 | if (! array_key_exists($type, $this->bindings)) {
1239 | throw new \InvalidArgumentException("Invalid binding type: {$type}.");
1240 | }
1241 |
1242 | if (is_array($value)) {
1243 | $this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value));
1244 | } else {
1245 | $this->bindings[$type][] = $value;
1246 | }
1247 |
1248 | return $this;
1249 | }
1250 |
1251 | /**
1252 | * Get the IODataClient instance.
1253 | *
1254 | * @return IODataClient
1255 | */
1256 | public function getClient()
1257 | {
1258 | return $this->client;
1259 | }
1260 | }
1261 |
--------------------------------------------------------------------------------
/src/Query/ExpandClause.php:
--------------------------------------------------------------------------------
1 | property = $property;
32 | $this->parentQuery = $parentQuery;
33 |
34 | parent::__construct(
35 | $parentQuery->getConnection(), $parentQuery->getGrammar(), $parentQuery->getProcessor()
36 | );
37 | }
38 |
39 | /**
40 | * Add an "on" clause to the join.
41 | *
42 | * On clauses can be chained, e.g.
43 | *
44 | * $join->on('contacts.user_id', '=', 'users.id')
45 | * ->on('contacts.info_id', '=', 'info.id')
46 | *
47 | * will produce the following SQL:
48 | *
49 | * on `contacts`.`user_id` = `users`.`id` and `contacts`.`info_id` = `info`.`id`
50 | *
51 | * @param \Closure|string $first
52 | * @param string|null $operator
53 | * @param string|null $second
54 | * @param string $boolean
55 | *
56 | * @return $this
57 | *
58 | * @throws \InvalidArgumentException
59 | */
60 | public function on($first, $operator = null, $second = null, $boolean = 'and')
61 | {
62 | if ($first instanceof Closure) {
63 | return $this->whereNested($first, $boolean);
64 | }
65 |
66 | return $this->whereColumn($first, $operator, $second, $boolean);
67 | }
68 |
69 | /**
70 | * Add an "or on" clause to the join.
71 | *
72 | * @param \Closure|string $first
73 | * @param string|null $operator
74 | * @param string|null $second
75 | *
76 | * @return ExpandClause
77 | */
78 | public function orOn($first, $operator = null, $second = null)
79 | {
80 | return $this->on($first, $operator, $second, 'or');
81 | }
82 |
83 | /**
84 | * Get a new instance of the join clause builder.
85 | *
86 | * @return ExpandClause
87 | */
88 | public function newQuery()
89 | {
90 | return new static($this->parentQuery, $this->property);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Query/Grammar.php:
--------------------------------------------------------------------------------
1 | ', '<=', '>=', '!<', '!>', '<>', '!='
14 | ];
15 |
16 | /**
17 | * All of the available clause functions.
18 | *
19 | * @var array
20 | */
21 | protected $functions = [
22 | 'contains', 'startswith', 'endswith', 'substringof'
23 | ];
24 |
25 | protected $operatorMapping = [
26 | '=' => 'eq',
27 | '<' => 'lt',
28 | '>' => 'gt',
29 | '<=' => 'le',
30 | '>=' => 'ge',
31 | '!<' => 'not lt',
32 | '!>' => 'not gt',
33 | '<>' => 'ne',
34 | '!=' => 'ne',
35 | ];
36 |
37 | /**
38 | * The components that make up an OData Request.
39 | *
40 | * @var array
41 | */
42 | protected $selectComponents = [
43 | 'entitySet',
44 | 'entityKey',
45 | 'count',
46 | 'queryString',
47 | 'properties',
48 | 'wheres',
49 | 'expands',
50 | //'search',
51 | 'orders',
52 | 'skip',
53 | 'skiptoken',
54 | 'take',
55 | 'totalCount',
56 | ];
57 |
58 | /**
59 | * Determine if query param is the first one added to uri
60 | *
61 | * @var bool
62 | */
63 | private $isFirstQueryParam = true;
64 |
65 | /**
66 | * @inheritdoc
67 | */
68 | public function compileSelect(Builder $query)
69 | {
70 | // If the query does not have any properties set, we'll set the properties to the
71 | // [] character to just get all of the columns from the database. Then we
72 | // can build the query and concatenate all the pieces together as one.
73 | $original = $query->properties;
74 |
75 | if (is_null($query->properties)) {
76 | $query->properties = [];
77 | }
78 |
79 | // To compile the query, we'll spin through each component of the query and
80 | // see if that component exists. If it does we'll just call the compiler
81 | // function for the component which is responsible for making the SQL.
82 | $uri = trim($this->concatenate(
83 | $this->compileComponents($query))
84 | );
85 |
86 | $query->properties = $original;
87 |
88 | //dd($uri);
89 |
90 | return $uri;
91 | }
92 |
93 | /**
94 | * Compile the components necessary for a select clause.
95 | *
96 | * @param Builder $query
97 | *
98 | * @return array
99 | */
100 | protected function compileComponents(Builder $query)
101 | {
102 | $uri = [];
103 |
104 | foreach ($this->selectComponents as $component) {
105 | // To compile the query, we'll spin through each component of the query and
106 | // see if that component exists. If it does we'll just call the compiler
107 | // function for the component which is responsible for making the SQL.
108 | if (! is_null($query->$component)) {
109 | $method = 'compile'.ucfirst($component);
110 |
111 | $uri[$component] = $this->$method($query, $query->$component);
112 | }
113 | }
114 | return $uri;
115 | }
116 |
117 | /**
118 | * Compile the "from" portion of the query.
119 | *
120 | * @param Builder $query
121 | * @param string $entitySet
122 | *
123 | * @return string
124 | */
125 | protected function compileEntitySet(Builder $query, $entitySet)
126 | {
127 | return $entitySet;
128 | }
129 |
130 | /**
131 | * Compile the entity key portion of the query.
132 | *
133 | * @param Builder $query
134 | * @param string $entityKey
135 | *
136 | * @return string
137 | */
138 | protected function compileEntityKey(Builder $query, $entityKey)
139 | {
140 | if (is_null($entityKey)) {
141 | return '';
142 | }
143 |
144 | if (is_array($entityKey)) {
145 | $entityKey = $this->compileCompositeEntityKey($entityKey);
146 | } else {
147 | $entityKey = $this->wrapKey($entityKey);
148 | }
149 |
150 | return "($entityKey)";
151 | }
152 |
153 | /**
154 | * Compile the composite entity key portion of the query.
155 | *
156 | * @param Builder $query
157 | * @param mixed $entityKey
158 | *
159 | * @return string
160 | */
161 | public function compileCompositeEntityKey($entityKey)
162 | {
163 | $entityKeys = [];
164 | foreach ($entityKey as $key => $value) {
165 | $entityKeys[] = $key . '=' . $this->wrapKey($value);
166 | }
167 |
168 | return implode(',', $entityKeys);
169 | }
170 |
171 | protected function compileQueryString(Builder $query, $queryString)
172 | {
173 | if (isset($query->entitySet)
174 | && (
175 | !empty($query->properties)
176 | || isset($query->wheres)
177 | || isset($query->orders)
178 | || isset($query->expands)
179 | || isset($query->take)
180 | || isset($query->skip)
181 | || isset($query->skiptoken)
182 | )) {
183 | return $queryString;
184 | }
185 | return '';
186 | }
187 |
188 | protected function wrapKey($entityKey)
189 | {
190 | if (is_uuid($entityKey) || is_int($entityKey)) {
191 | return $entityKey;
192 | }
193 | return "'$entityKey'";
194 | }
195 |
196 | /**
197 | * Compile an aggregated select clause.
198 | *
199 | * @param Builder $query
200 | * @param array $aggregate
201 | *
202 | * @return string
203 | */
204 | protected function compileCount(Builder $query, $aggregate)
205 | {
206 | return '/$count';
207 | }
208 |
209 | /**
210 | * Compile the "$select=" portion of the OData query.
211 | *
212 | * @param Builder $query
213 | * @param array $properties
214 | *
215 | * @return string|null
216 | */
217 | protected function compileProperties(Builder $query, $properties)
218 | {
219 | // If the query is actually performing an aggregating select, we will let that
220 | // compiler handle the building of the select clauses, as it will need some
221 | // more syntax that is best handled by that function to keep things neat.
222 | if (! is_null($query->count)) {
223 | return;
224 | }
225 |
226 | $select = '';
227 | if (! empty($properties)) {
228 | $select = $this->appendQueryParam('$select=') . $this->columnize($properties);
229 | }
230 |
231 | return $select;
232 | }
233 |
234 | /**
235 | * Compile the "expand" portions of the query.
236 | *
237 | * @param Builder $query
238 | * @param array $expands
239 | *
240 | * @return string
241 | */
242 | protected function compileExpands(Builder $query, $expands)
243 | {
244 | if (! empty($expands)) {
245 | return $this->appendQueryParam('$expand=') . implode(',', $expands);
246 | }
247 |
248 | return '';
249 | }
250 |
251 | /**
252 | * Compile the "where" portions of the query.
253 | *
254 | * @param Builder $query
255 | *
256 | * @return string
257 | */
258 | protected function compileWheres(Builder $query)
259 | {
260 | // Each type of where clauses has its own compiler function which is responsible
261 | // for actually creating the where clauses SQL. This helps keep the code nice
262 | // and maintainable since each clause has a very small method that it uses.
263 | if (is_null($query->wheres)) {
264 | return '';
265 | }
266 |
267 | // If we actually have some where clauses, we will strip off the first boolean
268 | // operator, which is added by the query builders for convenience so we can
269 | // avoid checking for the first clauses in each of the compilers methods.
270 | if (count($sql = $this->compileWheresToArray($query)) > 0) {
271 | return $this->concatenateWhereClauses($query, $sql);
272 | }
273 |
274 | return '';
275 | }
276 |
277 | /**
278 | * Get an array of all the where clauses for the query.
279 | *
280 | * @param Builder $query
281 | *
282 | * @return array
283 | */
284 | protected function compileWheresToArray($query)
285 | {
286 | return collect($query->wheres)->map(function ($where) use ($query) {
287 | return $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where);
288 | })->all();
289 | }
290 |
291 | protected function whereRaw(Builder $query, $where)
292 | {
293 | return $where['rawString'];
294 | }
295 |
296 | /**
297 | * Format the where clause statements into one string.
298 | *
299 | * @param Builder $query
300 | * @param array $filter
301 | *
302 | * @return string
303 | */
304 | protected function concatenateWhereClauses($query, $filter)
305 | {
306 | //$conjunction = $query instanceof JoinClause ? 'on' : 'where';
307 | $conjunction = $this->appendQueryParam('$filter=');
308 |
309 | return $conjunction . $this->removeLeadingBoolean(implode(' ', $filter));
310 | }
311 |
312 | /**
313 | * Compile a basic where clause.
314 | *
315 | * @param Builder $query
316 | * @param array $where
317 | *
318 | * @return string
319 | */
320 | protected function whereBasic(Builder $query, $where)
321 | {
322 | $value = $this->prepareValue($where['value']);
323 | return $where['column'].' '.$this->getOperatorMapping($where['operator']).' '.$value;
324 | }
325 |
326 | /**
327 | * Compile a where clause comparing two columns.
328 | *
329 | * @param Builder $query
330 | * @param array $where
331 | * @return string
332 | */
333 | protected function whereColumn(Builder $query, $where)
334 | {
335 | return $where['first'].' '.$this->getOperatorMapping($where['operator']).' '.$where['second'];
336 | }
337 |
338 | /**
339 | * Compile a "where function" clause.
340 | *
341 | * @param Builder $query
342 | * @param array $where
343 | * @return string
344 | */
345 | protected function whereFunction(Builder $query, $where)
346 | {
347 | $value = $this->prepareValue($where['value']);
348 | return $where['operator'] . '(' . $where['column'] . ',' . $value . ')';
349 | }
350 |
351 | /**
352 | * Determines if the value is a special primitive data type (similar syntax with enums)
353 | *
354 | * @param string $value
355 | * @return string
356 | */
357 | protected function isSpecialPrimitiveDataType($value){
358 | return preg_match("/^(binary|datetime|guid|time|datetimeoffset)(\'[\w\:\-\.]+\')$/i", $value);
359 | }
360 |
361 | /**
362 | * Compile the "order by" portions of the query.
363 | *
364 | * @param Builder $query
365 | * @param array $orders
366 | *
367 | * @return string
368 | */
369 | protected function compileOrders(Builder $query, $orders)
370 | {
371 | if (! empty($orders)) {
372 | return $this->appendQueryParam('$orderby=') . implode(',', $this->compileOrdersToArray($query, $orders));
373 | }
374 |
375 | return '';
376 | }
377 |
378 | /**
379 | * Compile the query orders to an array.
380 | *
381 | * @param Builder $query
382 | * @param array $orders
383 | *
384 | * @return array
385 | */
386 | protected function compileOrdersToArray(Builder $query, $orders)
387 | {
388 | return array_map(function ($order) {
389 | return ! isset($order['sql'])
390 | ? $order['column'].' '.$order['direction']
391 | : $order['sql'];
392 | }, $orders);
393 | }
394 |
395 | /**
396 | * Compile the "$top" portions of the query.
397 | *
398 | * @param Builder $query
399 | * @param int $take
400 | *
401 | * @return string
402 | */
403 | protected function compileTake(Builder $query, $take)
404 | {
405 | // If we have an entity key $top is redundant and invalid, so bail
406 | if (! empty($query->entityKey)) {
407 | return '';
408 | }
409 | return $this->appendQueryParam('$top=') . (int) $take;
410 | }
411 |
412 | /**
413 | * Compile the "$skip" portions of the query.
414 | *
415 | * @param Builder $query
416 | * @param int $skip
417 | *
418 | * @return string
419 | */
420 | protected function compileSkip(Builder $query, $skip)
421 | {
422 | return $this->appendQueryParam('$skip=') . (int) $skip;
423 | }
424 |
425 | /**
426 | * Compile the "$skiptoken" portions of the query.
427 | *
428 | * @param Builder $query
429 | * @param int $skip
430 | *
431 | * @return string
432 | */
433 | protected function compileSkipToken(Builder $query, $skiptoken)
434 | {
435 | return $this->appendQueryParam('$skiptoken=') . $skiptoken;
436 | }
437 |
438 | /**
439 | * Compile the "$count" portions of the query.
440 | *
441 | * @param Builder $query
442 | * @param int $totalCount
443 | *
444 | * @return string
445 | */
446 | protected function compileTotalCount(Builder $query, $totalCount)
447 | {
448 | if (isset($query->entityKey)) {
449 | return '';
450 | }
451 | return $this->appendQueryParam('$count=true');
452 | }
453 |
454 | /**
455 | * @inheritdoc
456 | */
457 | public function columnize(array $properties)
458 | {
459 | return implode(',', $properties);
460 | }
461 |
462 | /**
463 | * Concatenate an array of segments, removing empties.
464 | *
465 | * @param array $segments
466 | *
467 | * @return string
468 | */
469 | protected function concatenate($segments)
470 | {
471 | // return implode('', array_filter($segments, function ($value) {
472 | // return (string) $value !== '';
473 | // }));
474 | $uri = '';
475 | foreach ($segments as $segment => $value) {
476 | if ((string) $value !== '') {
477 | $uri.= strpos($uri, '?$') ? '&' . $value : $value;
478 | }
479 | }
480 | return $uri;
481 | }
482 |
483 | /**
484 | * Remove the leading boolean from a statement.
485 | *
486 | * @param string $value
487 | *
488 | * @return string
489 | */
490 | protected function removeLeadingBoolean($value)
491 | {
492 | return preg_replace('/and |or /i', '', $value, 1);
493 | }
494 |
495 | /**
496 | * @inheritdoc
497 | */
498 | public function getOperators()
499 | {
500 | return $this->operators;
501 | }
502 |
503 | /**
504 | * @inheritdoc
505 | */
506 | public function getFunctions()
507 | {
508 | return $this->functions;
509 | }
510 |
511 | /**
512 | * @inheritdoc
513 | */
514 | public function getOperatorsAndFunctions()
515 | {
516 | return array_merge($this->operators, $this->functions);
517 | }
518 |
519 | /**
520 | * Get the OData operator for the passed operator
521 | *
522 | * @param string $operator The passed operator
523 | *
524 | * @return string The OData operator
525 | */
526 | protected function getOperatorMapping($operator)
527 | {
528 | if (array_key_exists($operator, $this->operatorMapping)) {
529 | return $this->operatorMapping[$operator];
530 | }
531 | return $operator;
532 | }
533 |
534 | /**
535 | * @inheritdoc
536 | */
537 | public function prepareValue($value)
538 | {
539 | //$value = $this->parameter($value);
540 |
541 | // stringify all values if it has NOT an odata enum or special syntax primitive data type
542 | // (ex. Microsoft.OData.SampleService.Models.TripPin.PersonGender'Female' or datetime'1970-01-01T00:00:00')
543 | if (!preg_match("/^([\w]+\.)+([\w]+)(\'[\w]+\')$/", $value) && !$this->isSpecialPrimitiveDataType($value)) {
544 | // Check if the value is a string and NOT a date
545 | if (is_string($value) && !\DateTime::createFromFormat('Y-m-d\TH:i:sT', $value)) {
546 | $value = "'".$value."'";
547 | } else if(is_bool($value)){
548 | $value = $value ? 'true' : 'false';
549 | }
550 | }
551 |
552 | return $value;
553 | }
554 |
555 | /**
556 | * @inheritdoc
557 | */
558 | public function parameter($value)
559 | {
560 | return $this->isExpression($value) ? $this->getValue($value) : '?';
561 | }
562 |
563 | /**
564 | * @inheritdoc
565 | */
566 | public function isExpression($value)
567 | {
568 | return $value instanceof Expression;
569 | }
570 |
571 | /**
572 | * @inheritdoc
573 | */
574 | public function getValue($expression)
575 | {
576 | return $expression->getValue();
577 | }
578 |
579 | /**
580 | * Compile a nested where clause.
581 | *
582 | * @param Builder $query
583 | * @param array $where
584 | *
585 | * @return string
586 | */
587 | protected function whereNested(Builder $query, $where)
588 | {
589 | // Here we will calculate what portion of the string we need to remove. If this
590 | // is a join clause query, we need to remove the "on" portion of the SQL and
591 | // if it is a normal query we need to take the leading "$filter=" of queries.
592 | // $offset = $query instanceof JoinClause ? 3 : 6;
593 | $wheres = $this->compileWheres($where['query']);
594 | $offset = (substr($wheres, 0, 1) === '&') ? 9 : 8;
595 | return '('.substr($wheres, $offset).')';
596 | }
597 |
598 | /**
599 | * Compile a "where null" clause.
600 | *
601 | * @param Builder $query
602 | * @param array $where
603 | * @return string
604 | */
605 | protected function whereNull(Builder $query, $where)
606 | {
607 | return $where['column'] . ' eq null';
608 | }
609 |
610 | /**
611 | * Compile a "where not null" clause.
612 | *
613 | * @param Builder $query
614 | * @param array $where
615 | * @return string
616 | */
617 | protected function whereNotNull(Builder $query, $where)
618 | {
619 | return $where['column'] . ' ne null';
620 | }
621 |
622 | /**
623 | * Compile a "where in" clause.
624 | *
625 | * @param Builder $query
626 | * @param array $where
627 | * @return string
628 | */
629 | protected function whereIn(Builder $query, $where)
630 | {
631 | return $where['column'] . ' in (\'' . implode('\',\'', $where['list']) . '\')';
632 | }
633 |
634 | /**
635 | * Compile a "where not in" clause.
636 | *
637 | * @param Builder $query
638 | * @param array $where
639 | * @return string
640 | */
641 | protected function whereNotIn(Builder $query, $where)
642 | {
643 | return 'not(' . $where['column'] . ' in (\'' . implode('\',\'', $where['list']) . '\'))';
644 | }
645 |
646 | /**
647 | * Append query param to existing uri
648 | *
649 | * @param string $value
650 | * @return mixed
651 | */
652 | private function appendQueryParam(string $value)
653 | {
654 | //$param = $this->isFirstQueryParam ? $value : '&' . $value;
655 | //$this->isFirstQueryParam = false;
656 | return $value;
657 | }
658 | }
659 |
--------------------------------------------------------------------------------
/src/Query/IGrammar.php:
--------------------------------------------------------------------------------
1 | value();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/RequestHeader.php:
--------------------------------------------------------------------------------
1 | value();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/ResponseHeader.php:
--------------------------------------------------------------------------------
1 | value();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Uri.php:
--------------------------------------------------------------------------------
1 | parsed = $uriParsed;
44 | foreach(self::URI_PARTS as $uriPart) {
45 | if (isset($uriParsed[$uriPart])) {
46 | $this->$uriPart = $uriParsed[$uriPart];
47 | }
48 | }
49 | }
50 |
51 | public function __toString()
52 | {
53 | return http_build_url($this->parsed);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/Core/HelpersTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(
12 | is_uuid('4291e9f7-dea1-eb11-b1ac-000d3ab7a7ea'),
13 | 'Normal UUID'
14 | );
15 | $this->assertTrue(
16 | is_uuid('d9737e50-dad9-5b02-268b-4ddcf570108c'),
17 | 'Microsoft Dynamics CRM generated previously invalid UUID'
18 | );
19 | $this->assertFalse(
20 | is_uuid('!4291e9f7-dea1-eb11-b1ac-000d3ab7a7eaLOL'),
21 | 'Invalid prefix and suffix'
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/ODataClientTest.php:
--------------------------------------------------------------------------------
1 | baseUrl = 'https://services.odata.org/V4/TripPinService';
19 | }
20 |
21 | public function testODataClientConstructor()
22 | {
23 | $odataClient = new ODataClient($this->baseUrl);
24 | $this->assertNotNull($odataClient);
25 | $baseUrl = $odataClient->getBaseUrl();
26 | $this->assertEquals('https://services.odata.org/V4/TripPinService/', $baseUrl);
27 | }
28 |
29 | public function testODataClientEntitySetQuery()
30 | {
31 | $odataClient = new ODataClient($this->baseUrl);
32 | $this->assertNotNull($odataClient);
33 | $people = $odataClient->from('People')->get();
34 | $this->assertTrue(is_array($people->toArray()));
35 | }
36 |
37 | public function testODataClientEntitySetQueryWithSelect()
38 | {
39 | $odataClient = new ODataClient($this->baseUrl);
40 | $this->assertNotNull($odataClient);
41 | $people = $odataClient->select('FirstName','LastName')->from('People')->get();
42 | $this->assertTrue(is_array($people->toArray()));
43 | }
44 |
45 | public function testODataClientFromQueryWithWhere()
46 | {
47 | $odataClient = new ODataClient($this->baseUrl);
48 | $this->assertNotNull($odataClient);
49 | $people = $odataClient->from('People')->where('FirstName','Russell')->get();
50 | $this->assertTrue(is_array($people->toArray()));
51 | $this->assertEquals(1, $people->count());
52 | }
53 |
54 | public function testODataClientFromQueryWithWhereOrWhere()
55 | {
56 | $odataClient = new ODataClient($this->baseUrl);
57 | $this->assertNotNull($odataClient);
58 | $people = $odataClient->from('People')
59 | ->where('FirstName','Russell')
60 | ->orWhere('LastName','Ketchum')
61 | ->get();
62 | // dd($people);
63 | $this->assertTrue(is_array($people->toArray()));
64 | $this->assertEquals(2, $people->count());
65 | }
66 |
67 | public function testODataClientFromQueryWithWhereOrWhereArrays()
68 | {
69 | $odataClient = new ODataClient($this->baseUrl);
70 | $this->assertNotNull($odataClient);
71 | $people = $odataClient->from('People')
72 | ->where([
73 | ['FirstName', 'Russell'],
74 | ['LastName', 'Whyte'],
75 | ])
76 | ->orWhere([
77 | ['FirstName', 'Scott'],
78 | ['LastName', 'Ketchum'],
79 | ])
80 | ->get();
81 | $this->assertTrue(is_array($people->toArray()));
82 | $this->assertEquals(2, $people->count());
83 | }
84 |
85 | public function testODataClientFromQueryWithWhereOrWhereArraysAndOperators()
86 | {
87 | $odataClient = new ODataClient($this->baseUrl);
88 | $this->assertNotNull($odataClient);
89 | $people = $odataClient->from('People')
90 | ->where([
91 | ['FirstName', '=', 'Russell'],
92 | ['LastName', '=', 'Whyte'],
93 | ])
94 | ->orWhere([
95 | ['FirstName', '=', 'Scott'],
96 | ['LastName', '=', 'Ketchum'],
97 | ])
98 | ->get();
99 | $this->assertTrue(is_array($people->toArray()));
100 | $this->assertEquals(2, $people->count());
101 | }
102 |
103 | public function testODataClientFind()
104 | {
105 | $odataClient = new ODataClient($this->baseUrl);
106 | $this->assertNotNull($odataClient);
107 | $person = $odataClient->from('People')->find('russellwhyte');
108 | $this->assertEquals('russellwhyte', $person->UserName);
109 | }
110 |
111 | public function testODataClientSkipToken()
112 | {
113 | $pageSize = 8;
114 | $odataClient = new ODataClient($this->baseUrl, function($request) use($pageSize) {
115 | $request->headers[RequestHeader::PREFER] = Constants::ODATA_MAX_PAGE_SIZE . '=' . $pageSize;
116 | });
117 | $this->assertNotNull($odataClient);
118 | $odataClient->setEntityReturnType(false);
119 | $page1response = $odataClient->from('People')->get()->first();
120 | $page1results = collect($page1response->getResponseAsObject(Entity::class));
121 | $this->assertEquals($pageSize, $page1results->count());
122 |
123 | $page1skiptoken = $page1response->getSkipToken();
124 | if ($page1skiptoken) {
125 | $page2response = $odataClient->from('People')->skiptoken($page1skiptoken)->get()->first();
126 | $page2results = collect($page2response->getResponseAsObject(Entity::class));
127 | $page2skiptoken = $page2response->getSkipToken();
128 | $this->assertEquals($pageSize, $page2results->count());
129 | }
130 |
131 | $lastPageSize = 4;
132 | if ($page2skiptoken) {
133 | $page3response = $odataClient->from('People')->skiptoken($page2skiptoken)->get()->first();
134 | $page3results = collect($page3response->getResponseAsObject(Entity::class));
135 | $page3skiptoken = $page3response->getSkipToken();
136 | $this->assertEquals($lastPageSize, $page3results->count());
137 | $this->assertNull($page3skiptoken);
138 | }
139 | }
140 |
141 | public function testODataClientCursorBeLazyCollection()
142 | {
143 | $odataClient = new ODataClient($this->baseUrl);
144 |
145 | $pageSize = 8;
146 |
147 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
148 |
149 | $this->assertInstanceOf(LazyCollection::class, $data);
150 | }
151 |
152 | public function testODataClientCursorCountShouldEqualTotalEntitySetCount()
153 | {
154 | $odataClient = new ODataClient($this->baseUrl);
155 |
156 | $pageSize = 8;
157 |
158 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
159 |
160 | $expectedCount = 20;
161 |
162 | $this->assertEquals($expectedCount, $data->count());
163 | }
164 |
165 | public function testODataClientCursorToArrayCountShouldEqualPageSize()
166 | {
167 | $odataClient = new ODataClient($this->baseUrl);
168 |
169 | $pageSize = 8;
170 |
171 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
172 |
173 | $this->assertEquals($pageSize, count($data->toArray()));
174 | }
175 |
176 | public function testODataClientCursorFirstShouldReturnEntityRussellWhyte()
177 | {
178 | $odataClient = new ODataClient($this->baseUrl);
179 |
180 | $pageSize = 8;
181 |
182 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
183 |
184 | $first = $data->first();
185 | $this->assertInstanceOf(Entity::class, $first);
186 | $this->assertEquals('russellwhyte', $first->UserName);
187 | }
188 |
189 | public function testODataClientCursorLastShouldReturnEntityKristaKemp()
190 | {
191 | $odataClient = new ODataClient($this->baseUrl);
192 |
193 | $pageSize = 8;
194 |
195 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
196 |
197 | $last = $data->last();
198 | $this->assertInstanceOf(Entity::class, $last);
199 | $this->assertEquals('kristakemp', $last->UserName);
200 | }
201 |
202 | public function testODataClientCursorSkip1FirstShouldReturnEntityScottKetchum()
203 | {
204 | $odataClient = new ODataClient($this->baseUrl);
205 |
206 | $pageSize = 8;
207 |
208 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
209 |
210 | $second = $data->skip(1)->first();
211 | $this->assertInstanceOf(Entity::class, $second);
212 | $this->assertEquals('scottketchum', $second->UserName);
213 | }
214 |
215 | public function testODataClientCursorSkip4FirstShouldReturnEntityWillieAshmore()
216 | {
217 | $odataClient = new ODataClient($this->baseUrl);
218 |
219 | $pageSize = 8;
220 |
221 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
222 |
223 | $fifth = $data->skip(4)->first();
224 | $this->assertInstanceOf(Entity::class, $fifth);
225 | $this->assertEquals('willieashmore', $fifth->UserName);
226 | }
227 |
228 | public function testODataClientCursorSkip7FirstShouldReturnEntityKeithPinckney()
229 | {
230 | $odataClient = new ODataClient($this->baseUrl);
231 |
232 | $pageSize = 8;
233 |
234 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
235 |
236 | $eighth = $data->skip(7)->first();
237 | $this->assertInstanceOf(Entity::class, $eighth);
238 | $this->assertEquals('keithpinckney', $eighth->UserName);
239 | }
240 |
241 | public function testODataClientCursorSkip8FirstShouldReturnEntityMarshallGaray()
242 | {
243 | $odataClient = new ODataClient($this->baseUrl);
244 |
245 | $pageSize = 8;
246 |
247 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
248 |
249 | $ninth = $data->skip(8)->first();
250 | $this->assertInstanceOf(Entity::class, $ninth);
251 | $this->assertEquals('marshallgaray', $ninth->UserName);
252 | }
253 |
254 | public function testODataClientCursorSkip16FirstShouldReturnEntitySandyOsbord()
255 | {
256 | $odataClient = new ODataClient($this->baseUrl);
257 |
258 | $pageSize = 8;
259 |
260 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
261 |
262 | $seventeenth = $data->skip(16)->first();
263 | $this->assertInstanceOf(Entity::class, $seventeenth);
264 | $this->assertEquals('sandyosborn', $seventeenth->UserName);
265 | }
266 |
267 | public function testODataClientCursorSkip16LastPageShouldBe4Records()
268 | {
269 | $odataClient = new ODataClient($this->baseUrl);
270 |
271 | $pageSize = 8;
272 |
273 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
274 |
275 | $lastPage = $data->skip(16);
276 | $lastPageSize = 4;
277 | $this->assertEquals($lastPageSize, count($lastPage->toArray()));
278 | }
279 |
280 | public function testODataClientCursorIteratingShouldReturnAll20Entities()
281 | {
282 | $odataClient = new ODataClient($this->baseUrl);
283 |
284 | $pageSize = 8;
285 |
286 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
287 |
288 | $expectedCount = 20;
289 | $counter = 0;
290 |
291 | $data->each(function ($person) use(&$counter) {
292 | $counter++;
293 | $this->assertInstanceOf(Entity::class, $person);
294 | });
295 |
296 | $this->assertEquals($expectedCount, $counter);
297 | }
298 |
299 | public function testODataClientCursorPageSizeOf20ShouldReturnAllEntities()
300 | {
301 | $odataClient = new ODataClient($this->baseUrl);
302 |
303 | $pageSize = 20;
304 |
305 | $data = $odataClient->from('People')->pageSize($pageSize)->cursor();
306 |
307 | $this->assertEquals($pageSize, count($data->toArray()));
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/tests/Query/BuilderTest.php:
--------------------------------------------------------------------------------
1 | baseUrl = 'https://services.odata.org/V4/TripPinService';
21 | $this->client = new ODataClient($this->baseUrl);
22 | }
23 |
24 | public function getBuilder()
25 | {
26 | return new Builder(
27 | $this->client, $this->client->getQueryGrammar(), $this->client->getPostProcessor()
28 | );
29 | }
30 |
31 | public function testConstructor()
32 | {
33 | $builder = $this->getBuilder();
34 |
35 | $this->assertNotNull($builder);
36 | }
37 |
38 | public function testEntitySet()
39 | {
40 | $builder = $this->getBuilder();
41 |
42 | $entitySet = 'People';
43 |
44 | $builder->from($entitySet);
45 |
46 | $expected = $entitySet;
47 | $actual = $builder->entitySet;
48 |
49 | $this->assertEquals($expected, $actual);
50 |
51 | $request = $builder->toRequest();
52 | $this->assertEquals($expected, $request);
53 | }
54 |
55 | public function testNoEntitySetFind()
56 | {
57 | $this->expectException(ODataQueryException::class);
58 |
59 | $builder = $this->getBuilder();
60 | $builder->find('russellwhyte');
61 | }
62 |
63 | public function testEntitySetFindStringKey()
64 | {
65 | $builder = $this->getBuilder();
66 |
67 | $entitySet = 'People';
68 |
69 | $builder->from($entitySet);
70 |
71 | $builder->whereKey('russellwhyte');
72 |
73 | $expected = $entitySet.'(\'russellwhyte\')';
74 | $actual = $builder->toRequest();
75 |
76 | $this->assertEquals($expected, $actual);
77 | }
78 |
79 | public function testEntitySetFindNumericKey()
80 | {
81 | $builder = $this->getBuilder();
82 |
83 | $entitySet = 'EntitySet';
84 |
85 | $builder->from($entitySet);
86 |
87 | $builder->whereKey(12345);
88 |
89 | $expected = $entitySet.'(12345)';
90 | $actual = $builder->toRequest();
91 |
92 | $this->assertEquals($expected, $actual);
93 | }
94 |
95 | public function testEntitySetWithSelect()
96 | {
97 | $builder = $this->getBuilder();
98 |
99 | $entitySet = 'People';
100 |
101 | $builder->select('FirstName','LastName')->from($entitySet);
102 |
103 | $expected = $entitySet.'?$select=FirstName,LastName';
104 |
105 | $request = $builder->toRequest();
106 |
107 | $this->assertEquals($expected, $request);
108 | }
109 |
110 | public function testEntitySetCount()
111 | {
112 | $builder = $this->getBuilder();
113 |
114 | $entitySet = 'People';
115 |
116 | $expected = 20;
117 |
118 | $actual = $builder->from($entitySet)->count();
119 |
120 | $this->assertTrue(is_numeric($actual));
121 | $this->assertTrue($actual > 0);
122 | $this->assertEquals($expected, $actual);
123 | }
124 |
125 | // public function testEntitySetCountWithWhere()
126 | // {
127 | // $builder = $this->getBuilder();
128 |
129 | // $entitySet = 'People';
130 |
131 | // $expected = 1;
132 |
133 | // $actual = $builder->from($entitySet)->where('FirstName','Russell')->get(QueryOptions::INCLUDE_REF | QueryOptions::INCLUDE_COUNT);
134 |
135 | // $this->assertTrue(is_numeric($actual));
136 | // $this->assertTrue($actual > 0);
137 | // $this->assertEquals($expected, $actual);
138 | // }
139 |
140 | public function testEntitySetGet()
141 | {
142 | $builder = $this->getBuilder();
143 |
144 | $entitySet = 'People';
145 |
146 | $people = $builder->from($entitySet)->get();
147 |
148 | // dd($people);
149 | $this->assertTrue(is_array($people->toArray()));
150 | //$this->assertInstanceOf(Collection::class, $people);
151 | //$this->assertEquals($expected, $request);
152 | }
153 |
154 | public function testEntitySetGetWhere()
155 | {
156 | $builder = $this->getBuilder();
157 |
158 | $entitySet = 'People';
159 |
160 | $people = $builder->from($entitySet)->where('FirstName','Russell')->get();
161 |
162 | // dd($people);
163 | $this->assertTrue(is_array($people->toArray()));
164 | $this->assertTrue($people->count() == 1);
165 | //$this->assertInstanceOf(Collection::class, $people);
166 | //$this->assertEquals($expected, $request);
167 | }
168 |
169 | public function testEntitySetGetWhereOrWhere()
170 | {
171 | $builder = $this->getBuilder();
172 |
173 | $entitySet = 'People';
174 |
175 | $people = $builder->from($entitySet)->where('FirstName','Russell')->orWhere('LastName','Ketchum')->get();
176 |
177 | //dd($people);
178 | $this->assertTrue(is_array($people->toArray()));
179 | $this->assertTrue($people->count() == 2);
180 | //$this->assertInstanceOf(Collection::class, $people);
181 | //$this->assertEquals($expected, $request);
182 | }
183 |
184 | public function testEntitySetGetWhereNested()
185 | {
186 | $builder = $this->getBuilder();
187 |
188 | $entitySet = 'People';
189 |
190 | $people = $builder->from($entitySet)->where('FirstName','Russell')->orWhere(function($query) {
191 | $query->where('LastName','Ketchum')
192 | ->where('FirstName','Scott');
193 | })->get();
194 |
195 | //dd($people);
196 | $this->assertTrue(is_array($people->toArray()));
197 | $this->assertTrue($people->count() == 2);
198 | //$this->assertInstanceOf(Collection::class, $people);
199 | //$this->assertEquals($expected, $request);
200 | }
201 |
202 | public function testEntityKeyString()
203 | {
204 | $builder = $this->getBuilder();
205 |
206 | $entityId = 'russellwhyte';
207 |
208 | $builder->whereKey($entityId);
209 |
210 | $expected = $entityId;
211 | $actual = $builder->entityKey;
212 |
213 | $this->assertEquals($expected, $actual);
214 |
215 | $expectedUri = "('$entityId')";
216 | $actualUri = $builder->toRequest();
217 |
218 | $this->assertEquals($expectedUri, $actualUri);
219 | }
220 |
221 | public function testEntityKeyNumeric()
222 | {
223 | $builder = $this->getBuilder();
224 |
225 | $entityId = 1;
226 |
227 | $builder->whereKey($entityId);
228 |
229 | $expected = $entityId;
230 | $actual = $builder->entityKey;
231 |
232 | $this->assertEquals($expected, $actual);
233 |
234 | $expectedUri = "($entityId)";
235 | $actualUri = $builder->toRequest();
236 |
237 | $this->assertEquals($expectedUri, $actualUri);
238 | }
239 |
240 | public function testEntityKeyUuid()
241 | {
242 | $builder = $this->getBuilder();
243 |
244 | $entityId = 'c78ae94b-0983-e511-80e5-3863bb35ddb8';
245 |
246 | $builder->whereKey($entityId);
247 |
248 | $expected = $entityId;
249 | $actual = $builder->entityKey;
250 |
251 | $this->assertEquals($expected, $actual);
252 |
253 | $expectedUri = "($entityId)";
254 | $actualUri = $builder->toRequest();
255 |
256 | $this->assertEquals($expectedUri, $actualUri);
257 | }
258 |
259 | public function testEntityKeyUuidNegative()
260 | {
261 | $builder = $this->getBuilder();
262 |
263 | $entityId = 'k78ae94b-0983-t511-80e5-3863bb35ddb8';
264 |
265 | $builder->whereKey($entityId);
266 |
267 | $expected = $entityId;
268 | $actual = $builder->entityKey;
269 |
270 | $this->assertEquals($expected, $actual);
271 |
272 | $expectedUri = "('$entityId')";
273 | $actualUri = $builder->toRequest();
274 |
275 | $this->assertEquals($expectedUri, $actualUri);
276 | }
277 |
278 | public function testEntityKeyComposite()
279 | {
280 | $builder = $this->getBuilder();
281 |
282 | $compositeKey = [
283 | 'Property1' => 'Value1',
284 | 'Property2' => 'Value2',
285 | ];
286 |
287 | $builder->whereKey($compositeKey);
288 |
289 | $expectedUri = "(Property1='Value1',Property2='Value2')";
290 | $actualUri = $builder->toRequest();
291 |
292 | $this->assertEquals($expectedUri, $actualUri);
293 | }
294 |
295 | public function testTake()
296 | {
297 | $builder = $this->getBuilder();
298 |
299 | $take = 1;
300 |
301 | $builder->take($take);
302 |
303 | $expected = $take;
304 | $actual = $builder->take;
305 |
306 | $this->assertEquals($expected, $actual);
307 | }
308 |
309 | public function testSkip()
310 | {
311 | $builder = $this->getBuilder();
312 |
313 | $skip = 5;
314 |
315 | $builder->skip($skip);
316 |
317 | $expected = $skip;
318 | $actual = $builder->skip;
319 |
320 | $this->assertEquals($expected, $actual);
321 | }
322 |
323 | public function testOrderColumnOnly()
324 | {
325 | $builder = $this->getBuilder();
326 |
327 | $builder->order('Name'); // default asc
328 |
329 | $expectedUri = '$orderby=Name asc';
330 | $actualUri = $builder->toRequest();
331 |
332 | $this->assertEquals($expectedUri, $actualUri);
333 | }
334 |
335 | public function testOrderWithDirection()
336 | {
337 | $builder = $this->getBuilder();
338 |
339 | $builder->order('Name', 'desc');
340 |
341 | $expectedUri = '$orderby=Name desc';
342 | $actualUri = $builder->toRequest();
343 |
344 | $this->assertEquals($expectedUri, $actualUri);
345 | }
346 |
347 | public function testOrderWithShortArray()
348 | {
349 | $builder = $this->getBuilder();
350 |
351 | $builder->order(['Name', 'desc']);
352 |
353 | $expectedUri = '$orderby=Name desc';
354 | $actualUri = $builder->toRequest();
355 |
356 | $this->assertEquals($expectedUri, $actualUri);
357 | }
358 |
359 | public function testOrderWithMultipleShortArray()
360 | {
361 | $builder = $this->getBuilder();
362 |
363 | $builder->order(['Id', 'asc'], ['Name', 'desc']);
364 |
365 | $expectedUri = '$orderby=Id asc,Name desc';
366 | $actualUri = $builder->toRequest();
367 |
368 | $this->assertEquals($expectedUri, $actualUri);
369 | }
370 |
371 | public function testOrderWithMultipleNestedShortArray()
372 | {
373 | $builder = $this->getBuilder();
374 |
375 | $builder->order(array(['Id', 'asc'], ['Name', 'desc']));
376 |
377 | $expectedUri = '$orderby=Id asc,Name desc';
378 | $actualUri = $builder->toRequest();
379 |
380 | $this->assertEquals($expectedUri, $actualUri);
381 | }
382 |
383 | public function testOrderWithArray()
384 | {
385 | $builder = $this->getBuilder();
386 |
387 | $builder->order(['column' => 'Name', 'direction' => 'desc']);
388 |
389 | $expectedUri = '$orderby=Name desc';
390 | $actualUri = $builder->toRequest();
391 |
392 | $this->assertEquals($expectedUri, $actualUri);
393 | }
394 |
395 | public function testOrderWithMultipleArray()
396 | {
397 | $builder = $this->getBuilder();
398 |
399 | $builder->order(['column' => 'Id', 'direction' => 'asc'], ['column' => 'Name', 'direction' => 'desc']);
400 |
401 | $expectedUri = '$orderby=Id asc,Name desc';
402 | $actualUri = $builder->toRequest();
403 |
404 | $this->assertEquals($expectedUri, $actualUri);
405 | }
406 |
407 | public function testOrderWithMultipleNestedArray()
408 | {
409 | $builder = $this->getBuilder();
410 |
411 | $builder->order(array(['column' => 'Id', 'direction' => 'asc'], ['column' => 'Name', 'direction' => 'desc']));
412 |
413 | $expectedUri = '$orderby=Id asc,Name desc';
414 | $actualUri = $builder->toRequest();
415 |
416 | $this->assertEquals($expectedUri, $actualUri);
417 | }
418 |
419 | public function testMultipleChainedQueryParams()
420 | {
421 | $builder = $this->getBuilder();
422 |
423 | $entitySet = 'People';
424 |
425 | $builder->from($entitySet)
426 | ->select('Name,Gender')
427 | ->where('Gender', '=', 'Female')
428 | ->order('Name', 'desc')
429 | ->take(5);
430 |
431 | $expectedUri = 'People?$select=Name,Gender&$filter=Gender eq \'Female\'&$orderby=Name desc&$top=5';
432 | $actualUri = $builder->toRequest();
433 |
434 | $this->assertEquals($expectedUri, $actualUri);
435 | }
436 |
437 | public function testEntityWithWhereEnum()
438 | {
439 | $builder = $this->getBuilder();
440 |
441 | $entitySet = 'People';
442 | $whereEnum = 'Microsoft.OData.Service.Sample.TrippinInMemory.Models.PersonGender\'Female\'';
443 |
444 | $builder->from($entitySet)
445 | ->where('Gender', '=', $whereEnum);
446 |
447 | $expectedUri = 'People?$filter=Gender eq Microsoft.OData.Service.Sample.TrippinInMemory.Models.PersonGender\'Female\'';
448 | $actualUri = $builder->toRequest();
449 |
450 | $this->assertEquals($expectedUri, $actualUri);
451 | }
452 |
453 | public function testEntityWithSingleExpand()
454 | {
455 | $builder = $this->getBuilder();
456 |
457 | $entitySet = 'People';
458 | $expand = 'PersonGender';
459 |
460 | $builder->from($entitySet)
461 | ->expand($expand);
462 |
463 | $expectedUri = 'People?$expand=PersonGender';
464 | $actualUri = $builder->toRequest();
465 |
466 | $this->assertEquals($expectedUri, $actualUri);
467 | }
468 |
469 | public function testEntityWithMultipleExpand()
470 | {
471 | $builder = $this->getBuilder();
472 |
473 | $entitySet = 'People';
474 | $expands = ['PersonGender', 'PersonOccupation'];
475 |
476 | $builder->from($entitySet)
477 | ->expand($expands);
478 |
479 | $expectedUri = 'People?$expand=PersonGender,PersonOccupation';
480 | $actualUri = $builder->toRequest();
481 |
482 | $this->assertEquals($expectedUri, $actualUri);
483 | }
484 |
485 | public function testEntityWithWhereColumn()
486 | {
487 | $builder = $this->getBuilder();
488 |
489 | $entitySet = 'People';
490 |
491 | $builder->from($entitySet)
492 | ->whereColumn('FirstName', 'LastName');
493 |
494 | $expectedUri = 'People?$filter=FirstName eq LastName';
495 | $actualUri = $builder->toRequest();
496 |
497 | $this->assertEquals($expectedUri, $actualUri);
498 | }
499 |
500 | public function testEntityWithOrWhereColumnO()
501 | {
502 | $builder = $this->getBuilder();
503 |
504 | $entitySet = 'People';
505 |
506 | $builder->from($entitySet)
507 | ->where('FirstName', '=', 'Russell')
508 | ->orWhereColumn('FirstName', 'LastName');
509 |
510 | $expectedUri = 'People?$filter=FirstName eq \'Russell\' or FirstName eq LastName';
511 | $actualUri = $builder->toRequest();
512 |
513 | $this->assertEquals($expectedUri, $actualUri);
514 | }
515 |
516 | public function testEntityWithWhereNull()
517 | {
518 | $builder = $this->getBuilder();
519 |
520 | $entitySet = 'People';
521 |
522 | $builder->from($entitySet)
523 | ->whereNull('FirstName');
524 |
525 | $expectedUri = 'People?$filter=FirstName eq null';
526 | $actualUri = $builder->toRequest();
527 |
528 | $this->assertEquals($expectedUri, $actualUri);
529 | }
530 |
531 | public function testEntityWithWhereNotNull()
532 | {
533 | $builder = $this->getBuilder();
534 |
535 | $entitySet = 'People';
536 |
537 | $builder->from($entitySet)
538 | ->whereNotNull('FirstName');
539 |
540 | $expectedUri = 'People?$filter=FirstName ne null';
541 | $actualUri = $builder->toRequest();
542 |
543 | $this->assertEquals($expectedUri, $actualUri);
544 | }
545 |
546 | public function testEntityWithWhereIn()
547 | {
548 | $builder = $this->getBuilder();
549 |
550 | $entitySet = 'People';
551 |
552 | $builder->from($entitySet)
553 | ->whereIn('FirstName', ['John', 'Jane']);
554 |
555 | $expectedUri = 'People?$filter=FirstName in (\'John\',\'Jane\')';
556 | $actualUri = $builder->toRequest();
557 |
558 | $this->assertEquals($expectedUri, $actualUri);
559 | }
560 |
561 | public function testEntityWithWhereNotIn()
562 | {
563 | $builder = $this->getBuilder();
564 |
565 | $entitySet = 'People';
566 |
567 | $builder->from($entitySet)
568 | ->whereNotIn('FirstName', ['John', 'Jane']);
569 |
570 | $expectedUri = 'People?$filter=not(FirstName in (\'John\',\'Jane\'))';
571 | $actualUri = $builder->toRequest();
572 |
573 | $this->assertEquals($expectedUri, $actualUri);
574 | }
575 |
576 | public function testEntityWhereString()
577 | {
578 | $builder = $this->getBuilder();
579 |
580 | $entitySet = 'People';
581 |
582 | $builder->from($entitySet)
583 | ->where('FirstName', 'Russell');
584 |
585 | $expectedUri = 'People?$filter=FirstName eq \'Russell\'';
586 | $actualUri = $builder->toRequest();
587 |
588 | $this->assertEquals($expectedUri, $actualUri);
589 | }
590 |
591 | public function testEntityWhereNumeric()
592 | {
593 | $builder = $this->getBuilder();
594 |
595 | $entitySet = 'Photos';
596 |
597 | $builder->from($entitySet)
598 | ->where('Id', 1);
599 |
600 | $expectedUri = 'Photos?$filter=Id eq 1';
601 | $actualUri = $builder->toRequest();
602 |
603 | $this->assertEquals($expectedUri, $actualUri);
604 | }
605 |
606 | public function testEntityMultipleWheres()
607 | {
608 | $builder = $this->getBuilder();
609 |
610 | $entitySet = 'People';
611 |
612 | $builder->from($entitySet)
613 | ->where('FirstName', 'Russell')
614 | ->where('LastName', 'Whyte');
615 |
616 | $expectedUri = 'People?$filter=FirstName eq \'Russell\' and LastName eq \'Whyte\'';
617 | $actualUri = $builder->toRequest();
618 |
619 | $this->assertEquals($expectedUri, $actualUri);
620 | }
621 |
622 | public function testEntityMultipleWheresArray()
623 | {
624 | $builder = $this->getBuilder();
625 |
626 | $entitySet = 'People';
627 |
628 | $builder->from($entitySet)
629 | ->where([
630 | ['FirstName', 'Russell'],
631 | ['LastName', 'Whyte'],
632 | ]);
633 |
634 | $expectedUri = 'People?$filter=(FirstName eq \'Russell\' and LastName eq \'Whyte\')';
635 | $actualUri = $builder->toRequest();
636 |
637 | $this->assertEquals($expectedUri, $actualUri);
638 | }
639 |
640 | public function testEntityMultipleWheresArrayWithSelect()
641 | {
642 | $builder = $this->getBuilder();
643 |
644 | $entitySet = 'People';
645 |
646 | $builder->from($entitySet)
647 | ->select('Name')
648 | ->where([
649 | ['FirstName', 'Russell'],
650 | ['LastName', 'Whyte'],
651 | ]);
652 |
653 | $expectedUri = 'People?$select=Name&$filter=(FirstName eq \'Russell\' and LastName eq \'Whyte\')';
654 | $actualUri = $builder->toRequest();
655 |
656 | $this->assertEquals($expectedUri, $actualUri);
657 | }
658 |
659 | public function testEntityMultipleWheresNested()
660 | {
661 | $builder = $this->getBuilder();
662 |
663 | $entitySet = 'People';
664 |
665 | $builder->from($entitySet)
666 | ->where(function($query) {
667 | $query->where('FirstName','Russell');
668 | $query->where('LastName','Whyte');
669 | });
670 |
671 | $expectedUri = 'People?$filter=(FirstName eq \'Russell\' and LastName eq \'Whyte\')';
672 | $actualUri = $builder->toRequest();
673 |
674 | $this->assertEquals($expectedUri, $actualUri);
675 | }
676 |
677 | public function testEntityMultipleWheresNestedWithSelect()
678 | {
679 | $builder = $this->getBuilder();
680 |
681 | $entitySet = 'People';
682 |
683 | $builder->from($entitySet)
684 | ->select('Name')
685 | ->where(function($query) {
686 | $query->where('FirstName','Russell');
687 | $query->where('LastName','Whyte');
688 | });
689 |
690 | $expectedUri = 'People?$select=Name&$filter=(FirstName eq \'Russell\' and LastName eq \'Whyte\')';
691 | $actualUri = $builder->toRequest();
692 |
693 | $this->assertEquals($expectedUri, $actualUri);
694 | }
695 |
696 | }
697 |
--------------------------------------------------------------------------------