├── .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 | [![Build Status](https://github.com/saintsystems/odata-client-php/actions/workflows/ci.yml/badge.svg)](https://github.com/saintsystems/odata-client-php/actions/workflows/ci.yml) 8 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/saintsystems/odata-client.svg?style=flat-square)](https://packagist.org/packages/saintsystems/odata-client) 9 | [![Total Downloads](https://img.shields.io/packagist/dt/saintsystems/odata-client.svg?style=flat-square)](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 | --------------------------------------------------------------------------------